Реалізація узагальненого варіанту IHierarchicalDataSource, Різне, Програмування, статті

Завдання відображення ієрархічних даних досить часто зустрічається в практиці. Це і файлова структура (папка-> папка-> … -> файл), і організаційна структура компанії (департамент-> відділ-> група), і ієрархія підпорядкування співробітників (директор-> менеджер-> рядовий співробітник), і проектна діяльність (група проектів-> проект-> підсистема-> задача).


У стандартному постачанні ASP.Net є два основних контрола, які підтримують відображення ієрархічних даних: asp:TreeView і asp:Menu. Крім того, вони також підтримують Біндінг з ієрархічним джерелом даних, тобто з таким джерелом, який реалізує інтерфейс IHierarchicalDataSource.


Джерел, які реалізують інтерфейс IHierarchicalDataSource, Існує всього два, це: XmlDataSource і SiteMapDataSource. Крім того, ці джерела одночасно є декларативними елементами UI, які можна використовувати в розмітці.


Наприклад:


XmlDataSource.aspx


<%@ Page 
    Language=”C#”
    MasterPageFile=”~/Shared/Site.Master”
    AutoEventWireup=”true” %>
<asp:Content ContentPlaceHolderID=”BodyPlaceHolder” runat=”server”>
    <asp:TreeView ID=”treeView” runat=”server”
        DataSourceID=”xmlDataSource”>
        <DataBindings>
            <asp:TreeNodeBinding 
                DataMember=”Root” 
                Text=”Root”
                />
            <asp:TreeNodeBinding 
                DataMember=”Node” 
                TextField=”Name” />
        </DataBindings>
    </asp:TreeView>
    <asp:XmlDataSource ID=”xmlDataSource” runat=”server”>
        <Data>
            <Root>
                <Node Name=”Node 1″ />
                <Node Name=”Node 2″>
                    <Node Name=”Node 2.1″ />
                    <Node Name=”Node 2.2″ />
                </Node>
                <Node Name=”Node 1″ />
            </Root>
        </Data>
    </asp:XmlDataSource>
</asp:Content>

Примітка: Більш детальний приклад використання Біндінга asp: TreeView до XmlDataSource можна подивитися в такій замітці.


А що, якщо потрібно відобразити дані отримані з бази даних? Для цього доведеться самостійно реалізовувати зазначений інтерфейс IHierarchicalDataSource.


Модель


Розглянемо типову модель деревовидних даних зберігаються в БД:


 Скріншот: Таблиця в базі даних з типовою деревоподібної структурою


Як видно на скріншоті, модель являє собою таблицю з наступними полями: ID – унікальний ідентифікатор запису, ParentID – ключ для зв’язку з батьківською записом або NULL – якщо запис є кореневою.


Після отримання даних, деревоподібні дані відображаються на конкретну модель. Розглянемо дві найбільш часто використовуваних моделі.


Перша – це прямолінійна проекція структури БД на об’єкт:


 Скріншот: діграмм класів для PlainTreeModel


Другий варіант зазвичай виникає при використанні просунутих засобів відображення (ORM):


Скріншот: Діаграма класів для ORMTreeModel


В даному випадку, бачимо що замість одного поля ParentID створено два додаткових поля: Parent і Children. Parent – Це посилання на батьківський елемент ORMTreeModel, А Children – Перебирає послідовність дочірніх об’єктів ORMTreeModel.


Уявімо собі, що нам потрібно відобразити дані змодельовані подібним чином у контролі asp: TreeView. Для цього буде потрібно реалізувати інтерфейс IHierarchicalDataSource.


Реалізація інтерфейсу IHierarchicalDataSource


Будь реалізація IHierarchicalDataSource безпосередньо пов’язана з реалізацією абстрактного класу HierarchicalDataSourceView і інтерфейсів IHierarchicalEnumerable і IHierarchyData.


Ось як це виглядає у вигляді діаграми класів:


Скріншот: Діаграма класів для реалізації IHierarchicalDataSource


Центральною ланкою реалізації є IHierarchyData, який і абстрагує модель деревовидних даних.


Примітка: На скріншоті є одна відома помилка діаграми класів генерується Visual Studio. Дизайнер діаграм показує поля з ім’ям Item перейменованими в this. Що добре видно в інтерфейсі IHierarchyData. В даному випадку, це звичайно ж невірно.


Примітка: Також зверніть увагу, що інтерфейс IHierarchyData багато в чому повторює клас ORMTreeModel наведений вище.


Тепер визначимося з завданням, яке нам належить вирішити. Потрібно отримувати деревовидні дані із зовнішнього джерела даних і перетворювати їх до ієрархічним джерела даних. Бажано абстрагуватися від конкретного джерела і конкретного виду моделі даних.


Типізовані обгортки для базових інтерфейсів


Для початку, зробимо деякі приготування і реалізуємо типізовані версії інтерфейсів IHierarchyData і IHierarchyEnumerable:


HierarchyData.cs


using System.Web.UI;
 
namespace Home.Andir.Examples
{
    public abstract class HierarchyData<T> : IHierarchyData
    {
        public abstract HierarchyData<T> GetParent();
        public abstract HierarchicalEnumerable<T> GetChildren();
        public abstract T Item { get; }
 
        #region IHierarchyData Members
 
        IHierarchyData IHierarchyData.GetParent()
        {
            return GetParent();
        }
 
        public abstract bool HasChildren { get; }
 
        IHierarchicalEnumerable IHierarchyData.GetChildren()
        {
            return GetChildren();
        }
 
        object IHierarchyData.Item
        {
            get { return Item; }
        }
 
        public abstract string Path { get; }
        public abstract string Type { get; }
 
        #endregion
    }
}

HierarchyEnumerable.cs


using System.Collections;
using System.Web.UI;
 
namespace Home.Andir.Examples
{
    public abstract class HierarchicalEnumerable<T> 
        : IHierarchicalEnumerable
    {
        public abstract HierarchyData<T> GetHierarchyData(
            T enumeratedItem);
 
        #region IHierarchicalEnumerable Members
 
        IHierarchyData IHierarchicalEnumerable.GetHierarchyData(
            object enumeratedItem)
        {
            return GetHierarchyData((T)enumeratedItem);
        }
 
        #endregion
 
        #region IEnumerable Members
 
        public abstract IEnumerator GetEnumerator();
 
        #endregion
    }
}

Все максимально прямолінійно, просто ховаємо нетипізовані версії за явною реалізацією інтерфейсу, а назовні виставляємо лише типізовані версії цих же методів і властивостей.


Репозиторій


Визначимо інтерфейс для узагальненого сховища об’єктів, з якого ми будемо отримувати дані для ієрархічного обходу:


IHierarchyDataRepository.cs


using System.Collections.Generic;
 
namespace Home.Andir.Examples
{
    public interface IHierarchyDataRepository<T> 
        where T : class
    {
        T GetParent(T item);
        IEnumerable<T> GetChildren(T item);
        string GetItemType(T item);
 
        T GetItem(string hierarchyPath);
        string GetItemHierarchyPath(
            string parentHierarchyPath, T item);
    }
}

Для чого потрібен цей інтерфейс? Він призначений для абстрагування від конкретних джерел ієрархічних даних і його реалізації повинні бути прив’язані до деякого зовнішнього джерела.


Примітка: Необхідно звернути увагу, що в даному варіанті інтерфейсу і подальшої реалізації використовується неявне припущення для методу GetChildren: якщо переданий параметр із значенням null, То реалізація повинна повернути список кореневих елементів дерева. Взагалі, це не дуже гарне рішення, і, з точки зору архітектури, набагато краще мати окремий метод для отримання кореневих елементів дерева.


Реалізацію репозиторію поки відкладемо і реалізуємо необхідні інтерфейси для IHierarchicalDataSource припускаючи, що дані надходять до нас з деякого репозиторія IHierarchyDataRepository.


GenericHierarchicalDataSource.cs


using System;
using System.Web.UI;
 
namespace Home.Andir.Examples
{
    public sealed class GenericHierarchicalDataSource<T>
        : IHierarchicalDataSource 
        where T : class
    {
        readonly IHierarchyDataRepository<T> repository;
 
        public GenericHierarchicalDataSource(
            IHierarchyDataRepository<T> repository)
        {
            this.repository = repository;
        }
 
        #region IHierarchicalDataSource Members
 
        public event EventHandler DataSourceChanged;
 
        public HierarchicalDataSourceView GetHierarchicalView(
            string viewPath)
        {
            return new GenericHierarchicalDataSourceView<T>(
                repository, viewPath);
        }
 
        #endregion
    }
}

Отже, сам IHierarchicalDataSource досить примітивний: приймає в якості параметрів репозиторій і передає його далі – реалізації абстрактного HierarchicalDataSourceView.


GenericHierarchicalDataSourceView.cs


using System.Web.UI;
 
namespace Home.Andir.Examples
{
    public sealed class GenericHierarchicalDataSourceView<T> 
        : HierarchicalDataSourceView 
        where T : class
    {
        readonly IHierarchyDataRepository<T> repository;
        readonly string viewPath;
 
        public GenericHierarchicalDataSourceView(
            IHierarchyDataRepository<T> repository, string viewPath)
        {
            this.repository = repository;
            this.viewPath = viewPath;
        }
 
        public override IHierarchicalEnumerable Select()
        {
            if (!string.IsNullOrEmpty(viewPath))
            {
                var hierarchyItem = new GenericHierarchyData<T>(
                    repository, viewPath);
                return hierarchyItem.GetChildren();
            }
 
            return new GenericHierarchicalEnumerable<T>(
                repository, null, repository.GetChildren(null));
        }
    }
}

Тут потрібно звернути увагу на параметр конструктора viewPath, Який являє собою початковий шлях обходу ієрархічного джерела даних.


В іншому реалізація GenericDataSourceView також не представляє інтересу, все передається на відкуп двом залишилися інтерфейсів.


GenericHierarchicalEnumerable.cs


using System.Collections;
using System.Collections.Generic;
 
namespace Home.Andir.Examples
{
    public sealed class GenericHierarchicalEnumerable<T>
        : HierarchicalEnumerable<T> 
        where T : class
    {
        readonly IHierarchyDataRepository<T> repository;
        readonly HierarchyData<T> parent;
        readonly IEnumerable<T> enumerableList;
 
        public GenericHierarchicalEnumerable(
            IHierarchyDataRepository<T> repository,
            HierarchyData<T> parent,
            IEnumerable<T> enumerableList
            )
        {
            this.repository = repository;
            this.parent = parent;
            this.enumerableList = enumerableList;
        }
 
        #region IHierarchicalEnumerable Members
 
        public override HierarchyData<T> GetHierarchyData(T item)
        {
            return new GenericHierarchyData<T>(
                repository, parent, item);
        }
 
        #endregion
 
        #region IEnumerable Members
 
        public override IEnumerator GetEnumerator()
        {
            return enumerableList.GetEnumerator();
        }
 
        #endregion
    }
}

Цей інтерфейс є розширення абстракції IEnumerable – перечислимого даних – до абстракції перерахування дерева з можливістю просування від деякого кореневого елемента дерева до його листя.


IHierarchicalEnumerable відрізняється від звичайного IEnumerable тільки додатковим методом GetHierarchyData, Який призначений для конвертації перебираються елементів в реалізацію ієрархічної моделі HierarchyData.


GenericHierarchyData.cs


using System.Collections.Generic;
using System.Linq;
 
namespace Home.Andir.Examples
{
    public sealed class GenericHierarchyData<T> 
        : HierarchyData<T> 
        where T : class
    {
        readonly IHierarchyDataRepository<T> repository;
 
        readonly T item;
        readonly HierarchyData<T> parent;
        readonly IList<T> children;
        readonly string path;
        readonly string type;
 
        public GenericHierarchyData(
            IHierarchyDataRepository<T> repository,
            string itemPath)
        {
            this.repository = repository;
 
            this.item = repository.GetItem(itemPath);
            this.parent = null;
            this.children = repository.GetChildren(item).ToList();
            this.path = itemPath;
            this.type = repository.GetItemType(item);
        }
 
        public GenericHierarchyData(
            IHierarchyDataRepository<T> repository,
            HierarchyData<T> parent,
            T item
            )
        {
            this.repository = repository;
 
            this.item = item;
            this.parent = parent;
            this.children = repository.GetChildren(item).ToList();
            this.path = repository.GetItemHierarchyPath(
                parent == null ? “” : parent.Path, item);
            this.type = repository.GetItemType(item);
        }
 
        #region IHierarchyData Members
 
        public override HierarchyData<T> GetParent()
        {
            return parent;
        }
 
        public override bool HasChildren
        {
            get { return children.Count > 0; }
        }
 
        public override HierarchicalEnumerable<T> GetChildren()
        {
            return new GenericHierarchicalEnumerable<T>(
                repository, this, children
                );
        }
 
        public override T Item { get { return item; } }
        public override string Path { get { return path; } }
        public override string Type { get { return type; } }
 
        #endregion
    }
}

У реалізації IHierarchyData фактично виконується кінцеве вилучення даних з нашої абстракції репозиторію ієрархічних даних.


Використовується два конструктора: перший призначений для створення об’єкта від деякого початкового шляху itemPath в дереві, другий створює поточний об’єкт обходу.


А тепер найцікавіше, необхідно реалізувати певний кінцевий репозиторій ієрархічних даних.


Реалізація IHierarchyDataRepository


Так як справжній шар даних зазвичай реалізується незалежно від тих усіляких UI-Біндінг, то ми будемо представляти що десь в шарі даних вже є реалізований набір операцій доступу до сховища об’єктів.


Згадуємо наші початкові моделі деревовидних даних PlainTreeModel і ORMTreeModel.


Для першої моделі, інтерфейс повинен виглядати приблизно так:


IPlaneTreeModelRepository.cs


using System.Collections.Generic;
 
namespace Home.Andir.Examples
{
    public interface IPlainTreeModelRepository<T>
    {
        IEnumerable<T> GetRoots();
        IEnumerable<T> GetItems(int parentID);
        T GetItem(int id);
    }
}

Але в цьому прикладі я не буду використовувати інтерфейси, а краще скористаюся можливостями мови C # і представлю даний інтерфейс у вигляді набору незалежних функцій вищого порядку.


Примітка: В якості основних полів об’єкта передбачається існування полів “ID” і “ParentID”, якщо їх в об’єкті не виявиться, то виникне помилка часу виконання.


PlainTreeModelRepository.cs


using System;
using System.Collections.Generic;
 
namespace Home.Andir.Examples
{
    public class PlainTreeModelRepository<T>
        : IHierarchyDataRepository<T> 
        where T : class
    {
        readonly Func<IEnumerable<T>> getRootsImpl;
        readonly Func<int, IEnumerable<T>> getItemsById;
        readonly Func<int, T> getItemByIdImpl;
 
        public PlainTreeModelRepository(
            IPlainTreeModelRepository<T> repository)
            :this(repository.GetRoots, repository.GetItems, repository.GetItem)
        { }
 
        public PlainTreeModelRepository(
            Func<IEnumerable<T>> getRootsImpl,
            Func<int, IEnumerable<T>> getItemsById,
            Func<int, T> getItemByIdImpl)
        {
            this.getRootsImpl = getRootsImpl;
            this.getItemsById = getItemsById;
            this.getItemByIdImpl = getItemByIdImpl;
        }
 
        public T GetItem(string path)
        {
            var pathItems = path.Split(“/”);
            if (pathItems.Length > 0)
            {
                int itemID = int.Parse(
                    pathItems[pathItems.Length – 1]);
                return getItemByIdImpl(itemID);
            }
 
            return null;
        }
 
        public IEnumerable<T> GetChildren(T item)
        {
            if (item == null)
                return getRootsImpl();
 
            return getItemsById(
                item.GetProperty<int>(“ID”)
                );
        }
 
        public T GetParent(T item)
        {
            if (item == null)
                throw new ArgumentNullException(“item”);
 
            var parentID = item.GetProperty<int>(“ParentID”);
 
            return getItemByIdImpl(parentID);
        }
 
        public string GetItemHierarchyPath(
            string parentHierarchyPath, T item)
        {
            if (parentHierarchyPath == null)
                throw new ArgumentNullException(“parentHierarchyPath”);
            if (item == null)
                throw new ArgumentNullException(“item”);
 
            return string.Format(“{0}/{1}”,
                parentHierarchyPath,
                item.GetProperty<int>(“ID”));
        }
 
        public string GetItemType(T item)
        {
            if (item == null)
                throw new ArgumentNullException(“item”);
 
            return typeof(T).ToString();
        }
    }
}

Для складання шляху всередині ієрархічної структури використовується нотація в вигляді / RootID / … / ParentID / ItemID / ChildID. В якості типу об’єктів використовується CLR-тип цього об’єкта.


Поля об’єктів витягуються з допомогою рефлексії (точніше TypeDescriptor “а) і наступного хелпери:


TypeDescriptorExtensions.cs


using System;
using System.ComponentModel;
 
namespace Home.Andir.Examples
{
    public static class TypeDescriptorExtensions
    {
        public static TResult GetProperty<TResult>(
            this object item,
            string propName)
        {
            var properties = TypeDescriptor.GetProperties(item);
            var descriptor = properties.Find(propName, true);
            if (descriptor != null
                && descriptor.PropertyType == typeof(TResult))
            {
                return (TResult)descriptor.GetValue(item);
            }
            else
            {
                throw new InvalidOperationException(
                    String.Format(“Property “{0}” with type “{1}” not found.”, 
                        propName, typeof(TResult)));
            }
        }
    }
}

Аналогічним чином реалізуємо модель характерну для ORM.


Інтерфейс сховища об’єктів, який нам знадобиться буде виглядати так:


using System.Collections.Generic;
 
namespace Home.Andir.Examples
{
    public interface IORMTreeModelRepository<T>
    {
        IEnumerable<T> GetRoots();
        T GetItem(int parentID);
    }
}

Але знову ж таки, використовувати даний інтерфейс ми не станемо, а обійдемося фукнція вищого поряд

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*