Общие сведения о шаблонах данных

Модель шаблонов данных 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 сейчас выглядит следующим образом:

Screenshot of the Introduction to Data Templating Sample window showing the My Task List ListBox displaying the string representation SDKSample.Task for each source object.

Происходит следующее: без конкретных инструкций 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 выглядит так:

Screenshot of the Introduction to Data Templating Sample window showing the My Task List ListBox displaying a list of tasks.

Тем не менее это характеризуется ограниченностью и негибкостью. Кроме того, если выполняется привязка к данным 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 следующим образом:

Screenshot of the Introduction to Data Templating Sample window showing the My Task List ListBox displaying the tasks as TextBlock elements.

Создание шаблона 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:

Screenshot of the Introduction to Data Templating Sample window showing the My Task List ListBox with the modified DataTemplate.

Мы можем задать значение HorizontalContentAlignment для Stretch в ListBox, чтобы убедиться, что элементы занимают все пространство по ширине:

<ListBox Width="400" Margin="10"
     ItemsSource="{Binding Source={StaticResource myTodoList}}"
     ItemTemplate="{StaticResource myTaskTemplate}" 
     HorizontalContentAlignment="Stretch"/>

Если для свойства HorizontalContentAlignment задано значениеStretch, ListBox теперь выглядит следующим образом:

Screenshot of the Introduction to Data Templating Sample window showing the My Task List ListBox stretched to fit the screen horizontally.

Использование триггеров данных для применения значений свойств

В настоящей презентации не говорится о том, является ли 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>

Наше приложение теперь выглядит следующим образом. Домашние задачи отображаются с желтой границей, а офисные — с синей границей:

Screenshot of the Introduction to Data Templating Sample window showing the My Task List ListBox with the home and office task borders highlighted in color.

В этом примере 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 теперь выглядит так:

Screenshot of Introduction to Data Templating Sample window showing the My Task List ListBox with the Priority 1 tasks prominently displayed with a red border.

Это заключительный шаг нашего обсуждения данного примера. Полный пример см. в разделе Вводная часть примера стилизации и использования шаблонов.

Стилизация и использование шаблонов для 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>

Ниже приведен снимок экрана примера при его просмотре:

ItemsControl example screenshot

Обратите внимание, что вместо использования 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 sample screenshot

См. также