Иерархические шаблоны данных в Silverlight - Генерация данных, имитация службы, TreeView

ОГЛАВЛЕНИЕ

Генерация данных

Для имитации реальной базы данных был добавлен класс MockDB. Он в основном создает иерархию группа/пользователь. Был добавлен список распространенных имен и фамилий, затем были сгенерированы случайные пользователи из списка.

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

Имитация службы

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

Первый элемент конечной точки службы - класс ServiceArgs. Он инкапсулирует данные, возвращаемые из асинхронного вызова:

public class ServiceArgs<T> : EventArgs where T: class 
{
    public T Entity { get; set; }

    public ServiceArgs(T entity)
    {
        Entity = entity;
    }
}

•    Совет:  Можно расширить концепцию ServiceArgs путем добавления свойства Exception. Таким образом можно полностью инкапсулировать вызов и всегда проверять возвращенную сущность или исключение и обрабатывать его должным образом.

Класс UserService моделирует конечную точку вызова службы:

public class UserService
{
    public event EventHandler<ServiceArgs<GroupTransport>> GroupLoaded;

    public event EventHandler<ServiceArgs<List<usertransport />>> UserLoaded;

    public void GetGroup()
    {
        Debug.WriteLine("GetGroup() invoked.");
        DispatcherTimer timer = new DispatcherTimer
                    {
                        Interval = TimeSpan.FromMilliseconds(new Random().Next(1500) + 500)
                    };
        timer.Tick += _TimerTick;
        timer.Start();
    }

    void _TimerTick(object sender, EventArgs e)
    {
        ((DispatcherTimer)sender).Stop();
        if (GroupLoaded != null)
        {
            GroupLoaded(this, new ServiceArgs<grouptransport>(MockDB.GetGroupTree()));
        }
    }

    public void GetUsersForGroup(string groupName)
    {
        Debug.WriteLine(string.Format("GetUsersForGroup() invoked for {0}.",groupName));
        Thread.Sleep(new Random().Next(50) + 10);
        GroupTransport group = (from gt in MockDB.GetGroupTree().Unroll()
                                where gt.GroupName == groupName
                                select gt).SingleOrDefault();
        if (UserLoaded != null)
        {
            UserLoaded(this, new ServiceArgs<list<usertransport>>(group.Users));
        }
    }       
}

GetGroup инициирует вызов для иерархии группы, и когда он завершается, возбуждается GroupLoaded с корневым GroupTransport. Используется таймер отправки, чтобы немного задержать это, чтобы сымитировать вызов через сеть. Сообщение Debug появится в окне вывода при его запуске в режиме отладки.

GetUsersForGroup инициирует "асинхронный" вызов, чтобы получить список пользователей для конкретной группы. Также печатается сообщение, позволяющее увидеть, как отложенная загрузка работает с иерархическими шаблонами данных. Имитируется краткая задержка, далее используется функция Unroll для отыскания нужной группы, затем возбуждается событие UserLoaded с пользователями для той группы.

Хитрости

Теперь начинается веселье. Надо привязать иерархический шаблон данных, но проблема заключается в том, что используются два типа данных (пользователи и группы). Как обойти эту проблему? Просто: для дерева создается составной объект, содержащий общие данные для отображения вместе со ссылкой на оригинальный объект. Так как происходит привязка к дереву, он был назван TreeNode:

public class TreeNode
{
    public object DataContext { get; set; }

    public string Name { get; set; }

    public bool IsUser { get; set; }

    private bool _usersLoaded;

    private ObservableCollection<treenode> _children =
            new ObservableCollection<treenode>();

    public ObservableCollection<treenode> Children
    {
        get
        {
            if (!_usersLoaded)
            {
                _usersLoaded = true;
                UserService service = new UserService();
                service.UserLoaded += _ServiceUserLoaded;
                service.GetUsersForGroup(Name);
            }

            return _children;
        }

        set
        {
            _children = value;
        }
    }

    public TreeNode(GroupTransport group)
    {
        DataContext = group;
        Name = group.GroupName;
        _usersLoaded = false;
        IsUser = false;
        foreach(GroupTransport child in group.Children)
        {
            _children.Add(new TreeNode(child));
        }
    }

    public TreeNode(UserTransport user)
    {
        DataContext = user;
        Name = user.Username;
        _usersLoaded = true;
        IsUser = true;
    }

    void _ServiceUserLoaded(object sender,
         ServiceArgs<List<UserTransport>> e)
    {
        e.Entity.Sort((u1,u2)=>u1.Username.CompareTo(u2.Username));
        foreach(UserTransport user in e.Entity)
        {
            TreeNode newNode = new TreeNode(user);
            _children.Add(newNode);
        }
    }
}

Базовые свойства узла дерева включают в себя имя (преобразуемое в имя группы или в имя пользователя) и флаг, указывающий, содержит ли узел пользователя (если нет –  это группа). Главное здесь –  коллекция потомков. Обратите внимание, как используется ObservableCollection. Это особый тип коллекции, автоматически уведомляющий любой управляющий элемент, к которому он привязан, когда изменяется содержимое списка. Важно иметь возможность загрузки по требованию информации о пользователе и информировать об изменении список с древовидным отображением.

Есть два конструктора. Первый принимает UserTransport, а второй принимает GroupTransport. Надлежащие поля перемещаются в узел, оригинальный объект хранится в DataContext, и в случае групп другие узлы дерева рекурсивно добавляются к потомку.

Следите за флагом _usersLoaded. По умолчанию он устанавливается в «Ложь» в случае GroupTransport. Ключ тут – получающий метод для потомков. При вызове получающего метода проверяется этот флаг. Если он установлен в «Ложь», то вызывается служба для пользователей. Когда вызов службы возвращает управление, каждый пользователь преобразуется в TreeNode и добавляется к коллекции.

Это ключ для иерархических шаблонов данных. В шаблоне указывается, какое свойство используется для нахождения следующего уровня или потомков для шаблона. Это будет видно сразу при его подключении. Важно отметить, что к свойству не обращаются, пока оно не понадобится, исходя из схемы уровня + 1.

То есть при нахождении в корне управляющий элемент "список с древовидным отображением" потребует потомков корня (уровень + 1). Это инициирует вызов, чтобы получить пользователей для корневого узла. Однако к группам потомков еще не был осуществлен доступ, и поэтому их потомки содержат только другие группы (потомков нет). Разворачивание корневого узла откроет дочерние группы, что в свою очередь инициирует вызов к их детям и заставит те группы инициировать вызов для извлечения их пользователей. В большинстве случаев, за исключением крайне медленного соединения, те пользователи загрузятся к тому времени, когда вы соберетесь развернуть подгруппу, но если нет –  они будут медленно появляться по мере загрузки.

Такое поведение легче увидеть и понять при запуске приложения в отладке. Откройте окно вывода и следите за подсказками. Затем медленно разверните дерево. Будет видно, когда дочерние группы возбудят событие для извлечения пользователей. Несмотря на то что в примере пользователи всегда есть из-за использования фиктивной базы данных, в реальном приложении на базе службы пользователи вызываются только при необходимости и вообще не извлекаются для узлов, к которым не обращаются и которые не просматриваются.

TreeView

После понимание узла дерева можно взяться за управляющий элемент "список с древовидным отображением" XAML:

<UserControl x:Class="UserGroups.Controls.UserGroups"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
    xmlns:Data="clr-namespace:System.Windows;assembly=System.Windows.Controls"
    xmlns:Converters="clr-namespace:UserGroups.Converters"
    >
    <UserControl.Resources>
        <Converters:UserGroupConverter x:Key="TreeIcon"/>
        <Data:HierarchicalDataTemplate x:Key="UserGroupTemplate"
                      ItemsSource="{Binding Children}">
            <StackPanel Orientation="Horizontal"
                      Height="Auto" Width="Auto" Grid.Row="0">
                <Image Source="{Binding IsUser,Converter={StaticResource TreeIcon}}"/>
                <TextBlock Text="{Binding Name}"/>              
            </StackPanel>
        </Data:HierarchicalDataTemplate>
        <Style TargetType="Controls:TreeView" x:Key="UserGroupStyle">
            <Setter Property="ItemTemplate"
                       Value="{StaticResource UserGroupTemplate}"/>
            <Setter Property="BorderThickness" Value="1"/>
        </Style>
        <Style TargetType="TextBlock" x:Key="LoadingStyle">
            <Setter Property="FontSize" Value="10"/>
            <Setter Property="TextWrapping" Value="Wrap"/>
            <Setter Property="Margin" Value="3"/>
        </Style>
    </UserControl.Resources>
    <Controls:TreeView x:Name="UserGroupsTree"
           Style="{StaticResource UserGroupStyle}">
        <Controls:TreeViewItem>
            <Controls:TreeViewItem.HeaderTemplate>
                <DataTemplate>
                    <TextBlock Text="Loading..."
                      Style="{StaticResource LoadingStyle}"></TextBlock>
                </DataTemplate>
            </Controls:TreeViewItem.HeaderTemplate>
        </Controls:TreeViewItem>
    </Controls:TreeView>
</UserControl>

HierarchicalDataTemplate указывает на свойство Children для его источника элементов. В результате управляющий элемент в состоянии разобрать граф объекта и рекурсивно обойти иерархию. Сам шаблон является StackPanel с изображением для приятной иконки, показывающей тип узла, затем имя узла.
Для иконки используется преобразователь значения. Показывается изображение Image, а значит, преобразователь значения должен вернуть нечто применимое к свойству Source. Возвращается фактический BitmapImage на основе значения IsUser. Преобразователь выглядит так:

public class UserGroupConverter : IValueConverter 
{
    private static readonly BitmapImage _user =
      new BitmapImage(new Uri("../Resources/user.png", UriKind.Relative));

    private static readonly BitmapImage _group =
      new BitmapImage(new Uri("../Resources/groups.png", UriKind.Relative));

    public object Convert(object value, Type targetType,
           object parameter, CultureInfo culture)
    {
        return (bool) value ? _user : _group;
    }

    public object ConvertBack(object value, Type targetType,
           object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

Обратите внимание, как два битовых массива загружаются только один раз. Нет повторной ссылки на изображения. Поступает свойство IsUser, затем возвращается битовый массив пользователя или группы для внедрения в изображение. Также заметьте, что генерируется NotSupportedException в методе ConvertBack, а не стандартное исключение NotImplemented. Это говорит любому, использующему приложение, что это свойство не поддерживается и что это не вопрос незавершенного кода, ждущего написания.

Отделенный код для управляющего элемента простой:

public partial class UserGroups
{
    public event EventHandler<ServiceArgs<TreeNode>> SelectionChanged;

    public UserGroups()
    {
        InitializeComponent();
        UserService service = new UserService();
        service.GroupLoaded += _ServiceGroupLoaded;
        service.GetGroup();
    }      

    void _ServiceGroupLoaded(object sender, ServiceArgs<Transport.GroupTransport> e)
    {
        UserGroupsTree.Items.Clear();
        UserGroupsTree.ItemsSource = new List<TreeNode> {new TreeNode(e.Entity)};
        UserGroupsTree.SelectedItemChanged += _UserGroupsTreeSelectedItemChanged;
    }

    void _UserGroupsTreeSelectedItemChanged(object sender,
         System.Windows.RoutedPropertyChangedEventArgs<object> e)
    {
        if (SelectionChanged != null)
        {
            SelectionChanged(this, new ServiceArgs<TreeNode>((TreeNode)e.NewValue));
        }
    }
}

В конструкторе устанавливается вызов для получения корневой группы. Древовидный список уже имеет жестко закодированный элемент, показывающий дружественное сообщение "Загрузка...". Когда группа извлекается, он очищается, и группа привязывается к данным путем установки свойства ItemsSource (помните, если что-то существует, сначала надо это удалить!).

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

На главной странице находится еще один удобный элемент функционала. При выборе узла возбуждается событие, которое слушает главная страница. Так как оригинальный объект хранится в свойстве по имени DataContext, можно извлечь его и отреагировать. Если узел является пользователем, пользователь привязывается к управляющему элементу, показывающему его имя, логин и адрес электронной почты. Если узел является группой –  пользователи привязываются к сетке данных, и показывается сетка пользователей для этой группы. Это эффективно, потому что оригинальный объект совместно используется управляющими элементами, и не требуется делать дополнительные вызовы или передачи данных туда-обратно, чтобы показать их.

Следующие шаги

Разумеется, с данным приложением можно сделать намного больше, чтобы очистить пользовательский интерфейс, разделить понятия, добавить какую-либо обработку ошибок. Хорошим следующим шагом был бы перенос фиктивной базы данных в веб-приложение с последующей установкой каких-либо служб для транспортировки их в Silverlight. Далее можно использовать утилиту типа Fiddler, чтобы увидеть, когда/как осуществляется доступ к службам.

Надеемся, статья дает полезные идеи по использованию иерархических шаблонов данных в Silverlight вместе с некоторыми базовыми средствами взаимодействия между службами и управляющими элементами.