Шаблон данных DataTemplate в WinRT
135WinRT --- Шаблон данных DataTemplate
Я продемонстрировал применение 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, поэтому они отображаются в виде обычного текста:
Это конкретное применение 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>
Когда вы видите список имен типов в списковом элементе управления, не огорчайтесь! На самом деле нужно радоваться, что привязка работает - это означает, что вы сможете отобразить эти данные более качественно. Нужно лишь задать свойству 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:
Свойство 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 = " />
<TextBlock Text="{Binding A}" />
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="1"
Orientation="Horizontal">
<TextBlock Text="R = " />
<TextBlock Text="{Binding R}" />
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="2"
Orientation="Horizontal">
<TextBlock Text="G = " />
<TextBlock Text="{Binding G}" />
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="3"
Orientation="Horizontal">
<TextBlock Text="B = " />
<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);
}
}
}
}
Теперь список цветов выводится с десятичными значениями составляющих каждого цвета:
К сожалению, этот способ нельзя использовать для вывода имен цветов, потому что они не являются частью структуры 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.