Шаблоны данных
90WPF --- Привязка, команды и стили WPF --- Шаблоны данных
»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ
Стили предоставляют некоторые базовые возможности форматирования, но они не преодолевают наиболее существенные ограничения списков, которые демонстрировались до сих пор: независимо от того, как изменяется ListBoxItem, это всего лишь ListBoxItem, а не более сложная комбинация элементов. И поскольку каждый ListBoxItem поддерживает только одно привязанное поле, нет способа создать развитый список, включающий в себя множество полей или графических изображений.
Однако в WPF есть другой инструмент, который может преодолеть эти тесные рамки, позволив использовать комбинацию свойств привязанного объекта и скомпоновать его специфическим способом либо отобразить визуальное представление, более сложное, чем простая строка. Этим инструментом является шаблон данных.
Шаблон данных (data template) — это фрагмент XAML-разметки, который определяет, как привязанный объект данных должен быть отображен. Шаблоны данных поддерживают два типа элементов управления:
Элементы управления содержимым поддерживают шаблоны данных через свойство ContentTemplate. Шаблон содержимого используется для отображения того, что помещается в свойство Content.
Списочные элементы управления (элементы, унаследованные от ItemsControl) поддерживают шаблоны данных через свойство ItemTemplate. Этот шаблон используется для отображения каждого элемента списка из коллекции (или каждой строки из DataTable), которая применяется в качестве ItemsSource.
Средство основанного на списке шаблона на самом деле базируется на шаблонах элементов управления с содержимым. Причина в том, что каждый элемент в списке помещен в оболочку элемента управления содержимым, такого как ListBoxItem для ListBox, ComboBoxItem для ComboBox и т.д. Какой бы шаблон не был указан для свойства ItemTemplate списка, он используется как ContentTemplate каждого элемента в списке.
Так что же можно поместить внутрь шаблона данных? На самом деле все довольно просто. Шаблон данных — это простой блок XAML-разметки. Подобно любому другому блоку XAML-разметки, шаблон может включать любую комбинацию элементов. Он также должен включать одно или более выражений привязки, которые извлекают информацию для отображения. (В конце концов, если не предусмотреть никаких выражений привязки данных, то каждый элемент в списке будет выглядеть одинаково, от чего мало толку.)
Ниже приведен пример, в котором каждый элемент списка заключается в прямоугольник со скругленными углами, отображаются две порции информации, и используется форматирование полужирным начертанием для выделения марки машины:
<ListBox Name="lstCars" Margin="5">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue" CornerRadius="4" Width="370">
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock FontWeight="Bold"
Text="{Binding Path=ModelName, Converter={StaticResource StringUpperConverter}}"/>
<TextBlock Grid.Row="1" Text="{Binding ModelNumber}"/>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
public class StringUpperConverter : IValueConverter
{
string text;
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
text = (string)value;
return text.ToUpper();
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
return text;
}
}
Когда этот список привязывается, для каждого автомобиля создается отдельный объект Border. Внутри элемента Border находится Grid с двумя порциями информации, как показано на рисунке:
Отделение и повторное использование шаблонов
Подобно стилям, шаблоны часто объявляются как ресурс окна или приложения, который определен в списке, где он используется. Такое отделение часто более ясно, особенно если применяются длинные сложные шаблоны или множество шаблонов к одному и тому же элементу управления. Это также дает возможность повторно использовать шаблоны в более чем одном списочном элементе управления или элементе с содержимым, если нужно представлять данные одинаковым образом в разных местах пользовательского интерфейса.
Все, что потребуется — это определить шаблон данных в коллекции ресурсов и назначить ему ключевое имя. Ниже приведен пример извлечения шаблона, показанного в предыдущем примере:
<Window.Resources>
...
<DataTemplate x:Key="CarsDataTemplate">
<Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue" CornerRadius="4" Width="370">
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock FontWeight="Bold" Text="{Binding Path=ModelName, Converter={StaticResource StringUpperConverter}}"/>
<TextBlock Grid.Row="1" Text="{Binding ModelNumber}"/>
</Grid>
</Border>
</DataTemplate>
</Window.Resources>
Теперь шаблон данных можно применить с использованием ссылки StaticResource:
<ListBox Name="lstCars" Margin="5" ItemTemplate="{StaticResource CarsDataTemplate}"/>
Если нужно автоматически повторно использовать тот же шаблон данных в других типах элементов управления, можно воспользоваться другим интересным трюком — установить свойство DataTemplate.DataType, чтобы идентифицировать тип привязанных данных, для которых должен применяться шаблон. Например, предыщущий пример можно было бы изменить, исключив ключ и указав этот шаблон, как предназначенный для привязки объектов CarTable, независимо от того, где они появляются:
<DataTemplate DataType="{x:Type databinding:CarTable}">
...
</DataTemplate>
В разметке предполагается, что определен префикс пространства имен XML по имени databinding, отображенный на пространство имен проекта.
Теперь этот шаблон будет использован с любым списочным элементом или элементом управления содержимым в данном окне, который привязан к объектам CarTable. Настройку ItemTemplate указывать не нужно.
Шаблоны данных не требуют привязки данных. Другими словами, использовать свойство ItemsSource для заполнения шаблона списка не понадобится. В предыдущих примерах добавлять объекты CarTable можно было декларативно (в XAML-разметке) или программно (вызывая метод ListBox.Items.Add()). В обоих случаях шаблон данных работает одинаково.
Более развитые шаблоны
Шаблоны данных могут быть замечательно самодостаточными. Наряду с базовыми элементами, такими как TextBlock и выражениями привязки данных, они также используют более изощренные элементы управления, присоединенные обработчики событий, преобразуют данные в различные представления, применяют анимацию и т.д.
Стоит рассмотреть пару небольших примеров, которые демонстрируют, насколько мощными могут быть шаблоны данных. Прежде всего, в привязке данных могут быть использованы различные объекты конвертеров, чтобы преобразовывать данные в более удобное представление (выше было показано как использовать конвертер StringUpperConverter, который преобразует названия машин в верхний регистр).
В следующем примере применяется показанный ранее конвертер ImagePathConverter для вывода изображений машин в списке:
<DataTemplate DataType="{x:Type databinding:CarTable}">
<Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue" CornerRadius="4" Width="370">
<Grid Margin="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image Grid.RowSpan="2" Width="100" Height="75" Margin="6"
Source="{Binding Path=ImageCar, Converter={StaticResource ImageConverter}}"/>
<StackPanel Grid.Column="1" Margin="2,6">
<TextBlock FontWeight="Bold" Text="{Binding Path=ModelName, Converter={StaticResource StringUpperConverter}}"/>
<TextBlock Text="{Binding ModelNumber}"/>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
Хотя данная разметка не содержит ничего экзотического, в результате получается более интересный список:
Селекторы шаблонов
Селекторы шаблонов работают точно так же, как рассмотренные ранее селекторы стилей — они проверяют привязанный объект и выбирают подходящий шаблон на основе заданной логики.
Ранее было показано, как строить селектор стилей, который ищет определенные значения и выделяет их стилем. Ниже приведен аналогичный селектор шаблонов:
public class CategoryHighlightTemplateSelector : DataTemplateSelector
{
public DataTemplate EconomyMiddleClassDataTemplate { get; set; }
public DataTemplate BuisnessPremiumClassDataTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
CarTable car = (CarTable)item;
switch (car.CategoryID)
{
case 1:
return EconomyMiddleClassDataTemplate;
case 2:
return EconomyMiddleClassDataTemplate;
case 3:
return BuisnessPremiumClassDataTemplate;
case 4:
return BuisnessPremiumClassDataTemplate;
default:
return null;
}
}
}
А вот разметка, создающая два шаблона:
<Window.Resources>
<DataTemplate x:Key="BuisnessPremiumClassDataTemplate">
<Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue" CornerRadius="4" Width="370">
<Grid Margin="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image Grid.RowSpan="2" Width="100" Height="75" Margin="6"
Source="{Binding Path=ImageCar, Converter={StaticResource ImageConverter}}"/>
<StackPanel Grid.Column="1" Margin="2,6">
<TextBlock FontWeight="Bold" Text="{Binding Path=ModelName, Converter={StaticResource StringUpperConverter}}"/>
<TextBlock Text="{Binding ModelNumber}"/>
<TextBlock FontStyle="Italic" HorizontalAlignment="Right" Text="*** Элитный автомобиль ***"/>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
<DataTemplate x:Key="EconomyMiddleClassDataTemplate">
<Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue" CornerRadius="4" Width="370">
<Grid Margin="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image Grid.RowSpan="2" Width="100" Height="75" Margin="6"
Source="{Binding Path=ImageCar, Converter={StaticResource ImageConverter}}"/>
<StackPanel Grid.Column="1" Margin="2,6">
<TextBlock Foreground="#777" Text="{Binding Path=ModelName, Converter={StaticResource StringUpperConverter}}"/>
<TextBlock FontStyle="Italic" Foreground="#777" Text="{Binding ModelNumber}"/>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</Window.Resources>
Ниже показана разметка, применяющая селектор шаблонов:
<ListBox Name="lstCars" Margin="5">
<ListBox.ItemTemplateSelector>
<databinding:CategoryHighlightTemplateSelector
EconomyMiddleClassDataTemplate="{StaticResource EconomyMiddleClassDataTemplate}"
BuisnessPremiumClassDataTemplate="{StaticResource BuisnessPremiumClassDataTemplate}"/>
</ListBox.ItemTemplateSelector>
</ListBox>
Как видите, селекторы шаблонов намного мощнее селекторов стилей, потому что каждый шаблон может отображать разные элементы, организованные в разной компоновке. Один недостаток этого подхода связан с тем, что, скорее всего, придется создавать несколько похожих шаблонов. В случае сложных шаблонов это может привести к большому объему дублирования. Чтобы облегчить сопровождение, не следует создавать слишком много шаблонов для одного списка; вместо этого используйте триггеры и стили для применения различного форматирования к шаблонам.