Шаблон данных DataTemplate в WinRT

135

Я продемонстрировал применение DataTemplate на примере специального класса производного от ContentControl, но такие классы встречаются не так уж часто, и откровенно говоря, DataTemplate с ними применяется еще реже. На практике DataTemplate обычно применяется с элементами управления, производными от ItemsControl. Эти элементы предназначены для хранения коллекции объектов, обычно относящихся к одному типу:

Object
    DependencyObject
        UIElement
            FrameworkElement
                Control
                    ItemsControl
                        Selector (без создания экземпляров)
                            ComboBox
                            ListBox
                            FlipView
                            ListViewBase (без создания экземпляров)
                                ListView
                                GridView

Конечно, самым известным в этом списке является элемент управления ListBox, существовавший в Windows (в той или иной форме) с первых версий. В исходном варианте ListBox выводился вертикальный список вариантов, которые пользователь прокручивал и выбирал при помощи клавиатуры или мыши. (Современные версии ListBox обладают большей гибкостью и поддерживают сенсорный ввод.) Элемент управления ComboBox появился в Windows позднее; как подсказывает название, он сочетает поле для редактирования текста с раскрывающимся списком. Элемент управления FlipView относится к нововведениям Windows 8.

При работе с разными элементами управления легко забыть о классе ItemsControl, производными от которого являются все остальные классы. ItemsControl просто выводит набор значений; концепция выбора в нем не поддерживается. Класс Selector добавляет логику выделения, а все остальные классы определяются как производные от него. Я обычно называю эту группу элементов управления «списковыми элементами управления», так как все они предназначены для вывода списков вариантов.

Существуют четыре способа заполнения коллекционных элементов управления объектами: по отдельности в XAML, по отдельности в коде, группой в коде или группой в XAML (обычно с использованием привязки данных). Объекты, заносимые в списковые элементы управления, обычно не являются производными от UIElement. Очень часто они представляют собой бизнес-объекты или модели представления.

Если количество вариантов невелико, их можно перечислить прямо в файле XAML:

<Grid Background="#FF1D1D1D">
        <ItemsControl FontSize="32">
            <x:String>Один апельсин</x:String>
            <x:String>Два апельсина</x:String>
            <x:String>Три апельсина</x:String>
            <x:String>Четыре</x:String>
            <x:String>Пять</x:String>
        </ItemsControl>
</Grid>

Реальное применение DataTemplate

Свойством содержимого ItemsControl является свойство Items, в котором хранится объект типа ItemCollection - класса, реализующего интерфейсы IList, IEnumerable и IObservableVector. (Вскоре мы поговорим об этих интерфейсах более подробно.)

В этом примере варианты, добавляемые в ItemsControl, представляют собой объекты типа String, поэтому они отображаются в виде обычного текста:

Передача коллекции объектов по отдельности в XAML

Это конкретное применение ItemsControl ненамного лучше StackPanel (не считая того, что список можно заполнить вариантами типа String вместо TextBlock). Конечно, «за кулисами» для каждого варианта в списке генерируется элемент TextBlock.

В отличие от элементов управления, производных от ItemsControl, сам класс ItemsControl не имеет встроенной поддержки прокрутки. Если у вас имеется список вариантов, которые, возможно, придется прокручивать, элемент управления ItemsControl стоит поместить в ScrollViewer, как в следующем примере:

<Grid Background="#FF1D1D1D">
        <ScrollViewer>
            <ItemsControl FontSize="32">
                ...
                <Color>LimeGreen</Color>
                <Color>Lime</Color>
                <Color>Yellow</Color>
                <Color>Orange</Color>
                ...
            </ItemsControl>
        </ScrollViewer>
</Grid>
Текстовое представление объектов коллекции Color

Когда вы видите список имен типов в списковом элементе управления, не огорчайтесь! На самом деле нужно радоваться, что привязка работает - это означает, что вы сможете отобразить эти данные более качественно. Нужно лишь задать свойству ItemTemplate объекта ItemsControl объект DataTemplate с привязками для визуализации вариантов из списка:

<Grid Background="#FF1D1D1D">
        <ScrollViewer>
            <ItemsControl FontSize="32">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Rectangle Width="120" Height="60" Margin="0,10">
                            <Rectangle.Fill>
                                <SolidColorBrush Color="{Binding}" />
                            </Rectangle.Fill>
                        </Rectangle>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
                
                <Color>LimeGreen</Color>
                <Color>Lime</Color>
                <Color>Yellow</Color>
                <Color>Orange</Color>
                <Color>AliceBlue</Color>
                <Color>AntiqueWhite</Color>
                <Color>Aqua</Color>
                <Color>Blue</Color>
                <Color>WhiteSmoke</Color>
                <Color>YellowGreen</Color>
                <Color>Pink</Color>
                <Color>Coral</Color>
                <Color>Violet</Color>
                ...
            </ItemsControl>
        </ScrollViewer>
</Grid>

Список прокручивается, но не отличается содержательностью, потому что для каждого варианта Color отображается его представление в формате ToString:

Визуализация через шаблон объектов коллекции Color

Свойство ItemTemplate класса ItemsControl аналогично свойству ContentTemplate класса ContentControl. Оба свойства относятся к типу DataTemplate. Однако в случае свойства ItemTemplate шаблон используется для генерирования визуального дерева для каждого варианта.

В процессе конструирования ItemsControl по шаблону DataTemplate генерируются 141 элемент Rectangle и 141 элемент SolidColorBrush, по одному для каждого элемента управления. Конечно, задавать весь список из 141 объекта Color в файле XAML не хочется. Вероятно, вы предпочтете сгенерировать их в коде. В следующем проекте файл XAML не содержит определений вариантов, но содержит более сложный шаблон, который также выводит компоненты цвета:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <ScrollViewer>
            <ItemsControl Name="items" FontSize="32">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid Width="300" Margin="0,10">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="160" />
                                <ColumnDefinition Width="Auto" />
                            </Grid.ColumnDefinitions>

                            <Grid.RowDefinitions>
                                <RowDefinition />
                                <RowDefinition />
                                <RowDefinition />
                                <RowDefinition />
                            </Grid.RowDefinitions>

                            <Rectangle Grid.RowSpan="4" Margin="10,0">
                                <Rectangle.Fill>
                                    <SolidColorBrush Color="{Binding}" />
                                </Rectangle.Fill>
                            </Rectangle>

                            <StackPanel Grid.Column="1" Orientation="Horizontal">
                                <TextBlock Text="A =&#x00A0;" />
                                <TextBlock Text="{Binding A}" />
                            </StackPanel>

                            <StackPanel Grid.Column="1" Grid.Row="1"
                                        Orientation="Horizontal">
                                <TextBlock Text="R =&#x00A0;" />
                                <TextBlock Text="{Binding R}" />
                            </StackPanel>

                            <StackPanel Grid.Column="1" Grid.Row="2"
                                        Orientation="Horizontal">
                                <TextBlock Text="G =&#x00A0;" />
                                <TextBlock Text="{Binding G}" />
                            </StackPanel>

                            <StackPanel Grid.Column="1" Grid.Row="3"
                                        Orientation="Horizontal">
                                <TextBlock Text="B =&#x00A0;" />
                                <TextBlock Text="{Binding B}" />
                            </StackPanel>
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>
    </Grid>
</Page>

Сами варианты генерируются в программном коде. Как вы, вероятно, уже поняли, файл фонового кода использует отражение для получения всех свойств Color, определенных статическим классом Colors. Каждое значение Color добавляется в ItemsControl с использованием метода Add(), определенного ItemCollection. Это пример второго способа включения вариантов в списковый элемент управления:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Reflection;
using Windows.UI;
using Windows.UI.Xaml.Controls;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            List<PropertyInfo> properties = typeof(Colors).GetTypeInfo()
                        .DeclaredProperties.ToList();

            foreach (PropertyInfo property in properties)
            {
                Color color = (Color)property.GetValue(null);
                items.Items.Add(color);
            }
        }
    }
}

Теперь список цветов выводится с десятичными значениями составляющих каждого цвета:

Шаблон с панелью RGB

К сожалению, этот способ нельзя использовать для вывода имен цветов, потому что они не являются частью структуры Color. Чтобы вывести имя вместе с цветом, необходимо заполнить ItemsControl экземплярами класса, предоставляющего это имя.

Давайте создадим такой класс. Я определил класс с именем NamedColor:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Reflection;
using Windows.UI;

namespace WinRTTestApp
{
    public class NamedColor
    {
        static NamedColor()
        {
            List<NamedColor> list = new List<NamedColor>();
            List<PropertyInfo> properties = typeof(Colors).GetTypeInfo().DeclaredProperties.ToList();

            foreach (PropertyInfo property in properties)
            {
                NamedColor namedColor = new NamedColor
                {
                    Name = property.Name,
                    Color = (Color)property.GetValue(null)
                };

                list.Add(namedColor);
            }

            All = list;
        }

        public static IEnumerable<NamedColor> All { private set; get; }

        public Color Color { private set; get; }

        public string Name { private set; get; }
    }
}

Класс NamedColor содержит два открытых свойства: свойство Name типа string и свойство Color типа Color. Также он определяет статическое свойство с именем All типа IEnumerable<NamedColor>. Свойство задается из статического конструктора и содержит коллекцию всех объектов NamedColor, полученных посредством отражения из статического класса Colors.

Я не определил этот класс как реализующий INotifyPropertyChanged, потому что свойства объектов NamedColor не изменяются после инициализации объекта.

Для вывода шестнадцатеричных значений цветов в библиотеку также включается класс ByteToHexStringConverter:

using System;
using System.Collections.Generic;
using System.Linq;
using Windows.UI.Xaml.Data;

namespace WinRTTestApp
{
    public class ByteToHexStringConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object par, string lang)
        {
            return ((byte)value).ToString("X2");
        }
        public object ConvertBack(object value, Type targetType, object par, string lang)
        {
            return value;
        }
    }
}

Я уже показал, как заполнить объект ItemCollection, доступный из свойства Items объекта ItemsControl, из разметки XAML или кода. Альтернативное решение основано на использовании свойства ItemsSource. Это свойство определяется с типом object, но вы, несомненно, зададите ItemsSource объект, реализующий интерфейс IEnumerable. Объект, заданный свойству ItemsSource, превращается в коллекцию для ItemsControl, а свойство Items при этом становится доступным только для чтения.

Свойство ItemsSource может задаваться из программного кода или XAML. Для начала рассмотрим решение с кодом. Основную часть следующего файла XAML занимает шаблон DataTemplate, определяющий визуальное дерево для каждого варианта NamedColor в коллекции:

<Page ...>

    <Grid Background="#FF1D1D1D">
        
        <Grid.Resources>
            <local:ByteToHexStringConverter x:Key="hex" />
        </Grid.Resources>

        <ScrollViewer>
            <ItemsControl Name="items">

                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Border BorderBrush="#DEFFFFFF"
                                BorderThickness="1" Width="400"
                                Margin="5">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto" />
                                    <ColumnDefinition Width="*"/>
                                </Grid.ColumnDefinitions>

                                <Rectangle Height="96" Width="96" Margin="5,5,10,5">
                                    <Rectangle.Fill>
                                        <SolidColorBrush Color="{Binding Color}" />
                                    </Rectangle.Fill>
                                </Rectangle>

                                <StackPanel Grid.Column="1" VerticalAlignment="Center">
                                    <TextBlock FontSize="32" Text="{Binding Name}" />

                                    <ContentControl FontSize="20">
                                        <StackPanel Orientation="Horizontal">
                                            <TextBlock Text="{Binding Color.A,
                                                                      Converter={StaticResource hex}}" />
                                            <TextBlock Text="-" />
                                            <TextBlock Text="{Binding Color.R,
                                                                      Converter={StaticResource hex}}" />
                                            <TextBlock Text="-" />
                                            <TextBlock Text="{Binding Color.G,
                                                                      Converter={StaticResource hex}}" />
                                            <TextBlock Text="-" />
                                            <TextBlock Text="{Binding Color.B,
                                                                      Converter={StaticResource hex}}" />
                                        </StackPanel>
                                    </ContentControl>
                                </StackPanel>
                            </Grid>
                        </Border>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>
    </Grid>
</Page>

Обратите внимание на семь элементов TextBlock для вывода цветовых составляющих. Они размещаются в горизонтальной панели StackPanel, которая, в свою очередь, находится в элементе ContentControl, который нужен по единственной причине: он предоставляет свойство FontSize, наследуемое семью элементами TextBlock. Также сработало бы неявное определение стиля.

Привязки элементов TextBlock и SolidColorBrush явно подразумевают, что отображается объект типа NamedColor, но в файле XAML экземпляры NamedColor не создаются. Вместо этого свойство ItemsSource объекта ItemsControl задается в конструкторе файла фонового кода:

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            items.ItemsSource = NamedColor.All;
        }
    }
}

Когда задано свойство ItemsSource, элемент ItemsControl генерирует визуальные деревья для всех объектов в коллекции.

Расширение текстового шаблоны для коллекции цветов

Также возможно реализовать решение полностью в разметке XAML, определив привязку свойства ItemsSource к коллекции. Следующий проект очень похож на предыдущий: он тоже использует те же классы и определяет тот же шаблон DataTemplate в файле XAML. Но объект NamedColor в этом случае создается как ресурс, а привязка к свойству All определяется в свойстве ItemsSource объекта ItemsControl:

<Page ...>

    <Grid Background="#FF1D1D1D">
        
        <Grid.Resources>
            <local:ByteToHexStringConverter x:Key="hex" />
            <local:NamedColor x:Key="namedcolors" />
        </Grid.Resources>

        <ScrollViewer>
            <ItemsControl Name="items" ItemsSource="{Binding Source={StaticResource namedcolors},
                                                             Path=All}">

                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        ...
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>
    </Grid>
</Page>

Если бы ресурс сам по себе был объектом коллекции, то ItemsSource можно было бы задать этот ресурс в расширении разметки StaticResource, но поскольку коллекция доступна только из свойства All объекта NamedColor, для ссылки на объект NamedColor и свойство All потребуется расширение разметки Binding.

Напомню, что ранее рассматривалась пара программ для вывода списков цветов в разных форматах, и я сказал, что самый правильный способ решения этой задачи будет представлен позже, с использованием шаблонов. Он перед вами: класс определяет тип отображаемых вариантов, а шаблон DataTemplate объекта ItemsControl определяет способ их визуализации. Такое объединение коллекций с привязками и шаблонами является одним из важнейших аспектов программирования для Windows Runtime.

Пройди тесты
Лучший чат для C# программистов