Общие сведения о шаблонах данных
Модель шаблонов данных WPF предоставляет большую гибкость при определении представления данных. Элементы управления WPF имеют встроенные функции для поддержки настройки представления данных. В этом разделе сначала демонстрируется определение шаблона DataTemplate, а затем предоставляются другие возможности шаблонов данных, такие как выбор шаблона на основе пользовательской логики и поддержка отображения иерархических данных.
Необходимые компоненты
В этом разделе рассматриваются функции шаблонов данных. Здесь отсутствуют общие сведения о понятиях привязки данных. Сведения о базовых концепциях привязки данных содержатся в разделе Обзор привязки данных.
DataTemplate является представлением данных и является одной из многих функций, предоставляемых моделью стилей и шаблонов WPF. Введение в модель стилей и шаблонов WPF, как, например, использование Style для задания свойств элементов управления, см. в разделе Стилизация и использование шаблонов.
Кроме того, важно понимать Resources
, который необходим для того, чтобы Style и DataTemplate могли использоваться повторно. Дополнительные сведения о ресурсах см. в разделе Ресурсы XAML.
Основные сведения о шаблонах данных
Чтобы продемонстрировать важность DataTemplate, давайте разберем пример привязки данных. В этом примере у нас есть ListBox с привязкой к списку объектов Task
. Каждому Task
объекту соответствует TaskName
(строка), Description
(строка), Priority
(int) и свойство типа TaskType
, которое является Enum
со значениями Home
и Work
.
<Window x:Class="SDKSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SDKSample"
Title="Introduction to Data Templating Sample">
<Window.Resources>
<local:Tasks x:Key="myTodoList"/>
</Window.Resources>
<StackPanel>
<TextBlock Name="blah" FontSize="20" Text="My Task List:"/>
<ListBox Width="400" Margin="10"
ItemsSource="{Binding Source={StaticResource myTodoList}}"/>
</StackPanel>
</Window>
Без шаблона данных DataTemplate
Без DataTemplate, наш ListBox сейчас выглядит следующим образом:
Происходит следующее: без конкретных инструкций ListBox по умолчанию вызывает ToString
при попытке отображения объектов в коллекции. Таким образом, если объект Task
переопределяет метод ToString
, то ListBox отображает строковое представление каждого исходного объекта из базовой коллекции.
Например, если класс Task
переопределяет метод ToString
таким образом, что name
— поле для TaskName
свойства:
public override string ToString()
{
return name.ToString();
}
Public Overrides Function ToString() As String
Return _name.ToString()
End Function
Тогда ListBox выглядит так:
Тем не менее это характеризуется ограниченностью и негибкостью. Кроме того, если выполняется привязка к данным XML, невозможно переопределить ToString
.
Определение простого шаблона DataTemplate
Решение заключается в определении DataTemplate. Один из способов сделать это — задать свойство ItemTemplateListBox для DataTemplate. То, что вы указываете в своем шаблоне DataTemplate, становится визуальной структурой вашего объекта данных. Следующий DataTemplate очень прост. Мы даем инструкции, чтобы каждый элемент был представлен как три элемента TextBlock в StackPanel. Каждый элемент TextBlock привязан к свойству класса Task
.
<ListBox Width="400" Margin="10"
ItemsSource="{Binding Source={StaticResource myTodoList}}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=TaskName}" />
<TextBlock Text="{Binding Path=Description}"/>
<TextBlock Text="{Binding Path=Priority}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Базовые данные в примерах этого раздела — это коллекция объектов CLR. Если выполнить привязку к данным XML, основные понятия схожи, но есть незначительные синтаксические различия. Например, вместо Path=TaskName
, вы будете задавать для XPath значение @TaskName
(если TaskName
является атрибутом вашего узла XML).
Теперь выглядит ListBox следующим образом:
Создание шаблона DataTemplate как ресурса
В примере выше мы определили DataTemplate как встроенный. Обычно его определяют в разделе ресурсов, чтобы его можно было повторно использовать, как в следующем примере:
<Window.Resources>
<DataTemplate x:Key="myTaskTemplate">
<StackPanel>
<TextBlock Text="{Binding Path=TaskName}" />
<TextBlock Text="{Binding Path=Description}"/>
<TextBlock Text="{Binding Path=Priority}"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
Теперь вы можете использовать myTaskTemplate
в качестве ресурса, как показано в следующем примере:
<ListBox Width="400" Margin="10"
ItemsSource="{Binding Source={StaticResource myTodoList}}"
ItemTemplate="{StaticResource myTaskTemplate}"/>
Так как myTaskTemplate
является ресурсом, теперь можно использовать его для других элементов управления, которые имеют свойства, принимающие тип DataTemplate. Как показано выше, для объектов ItemsControl, таких как ListBox, это свойство ItemTemplate. Для объектов ContentControl, это свойство ContentTemplate.
Свойство DataType
Класс DataTemplate имеет свойство DataType, которое очень похоже на свойство TargetType класса Style. Таким образом, вместо указания x:Key
для DataTemplate в приведенном выше примере, можно сделать следующее:
<DataTemplate DataType="{x:Type local:Task}">
<StackPanel>
<TextBlock Text="{Binding Path=TaskName}" />
<TextBlock Text="{Binding Path=Description}"/>
<TextBlock Text="{Binding Path=Priority}"/>
</StackPanel>
</DataTemplate>
Этот DataTemplate автоматически применяется ко всем объектам Task
. Обратите внимание, что в этом случае x:Key
устанавливается неявно. Таким образом, если вы присваиваете этому DataTemplate значение x:Key
, вы переопределяете неявный x:Key
, и DataTemplate не будет применяться автоматически.
Если вы привязываете ContentControl к коллекции объектов Task
, ContentControl не использует вышеупомянутый DataTemplate автоматически. Это связано с тем, что привязка к ContentControl требует больше информации, чтобы определить, хотите вы выполнить привязку ко всей коллекции или к отдельным объектам. Если ваш ContentControl отслеживает выделение типа ItemsControl, вы можете задать свойство PathContentControl с привязкой к "/
", чтобы показать, что вы заинтересованы в текущем элементе. Для примера см. Выполнение привязки к коллекции и вывод сведений в зависимости от выделенного элемента. В противном случае вам нужно будет явно указать DataTemplate, задав значение свойству ContentTemplate.
Свойство DataType особенно полезно, если у вас CompositeCollection разных типов объектов данных. Пример см. в разделе Реализация CompositeCollection.
Добавление дополнительных данных в DataTemplate
В настоящее время данные содержат необходимую информацию, но определенно это можно улучшить. Давайте усилим презентацию, добавив Border, Grid и несколько элементов TextBlock, которые описывают демонстрируемые данные.
<DataTemplate x:Key="myTaskTemplate">
<Border Name="border" BorderBrush="Aqua" BorderThickness="1"
Padding="5" Margin="5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Task Name:"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=TaskName}" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="Description:"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Path=Description}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Priority:"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Path=Priority}"/>
</Grid>
</Border>
</DataTemplate>
На следующем снимке экрана показан ListBox с этим измененным DataTemplate:
Мы можем задать значение HorizontalContentAlignment для Stretch в ListBox, чтобы убедиться, что элементы занимают все пространство по ширине:
<ListBox Width="400" Margin="10"
ItemsSource="{Binding Source={StaticResource myTodoList}}"
ItemTemplate="{StaticResource myTaskTemplate}"
HorizontalContentAlignment="Stretch"/>
Если для свойства HorizontalContentAlignment задано значениеStretch, ListBox теперь выглядит следующим образом:
Использование триггеров данных для применения значений свойств
В настоящей презентации не говорится о том, является ли Task
домашней задачей или офисной. Помните, что объект Task
имеет свойство TaskType
типа TaskType
, который является перечислением со значениями Home
и Work
.
В следующем примере DataTrigger задает для BorderBrush элемента с именем border
значение Yellow
, если свойство TaskType
имеет значение TaskType.Home
.
<DataTemplate x:Key="myTaskTemplate">
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=TaskType}">
<DataTrigger.Value>
<local:TaskType>Home</local:TaskType>
</DataTrigger.Value>
<Setter TargetName="border" Property="BorderBrush" Value="Yellow"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
Наше приложение теперь выглядит следующим образом. Домашние задачи отображаются с желтой границей, а офисные — с синей границей:
В этом примере DataTrigger использует Setter для задания значения свойства. Классы триггеров также имеют свойства EnterActions и ExitActions, позволяющие вам запускать набор действий, таких как анимация. Кроме того, имеется также класс MultiDataTrigger, позволяющий применять изменения на основе значений нескольких свойств с привязкой к данным.
Альтернативным способом достижения такого же эффекта является привязка свойства BorderBrush к свойству TaskType
и использование преобразователя значения для возврата цвета на основе значения TaskType
. Создание вышеупомянутого эффекта с помощью преобразователя является немного более эффективным с точки зрения производительности. Кроме того, создание собственных преобразователей обеспечивает большую гибкость, так как вы предоставляете свою собственную логику. В конечном счете выбор техники зависит от сценария и предпочтений. Информацию об использовании преобразователя см. в IValueConverter.
Что входит в DataTemplate?
В предыдущем примере мы поместили триггер в DataTemplate, используя свойство DataTemplate.Triggers. Setter триггера задает значение свойства элемента (элемент Border) внутри DataTemplate. При этом, если свойства, с которыми связаны ваши Setters
, не являются свойствами элементов внутри текущего DataTemplate, может быть более целесообразно задать свойства, используя Style для класса ListBoxItem (если элемент управления, к которому вы осуществляете привязку, является ListBox). Например, если вы хотите, чтобы ваш Trigger анимировал значение Opacity элемента, когда курсор мыши указывает на элемент, вы определяете триггеры в рамках стиляListBoxItem. Пример см. в разделе Вводная часть примера стилизации и использования шаблонов.
В целом, имейте в виду, что DataTemplate применяется к каждому из созданных ListBoxItem (дополнительные сведения о том, как и где он фактически применяется, см. на странице ItemTemplate.). Ваш шаблон DataTemplate отвечает только за презентацию и внешний вид объектов данных. В большинстве случае, все другие аспекты представления, например как элемент выглядит при его выборе или как ListBox размещает элементы, не входят в определение шаблона DataTemplate. Пример см. в разделе Стилизация и использование шаблонов для ItemsControl.
Выбор DataTemplate на основе свойств объекта данных
В разделе Свойство DataType мы говорили о том, что можно определить различные шаблоны данных для различных объектов данных. Это особенно полезно при наличии CompositeCollection различных типов или коллекций с элементами различных типов. В разделе Использование триггеров данных для применения значений свойств было показано, что если имеется коллекция одинаковых типов объектов данных, можно создать шаблон DataTemplate и затем использовать триггеры для применения изменений на основании значений свойств каждого объекта данных. Тем не менее, хотя триггеры позволяют применить значения свойств или запустить анимацию, они не предоставляют гибкость, достаточную для реконструкции структуры объектов данных. Некоторые сценарии могут потребовать создания другого шаблона DataTemplate для данных объектов, которые имеют тот же тип, но отличающиеся свойства.
Например, если объект Task
имеет значение Priority
свойства 1
, вы можете задать совершенно другой вид для него, чтобы сделать его сигналом оповещения. В этом случае можно создать шаблон DataTemplate для отображения объектов с высоким приоритетом Task
. Давайте добавим следующий DataTemplate в раздел ресурсов:
<DataTemplate x:Key="importantTaskTemplate">
<DataTemplate.Resources>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="20"/>
</Style>
</DataTemplate.Resources>
<Border Name="border" BorderBrush="Red" BorderThickness="1"
Padding="5" Margin="5">
<DockPanel HorizontalAlignment="Center">
<TextBlock Text="{Binding Path=Description}" />
<TextBlock>!</TextBlock>
</DockPanel>
</Border>
</DataTemplate>
В этом примере используется свойство DataTemplate.Resources. Ресурсы, определенные в этом разделе, являются общими для элементов шаблона DataTemplate.
Чтобы задать логику для выбора того, какой DataTemplate использовать в зависимости от значения Priority
объекта данных, создайте подкласс DataTemplateSelector и переопределите метод SelectTemplate. В следующем примере метод SelectTemplate предоставляет логику для возвращения соответствующего шаблона на основе значения свойства Priority
. Возвращаемый шаблон находится в ресурсах запечатывающего элемента Window.
using System.Windows;
using System.Windows.Controls;
namespace SDKSample
{
public class TaskListDataTemplateSelector : DataTemplateSelector
{
public override DataTemplate
SelectTemplate(object item, DependencyObject container)
{
FrameworkElement element = container as FrameworkElement;
if (element != null && item != null && item is Task)
{
Task taskitem = item as Task;
if (taskitem.Priority == 1)
return
element.FindResource("importantTaskTemplate") as DataTemplate;
else
return
element.FindResource("myTaskTemplate") as DataTemplate;
}
return null;
}
}
}
Namespace SDKSample
Public Class TaskListDataTemplateSelector
Inherits DataTemplateSelector
Public Overrides Function SelectTemplate(ByVal item As Object, ByVal container As DependencyObject) As DataTemplate
Dim element As FrameworkElement
element = TryCast(container, FrameworkElement)
If element IsNot Nothing AndAlso item IsNot Nothing AndAlso TypeOf item Is Task Then
Dim taskitem As Task = TryCast(item, Task)
If taskitem.Priority = 1 Then
Return TryCast(element.FindResource("importantTaskTemplate"), DataTemplate)
Else
Return TryCast(element.FindResource("myTaskTemplate"), DataTemplate)
End If
End If
Return Nothing
End Function
End Class
End Namespace
Затем можно объявить TaskListDataTemplateSelector
как ресурс:
<Window.Resources>
<local:TaskListDataTemplateSelector x:Key="myDataTemplateSelector"/>
</Window.Resources>
Чтобы использовать ресурс селектора шаблонов, присвойте его свойству ItemTemplateSelectorListBox. ListBox Вызывает метод SelectTemplateTaskListDataTemplateSelector
для каждого элемента в базовой коллекции. Вызов передает объект данных в качестве параметра элемента. Затем шаблон DataTemplate, возвращенный этим методом, применяется к этому объекту данных.
<ListBox Width="400" Margin="10"
ItemsSource="{Binding Source={StaticResource myTodoList}}"
ItemTemplateSelector="{StaticResource myDataTemplateSelector}"
HorizontalContentAlignment="Stretch"/>
После размещения селектора шаблонов ListBox теперь выглядит так:
Это заключительный шаг нашего обсуждения данного примера. Полный пример см. в разделе Вводная часть примера стилизации и использования шаблонов.
Стилизация и использование шаблонов для ItemsControl
Хотя ItemsControl - не единственный тип элемента управления, с которым вы можете использовать DataTemplate, привязка ItemsControl к коллекции является очень распространенным сценарием. В разделе Что входит в DataTemplate мы обсуждали, что определение вашего шаблона DataTemplate должно быть связано только с представлением данных. Чтобы узнать, когда шаблон DataTemplate не подходит для использования, важно понимать различные свойства стиля и шаблона, которые предоставляются ItemsControl. Следующий пример предназначен для демонстрации функции каждого из этих свойств. Элемент ItemsControl в этом примере, привязан к той же коллекции Tasks
, что и в предыдущем примере. Для демонстрационных целей все стили и шаблоны в этом примере объявлены встроенными.
<ItemsControl Margin="10"
ItemsSource="{Binding Source={StaticResource myTodoList}}">
<!--The ItemsControl has no default visual appearance.
Use the Template property to specify a ControlTemplate to define
the appearance of an ItemsControl. The ItemsPresenter uses the specified
ItemsPanelTemplate (see below) to layout the items. If an
ItemsPanelTemplate is not specified, the default is used. (For ItemsControl,
the default is an ItemsPanelTemplate that specifies a StackPanel.-->
<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<Border BorderBrush="Aqua" BorderThickness="1" CornerRadius="15">
<ItemsPresenter/>
</Border>
</ControlTemplate>
</ItemsControl.Template>
<!--Use the ItemsPanel property to specify an ItemsPanelTemplate
that defines the panel that is used to hold the generated items.
In other words, use this property if you want to affect
how the items are laid out.-->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<!--Use the ItemTemplate to set a DataTemplate to define
the visualization of the data objects. This DataTemplate
specifies that each data object appears with the Proriity
and TaskName on top of a silver ellipse.-->
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate.Resources>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="18"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</DataTemplate.Resources>
<Grid>
<Ellipse Fill="Silver"/>
<StackPanel>
<TextBlock Margin="3,3,3,0"
Text="{Binding Path=Priority}"/>
<TextBlock Margin="3,0,3,7"
Text="{Binding Path=TaskName}"/>
</StackPanel>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
<!--Use the ItemContainerStyle property to specify the appearance
of the element that contains the data. This ItemContainerStyle
gives each item container a margin and a width. There is also
a trigger that sets a tooltip that shows the description of
the data object when the mouse hovers over the item container.-->
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Control.Width" Value="100"/>
<Setter Property="Control.Margin" Value="5"/>
<Style.Triggers>
<Trigger Property="Control.IsMouseOver" Value="True">
<Setter Property="Control.ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=Content.Description}"/>
</Trigger>
</Style.Triggers>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
Ниже приведен снимок экрана примера при его просмотре:
Обратите внимание, что вместо использования ItemTemplateможно использовать ItemTemplateSelector. Пример см. в предыдущем разделе. Аналогично, вместо использования ItemContainerStyle есть возможность использовать ItemContainerStyleSelector.
Двумя другими свойствами ItemsControl, связанными со стилем, которые не показаны здесь, являются GroupStyle и GroupStyleSelector.
Поддержка иерархических данных
Пока мы только рассматривали как привязывать и отображать одну коллекцию. Иногда встречается коллекция, содержащая другие коллекции. Класс HierarchicalDataTemplate предназначен для использования с типами HeaderedItemsControl для отображения таких данных. В следующем примере ListLeagueList
является списком объектов League
. Каждый объект League
содержит Name
и коллекцию объектов Division
. Каждый Division
содержит Name
и коллекцию объектов Team
, и каждый объект Team
содержит Name
.
<Window x:Class="SDKSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="HierarchicalDataTemplate Sample"
xmlns:src="clr-namespace:SDKSample">
<DockPanel>
<DockPanel.Resources>
<src:ListLeagueList x:Key="MyList"/>
<HierarchicalDataTemplate DataType = "{x:Type src:League}"
ItemsSource = "{Binding Path=Divisions}">
<TextBlock Text="{Binding Path=Name}"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType = "{x:Type src:Division}"
ItemsSource = "{Binding Path=Teams}">
<TextBlock Text="{Binding Path=Name}"/>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type src:Team}">
<TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>
</DockPanel.Resources>
<Menu Name="menu1" DockPanel.Dock="Top" Margin="10,10,10,10">
<MenuItem Header="My Soccer Leagues"
ItemsSource="{Binding Source={StaticResource MyList}}" />
</Menu>
<TreeView>
<TreeViewItem ItemsSource="{Binding Source={StaticResource MyList}}" Header="My Soccer Leagues" />
</TreeView>
</DockPanel>
</Window>
Пример показывает, что с помощью HierarchicalDataTemplate, можно отобразить данные списка, содержащего другие списки. Ниже приведен снимок экрана примера.
См. также
.NET Desktop feedback