Заметки на полях

Мой дядя самых честных правил программ исходники за так ...

13.09.2009

Пример декларативного биндинга asp:TreeView

Рассмотрим пример биндинга asp:TreeView к XmlDataSource.

<%@ Page
    Language="C#"
    MasterPageFile="~/Shared/Site.Master"
    AutoEventWireup="true"
    CodeBehind="TreeView.aspx.cs"
    Inherits="Home.Andir.Examples.TreeViewPage"
    %>
<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
 
    <asp:TreeView ID="treeView" runat="server"
        DataSourceID="xmlDataSource">
        <DataBindings>
            <asp:TreeNodeBinding TextField="Name" />
        </DataBindings>
    </asp:TreeView>
 
    <asp:XmlDataSource ID="xmlDataSource" runat="server">
        <Data>
            <Root Name="Root">
                <Node Name="Node 1" />
                <Node Name="Node 2" />
                <Node Name="Node 3">
                    <Node Name="Node 3.1" />
                </Node>
            </Root>
        </Data>
    </asp:XmlDataSource>
</asp:Content>

И результат:

 Скриншот: Результат выполнения декларативного биндинга к XmlDataSource

Как видим, биндинг вполне прямолинейный и полностью декларативный, XML-данные можно хранить прямо в разметке или подгружать из внешнего файла (с помощью свойства DataFile), мало того, XML можно перед биндингом трансформировать с помощью XSLT (с помощью свойства Transform).

Пример биндинга к RSS

Необходимость писать дополнительный код появляется только если требуется получать XML от какого-то внешнего источника. Например, строить по данным из БД или получать RSS-фид из интернета.

<%@ Page
    Language="C#"
    MasterPageFile="~/Shared/Site.Master"
    AutoEventWireup="true"
    CodeBehind="TreeView.aspx.cs"
    Inherits="Home.Andir.Examples.TreeViewPage"
    %>
<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
    <asp:TreeView ID="treeView" runat="server"
        DataSourceID="feedDataSource">
        <DataBindings>
            <asp:TreeNodeBinding 
                DataMember="feed" 
                FormatString="Блог: {0}"
                TextField="title" 
                />
            <asp:TreeNodeBinding 
                DataMember="entry"
                FormatString="Запись: {0}"
                TextField="title"
                NavigateUrlField="url" />
        </DataBindings>
    </asp:TreeView>
 
    <asp:XmlDataSource ID="feedDataSource" runat="server">
        <Transform>
            <?xml version="1.0" encoding="utf-8"?>
            <xsl:stylesheet version="1.0" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:atom="http://www.w3.org/2005/Atom"
                exclude-result-prefixes="atom"
                >
                <xsl:template match="atom:feed">
                    <feed title="{atom:title}">
                        <xsl:apply-templates />
                    </feed>
                </xsl:template>
 
                <xsl:template match="atom:entry">
                    <entry 
                        title="{atom:title}" 
                        url="{atom:link[@rel='alternate']/@href}">
                        <xsl:apply-templates />
                    </entry>
                </xsl:template>
            </xsl:stylesheet>
        </Transform>
    </asp:XmlDataSource>
</asp:Content>

и немного кода:

using System;
using System.Net;
using System.Text;
 
namespace Home.Andir.Examples
{
    public partial class TreeViewPage : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            var rssFeedUrl =
                "http://feeds2.feedburner.com/AndirNotes";
 
            feedDataSource.Data = 
                DownloadRssFeed(rssFeedUrl);
        }
 
        private static string DownloadRssFeed(string url)
        {
            var client = new WebClient();
            var resultBytes = client.DownloadData(url);
 
            return Encoding.UTF8.GetString(resultBytes);
        }
    }
}

Результатом будет следующее дерево:

 Скриншот: Результат выполнения биндинга к моему RSS

В этом случае всё очень просто, однако для более изощрённых истоников иерархических данных может понадобится собственная реализация IHierarchicalDataSource. Вопросы реализации этого интерфейса рассматриваются в заметке: Реализация обобщённого варианта IHierarchicalDataSource.

Реализация обобщённого варианта 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);
    }
}

Но опять же, использовать данный интерфейс мы не станем, а обойдёмся фукнциями высшего порядка в параметрах конструктора.

Путь в иерархической структуре строится аналогично предыдущей модели.

Примечание: В качестве основных полей объекта предполагается существование полей "ID", "Parent" и "Children", если их в объекте не окажется, то возникнет ошибка времени выполнения.

ORMTreeModelRepository.cs

using System;
using System.Collections.Generic;
 
namespace Home.Andir.Examples
{
    public class ORMTreeModelRepository<T>
        : IHierarchyDataRepository<T> 
        where T : class
    {
        readonly Func<IEnumerable<T>> getRootsImpl;
        readonly Func<int, T> getItemByIdImpl;
 
        public ORMTreeModelRepository(
            IORMTreeModelRepository<T> repository)
            : this(repository.GetRoots, repository.GetItem)
        { }
 
        public ORMTreeModelRepository(
            Func<IEnumerable<T>> getRootsImpl,
            Func<int, T> getItemImpl)
        {
            this.getRootsImpl = getRootsImpl;
            this.getItemByIdImpl = getItemImpl;
        }
 
        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 item.GetProperty<IEnumerable<T>>("Children");
        }
 
        public T GetParent(T item)
        {
            if (item == null)
                throw new ArgumentNullException("item");
 
            return item.GetProperty<T>("Parent");
        }
 
        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();
        }
    }
}

Для более типизированного использования потребуется реализовать интерфейс иерархического репозитория для конкретного типа или ввести интерфейс, который должны будут поддерживать объекты из иерархического хранилища.

Пример использования

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

Для доступа к этой БД будем использовать Entity Framework, модель будет выглядеть следующим образом:

Скриншот: Departments на диаграмме модели Entity Framework

Реализуем слой данных для доступа к данным в этой таблице:

using System.Collections.Generic;
using System.Linq;
 
namespace Home.Andir.Examples.Code.DataLayer
{
    public class DepartmentRepository
    {
        public IEnumerable<Department> GetDepartments()
        {
            using (var context = new HierarchicalDbEntities())
            {
                var query = from d in context.DepartmentSet
                            where !d.ParentID.HasValue
                            select d;
 
                return query.ToList();
            }
        }
 
        public IEnumerable<Department> GetDepartments(int parentID)
        {
            using (var context = new HierarchicalDbEntities())
            {
                var query = from d in context.DepartmentSet
                            where d.ParentID.HasValue 
                                && d.ParentID.Value == parentID
                            select d;
 
                return query.ToList();
            }
        }
 
        public Department GetDepartment(int id)
        {
            using (var context = new HierarchicalDbEntities())
            {
                var query = from d in context.DepartmentSet
                            where d.ID == id
                            select d;
 
                return query.First();
            }
        }
    }
}

Сделаем страничку, на которой будем отображать организационную структуру:

<%@ Page
    Language="C#" 
    MasterPageFile="~/Shared/Site.Master"
    AutoEventWireup="true"
    CodeBehind="TreeViewWithRepository.aspx.cs"
    Inherits="Home.Andir.Examples.TreeViewWithRepositoryPage" %>
<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
    <asp:TreeView ID="treeView" runat="server">
        <DataBindings>
            <asp:TreeNodeBinding ValueField="ID" TextField="Name" />
        </DataBindings>
    </asp:TreeView>
</asp:Content>

и теперь в CodeBehind реализуем биндинг данных к DepartmentRepository:

using System;
 
using Home.Andir.Examples.Code.DataLayer;
 
namespace Home.Andir.Examples
{
    public partial class TreeViewWithRepositoryPage 
        : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            var repository = new DepartmentRepository();
 
            treeView.DataSource =
                new GenericHierarchicalDataSource<Department>(
                    new PlainTreeModelRepository<Department>(
                        () => repository.GetDepartments(),
                        item => repository.GetDepartments(item.ID),
                        id => repository.GetDepartment(id)));
            treeView.DataBind();
        }
    }
}

Для проверки запускаем:

Скриншот: Результат выполнения TreeViewWithRepository

Вот, наконец-то, всё заработало :-)

Полный проект с реализацией IHierarchicalDataSource можно взять здесь.

26.08.2009

Пролог: Изучение с нуля

Добрался таки до пролога. Чтобы не потерять, все источники собираю в одном месте.

Реализации:

Редакторы кода:

Источники первичной информации:

       Большая статья из которой можно почерпнуть практически всю доступную информацию о языке Prolog.

        Очень хорошее начало, но, к сожалению, содержит пропуски в некоторых частях, для полноценного изучения не годится.

  • Prolog ISO/IEC 13211-1:1995 - стандарт языка,

       В 1995 году был принят стандарт языка, но к сожалению в свободном доступе его обнаружить не удалось, есть только черновик в PostScript формате.

        Набор документов и программ использованных при разработке стандарта языка.

Туториалы:

Книги:

Задачи:

Другое:

Очень краткое введение

Пролог – это декларативный логический язык программирования. Основными его конструкциями являются логические предложения (например: human('Socrat') :- true.), на основании которых пролог строит некоторые логические выводы. Предложения объединяются в правила с помощью операции and, которая обозначается знаком ','. Предложение при проверке может создавать побочные эффекты, что используется для ввода/вывода.

Краткий путеводитель по синтаксису

% Это строчный комментарий
/* А это блочный комментарий */

goal(X, Y) :-   % goal - это цель, для которой мы собираем правила,
                % имя цели всегда начинается с маленькой буквы.
                % результат у цели - это всегда true или false.
                % X, Y - параметры цели.

    X > Y       % Первое утверждение, которое утверждает что X больше Y.

    ,           % Разделитель между утверждениями, означает логическое И.

    X + 1 == Y  % Второе утверждение, которое может быть верным,
                % только если Y = X + 1.

    .           % Точка означает конец описания цели.

/*
 * Суммирую:
 * Имя цели всегда с маленькой буквы,
 * Параметры цели, и все другие переменные с большой буквы,
 * Утверждения разделяются запятой,
 * Описание цели всегда заканчивается точкой.
 */

prolog('logic language').
% Так записываются некоторые факты,
% и по сути являются сокращениями для цели,
% которая всегда возвращает true.
% в данном случае эквивалент выглядел бы так:
% prolog('logic language') :- true.

% Цели могут быть рекурсивными.
% Посчитаем сумму от 0 до X.
sum(0, 0).   % факт используется для остановки рекурсии.
sum(X, Result) :-
    Y is X - 1,             % is - это по сути присвоение значения.
    sum(Y, SubResult),      % Рекурсивный вызов.
    Result is X + SubResult.


% Цель, которая будет использоваться при запуске программы,
% имя 'main' выбрано мною произвольно.
main :-
    % запрашиваем значение факта
    prolog(Which),
    % выводим значение на экран
    format('prolog is a ~a.', [Which]),

    % newline
    nl,

    % задаём значение X
    X is 100,
    % считаем сумму от 0 до 100
    sum(X, Result),
    % выводим результат с помощью printf-like функции
    format('sum(0, ~a) = ~a.', [X, Result]),

    % newline.
    nl.

/*
 * Вывод программы при запуске:
 * prolog is a logic language.
 * sum(0, 100) = 5050.
 */

Типы данных

Атом: атом или 'атом', начинается с маленькой буквы или обрамляется одинарными кавычками.

Строка: "Строка", обрамляется двойными кавычками, по факту представляет список из чисел – кодов ASCII символов строки.

Число: 10, целые (-1,0,1), плавающая запятая (0.123, 123.456).

Список: [0, 1, 2], набор элементов перечисленных через запятую и обрамлённых квадратными скобками. Элементы могут быть разнородными (разных типов). Списки можно деструктурировать через синтаксис [Head | Tail].

 

Первые результаты

Теперь, с помощью новоприобретённых знаний, можно написать несколько известных примеров:

HelloWorld.pro:

main :- writeln('Hello, Prolog World!'). 

Factorial.pro:

% factorial implementation
factorial(1, 1).
factorial(X, Result) :-
    X > 0,
    X1 is X - 1,
    factorial(X1, Result1),
    Result is X * Result1.
 
main :-
    factorial(7, X),
    writeln(X). 

Fibonachi.pro:

% Fibbonachi sequence implementation
fib(0, 1).
fib(1, 1).
fib(X, Result) :-
    X1 is X - 1,
    X2 is X - 2,
    fib(X1, Result1),
    fib(X2, Result2),
    Result is Result1 + Result2.
 
main :-
    fib(7, X),
    writeln(X). 

QuickSort.pro:

qsort([], []).
qsort([X], [X]).
qsort([Head | Tail], Result) :-
    partition(Head, Tail, Left, Right),
    qsort(Left, LeftSorted),
    qsort(Right, RightSorted),
    append(LeftSorted, [Head | RightSorted], Result).
 
partition(_, [], [], []).
partition(Pivot, [Head | Tail], [Head | Left], Right) :-
    Head =< Pivot,
    partition(Pivot, Tail, Left, Right).
partition(Pivot, [Head | Tail], Left, [Head | Right]) :-
    Head > Pivot,
    partition(Pivot, Tail, Left, Right).
 
append([], Right, Right).
append([Head | Left], Right, [Head | Result]) :-
    append(Left, Right, Result).
 
main :-
    qsort([1, 5, 4, 6, 3, 2], X),
    writeln(X). 

 

Какие ещё есть логические языки программирования?

Путём нехитрых поисков в источниках близким к прологу собрал такой список:

        Расширение языка Haskell для реализации логического программирования.

       Mercury is a new logic/functional programming language, which combines the clarity and expressiveness of declarative programming with advanced static analysis and error detection features.

        Мультипарадигменный язык, который включает в себя подмножество логического программирования.

        Язык, комбинирующий функциональное и логическое программирование. По описанию сильно похоже, что к базе логического программирования добавлены функциональные возможности (функции, операторы, типы, структуры и т.п.).

14.08.2009

Расширение отладчика SOS для Visual Studio 2008

Много раз во время отладки приложений, мне хотелось получать гораздо больше информации от отладчика, о том как расположены мои объекты в памяти, сколько занимают места, как давно созданы и сколько их существует. Обычно, такое нужно, когда хочется составить более полное представление о том, как работает программа.

Так вот, оказывается существует специальное расширение для отладчиков Visual Studio и WinDbg, которое поставляется вместе с .Net Framework. Это расширение и называется SOS и находится в %SystemRoot%\Microsoft.Net\v2.0.50727\sos.dll для CLR 2.0.

Примечание: SOS расшифровывается как Son Of Strike.

Продемонстрирую работу с этим расширением и заодно покажу в действии набор команд, которые в нём существуют.

Итак, первое. Реализуем небольшой пример:

using System;
 
namespace Home.Andir.Examples
{
    class A
    {
        public uint Value { get; set; }
        public override string ToString()
        {
            return string.Format("A {{ Value = 0x{0:x} }}", Value);
        }
    }
 
    class Program
    {
        static void Main(string[] args)
        {
            TestSOSMethod(
                "Hello debugging world!");
        }
 
        private static void TestSOSMethod(string ignoreThis)
        {
            var a = new A() { Value = 0xDEADBEEF };
            var b = new A() { Value = 0xBEEFDEAD };
 
            Console.WriteLine("a = {0}, b = {1}", a, b);
 
            Console.ReadKey(); // breakpoint is here
        }
    }
}

В примере, вызывается метод с одним параметром и создаются два объекта типа Home.Andir.Examples.A с магическими числами, которые потом будет хорошо видно в отладчике.

Теперь, чтобы можно было воспользоваться возможностями SOS, нужно включить в свойствах проекта неуправляемую отладку.

Скриншот: VS2008 включение отладки в неуправляемом коде

Теперь, устанавливаем точку остановки (Breakpoint) в статическом методе TestSOSMethod на строке Console.ReadKey(). И запускаем в режиме отладки (F5).

В консоли будет выведено:

a = A { Value = 0xdeadbeef }, b = A { Value = 0xbeefdead }

Впрочем, как и ожидалось. Теперь загрузим sos.dll, для этого нужно открыть окно Immediate (Debug –> Windows –> Immediate) и написать первой командой .load sos.dll:

Скриншот: окно Immediate и загрузка sos.dll 

В окне выведено:

.load sos.dll
extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded

Отлично! SOS успешно загрузился, и теперь можно поэкспериментировать с его командами.

Начнём, как обычно с help.

!help
-------------------------------------------------------------------------------
SOS is a debugger extension DLL designed to aid in the debugging of managed
programs. Functions are listed by category, then roughly in order of
importance. Shortcut names for popular functions are listed in parenthesis.
Type "!help <functionname>" for detailed info on that function. 

Object Inspection                  Examining code and stacks
-----------------------------      -----------------------------
DumpObj (do)                       Threads
DumpArray (da)                     CLRStack
DumpStackObjects (dso)             IP2MD
DumpHeap                           U
DumpVC                             DumpStack
GCRoot                             EEStack
ObjSize                            GCInfo
FinalizeQueue                      EHInfo
PrintException (pe)                COMState
TraverseHeap                       BPMD 

Examining CLR data structures      Diagnostic Utilities
-----------------------------      -----------------------------
DumpDomain                         VerifyHeap
EEHeap                             DumpLog
Name2EE                            FindAppDomain
SyncBlk                            SaveModule
DumpMT                             GCHandles
DumpClass                          GCHandleLeaks
DumpMD                             VMMap
Token2EE                           VMStat
EEVersion                          ProcInfo 
DumpModule                         StopOnException (soe)
ThreadPool                         MinidumpMode 
DumpAssembly                       
DumpMethodSig                      Other
DumpRuntimeTypes                   -----------------------------
DumpSig                            FAQ
RCWCleanupList
DumpIL

Как видим, функций довольно много, и названия у них вполне себе говорящие. Подробнее о каждой функции можно узнать, если набрать !help <имя команды>.

Примечание: В дальнейшем, вывод команды !help <имя команды> будет показываться в урезанном виде, чтобы исключить неважные в данном случае детали. Для подробного описания команды используйте MSDN или самостоятельно вызывайте эту команду.

Продолжим с нашим примером. Изучим команду clrstack, которая как очевидно из названия может инспектировать содержимое стека текущего потока.

!help clrstack
-------------------------------------------------------------------------------
!CLRStack [-a] [-l] [-p]

CLRStack attempts to provide a true stack trace for managed code only. It is
handy for clean, simple traces when debugging straightforward managed 
programs. The -p parameter will show arguments to the managed function. The 
-l parameter can be used to show information on local variables in a frame.
SOS can't retrieve local names at this time, so the output for locals is in
the format <local address> = <value>. The -a (all) parameter is a short-cut
for -l and -p combined.

Сейчас мы находимся внутри метода TestSOSMethod и можно посмотреть, что находится в данный момент в стеке текущего потока.

!clrstack -a
OS Thread Id: 0x1984 (6532)
ESP       EIP     
0012ec2c 013b0178 Home.Andir.Examples.Program.TestSOSMethod(System.String)
    PARAMETERS:
        ignoreThis = 0x01a09290
    LOCALS:
        0x0012ec4c = 0x01a09304
        0x0012ec48 = 0x01a09310
        0x0012ec44 = 0x01a09304
        0x0012ec40 = 0x01a09310

0012ec94 013b00a0 Home.Andir.Examples.Program.Main(System.String[])
    PARAMETERS:
        args = 0x01a09280

0012eef4 67971b4c [GCFrame: 0012eef4] 

Видим, что в стеке находится два метода и у текущего метода, есть один параметр с именем ignoreThis и четыре записи в локальных переменных с неизвестными именами (при этом объекта всего два, что видно по повторяющимся адресам). Теперь посмотрим значения этих параметров и переменных. Для начала, рассмотрим параметр ignoreThis.

Чтобы посмотреть объекты в памяти существует команда dumpobj (сокращение do) которая принимает в параметре адрес объекта.

!help dumpobj
-------------------------------------------------------------------------------
!DumpObj [-nofields] <object address>

This command allows you to examine the fields of an object, as well as learn 
important properties of the object such as the EEClass, the MethodTable, and 
the size.

The arguments in detail:
-nofields:     do not print fields of the object, useful for objects like 
                  String

Итак, вызываем dumpobj для объект ignoreThis (используем параметр –nofields чтобы не выводить ненужные нам поля объекта string):

!dumpobj -nofields 0x01a09290
Name: System.String
MethodTable: 670d88a4
EEClass: 66e9a498
Size: 62(0x3e) bytes
 (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: Hello debugging world!

Итак, в параметре находится та самая строка, ради которой этот параметр и затевался :-) Занимает она 62 байта, что с учётом длины строки в 22 символа, и по 2 байта на символ в кодировке Unicode – получаем 18 байт оверхеда на хранение такой строки в типе System.String.

Продолжаем исследовать объекты в стеке, теперь посмотрим на локальные переменные (сразу все по очереди):

!do 0x01a09304
Name: Home.Andir.Examples.A
MethodTable: 0024338c
EEClass: 00241830
Size: 12(0xc) bytes
 (D:\development\projects\andir-notes\examples\SOSDebugExtension\SOSDebugExtension\bin\Debug\SOSDebugExtension.exe)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
670b9cc8  4000001        4        System.UInt32  1 instance 3735928559 <Value>k__BackingField

!do 0x01a09310
Name: Home.Andir.Examples.A
MethodTable: 0024338c
EEClass: 00241830
Size: 12(0xc) bytes
 (D:\development\projects\andir-notes\examples\SOSDebugExtension\SOSDebugExtension\bin\Debug\SOSDebugExtension.exe)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
670b9cc8  4000001        4        System.UInt32  1 instance 3203391149 <Value>k__BackingField

Видим два объекта в памяти типа Home.Andir.Examples.A, которые занимают по 12 байт и содержат ровно одно поле со странным именем <Value>k__BackingField и типом System.UInt32, как очевидно, которое является автоматически сгененированным полем для автосвойства Value.

Каждый объект занимает 12 байт, а так как внутри находится только 1 поле длиной 4 байта, то получаем что оверхед равняется 8 байтам.

Ещё одна полезная команда, которая позволит убедиться, что у нас в памяти находится ровно два объекта. Это dumpheap, у которой есть параметр –type для фильтрации вывода по имени типа (всё же в хипе находится довольно много служебной информации).

!dumpheap -type Home.Andir.Examples.A
 Address       MT     Size
01a09304 0024338c       12     
01a09310 0024338c       12     
total 2 objects
Statistics:
      MT    Count    TotalSize Class Name
0024338c        2           24 Home.Andir.Examples.A
Total 2 objects

О том, сколько всякого мусора находится в GC Heap можно узнать с помощью этой же команды и параметра –stat.

!dumpheap -stat
total 7737 objects
Statistics:
      MT    Count    TotalSize Class Name
670e18f0        1           12 System.Collections.Generic.GenericEqualityComparer`1[[System.String, mscorlib]]
670e0ba0        1           12 System.Security.Permissions.ReflectionPermission
670df8a8        1           12 System.Resources.FastResourceComparer
670dcd44        1           12 System.__Filters
....

Теперь понятно, зачем нужна фильтрация по имени типа. На самом деле, команда dumpheap – очень мощная команда и позволяет исследовать многие параметры GC Heap (см. !help dumpheap, где есть в том числе и примеры её полноценного использования).

Далее

Для подробного изучения порекомендую посетить следующие ресурсы:

Также, рекомендую прочитать Success Story об удачном использовании SoS для обнаружения проблем в приложении ASP.Net: Dan McKinley: App Server Autopsy.

12.08.2009

Visual Studio 2010 – Historical Debugger

Случайно посмотрел PodCast про новую фенечку в Visual Studio 2010. Это так называемый Historical Debugger ("Исторический отладчик").

На мой первый взгляд – весьма примечательная и полезная вещь.

MSDN 9 Channel: An Introduction to the Historical Debugger

Get Microsoft Silverlight

Замечание: Ну и быстро говорит этот товарищ!

По сути, что там происходит: система записывает (Trace) состояние приложения в контрольных точках (события, исключения и т.п.) и затем позволяет разработчику во время отладки воспроизвести выполнение приложения по шагам. Это, предположительно, позволяет упростить отладку в особо сложных случаях (например, когда ошибка возникает только у пользователей в их специфичных условиях работы). Словом, это такой способ воспроизведения ошибок, когда нет необходимости воспроизведения условий их возникновения.

А для продвинутых пользователей (тестеров и самих разработчиков) появится возможность приложить "Лог исполнения" (Execution Log) к описанию ошибки в багтрекере. Затем разработчик ответственный за исправление ошибки сможет использовать этот лог для быстрой диагностики ошибки и её исправления.

Настройки в VS:

Скриншот: VS2010 Options - Historical Debugging

Новая вкладка:

Скриншот: Вкладка "Debug History" в действии 

Подробнее о новом отладчике можно прочитать: