Обзор шаблонизации данных
Модель шаблонов данных 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. Один из способов сделать это — установить свойству ItemTemplate объекта ListBox значение 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 inline. Чаще всего его можно определить в разделе ресурсов, чтобы он был повторно используемым объектом, как показано в следующем примере:
<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, можно задать для свойства Path привязки ContentControl значение "/
", чтобы указать, что вы заинтересованы в текущем элементе. Как пример, см. раздел Привязка к коллекции и отображение сведений на основе выбора. В противном случае необходимо явно указать 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 теперь выглядит следующим образом:
Применение значений свойств с помощью DataTriggers
Текущая презентация не сообщает нам, является ли 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".
выбор шаблона данных на основе свойств объекта данных
В разделе свойство DataType мы обсудили, что можно определить различные шаблоны данных для различных объектов данных. Это особенно полезно, когда у вас есть несколько CompositeCollection различных типов или коллекций с элементами разных типов. В разделе , который посвящен использованию DataTriggers для применения значений свойств, мы показали, что при наличии коллекции объектов данных одного типа можно создать 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 использовать на основе значения 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 вызывает метод SelectTemplate объекта TaskListDataTemplateSelector
для каждого элемента в базовой коллекции. Вызов передает объект данных в качестве параметра элемента. Затем к объекту данных применяется DataTemplate, возвращаемая методом.
<ListBox Width="400" Margin="10"
ItemsSource="{Binding Source={StaticResource myTodoList}}"
ItemTemplateSelector="{StaticResource myDataTemplateSelector}"
HorizontalContentAlignment="Stretch"/>
С помощью селектора шаблона ListBox теперь отображается следующим образом:
Это завершает наше обсуждение этого примера. Полный пример см. в разделе Введение в шаблон данных.
Стилизация и создание шаблонов для ItemsControl
Несмотря на то, что ItemsControl не является единственным типом управления, с которым можно использовать DataTemplate, это очень распространенный сценарий привязки ItemsControl к коллекции. В разделе «Что включается в DataTemplate» мы обсудили, что определение вашего DataTemplate должно касаться только представления данных. Важно понимать различные свойства стиля и шаблона, предоставляемые ItemsControl, чтобы знать, когда не следует использовать DataTemplate. В следующем примере показана функция каждого из этих свойств.
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>
Ниже приведен снимок экрана примера при отрисовки:
Пример скриншота 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можно легко отобразить данные списка, содержащие другие списки. Ниже приведен снимок экрана примера.
снимок экрана с примером HierarchicalDataTemplate
См. также
.NET Desktop feedback