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

Модель шаблонов данных WPF обеспечивает большую гибкость при определении представления данных. В элементах управления WPF предусмотрены встроенные функциональные возможности поддержки настройки представления данных. В начале этого раздела демонстрируется метод определения DataTemplate, после чего приводится описание других функций шаблонов данных, например выбора шаблонов на основе пользовательской логики и поддержки отображения иерархических данных.

В этом разделе содержатся следующие подразделы.

  • Предварительные требования
  • Основы шаблонов данных
  • Добавление нескольких DataTemplate
  • Выбор DataTemplate в зависимости от свойства объекта данных
  • Определение стиля и шаблона ItemsControl
  • Поддержка иерархических данных
  • Связанные разделы

Предварительные требования

В этом разделе основное внимание уделяется возможностям шаблонов данных, а не использованию привязки данных. Сведения о базовых концепциях привязки данных содержатся в разделе Общие сведения о связывании данных.

DataTemplate является представлением данных и одним из многих средств, предоставляемых моделью стилей и шаблонов WPF. Введение в модель стилей и шаблонов WPF, например порядок использования Style для задания свойств элементов управления, содержится в разделе Стилизация и использование шаблонов.

Кроме того, важно понимать, что такое Resources, которые являются по существу объектами, такими как Style и DataTemplate, для многократного использования. Дополнительные сведения о ресурсах содержатся в разделе Общие сведения о ресурсах.

Основы шаблонов данных

Этот подраздел состоит из следующих пунктов.

  • Без DataTemplate
  • Определение простого DataTemplate
  • Создание DataTemplate в качестве ресурса
  • Свойство DataType

Чтобы продемонстрировать важность DataTemplate, давайте перейдем к примеру привязки данных. В этом примере используется объект ListBox, который связан со списком объектов Task. Каждый объект Task имеет TaskName (строка), Description (строка), Priority (int) и свойство типа TaskType, которое представляет Enum со значениями Home и Work.

<Window x:Class="SDKSample.Window1"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://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 Overrides Function ToString() As String
    Return _name.ToString()
End Function
public override string ToString()
{
    return name.ToString();
}

Тогда ListBox выглядит следующим образом:

Снимок экрана примера шаблона данных

Однако это ограничивает действия и не является гибким методом. Кроме того, при осуществлении привязки к данным XML невозможно переопределить ToString.

Определение простого DataTemplate

Решением является определение 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. Чаще он определяется в разделе ресурсов, поэтому он может быть повторно используемым объектом, как в следующем примере:

<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. Пример см. в разделе Introduction to Styling and Templating Sample (пример "Введение в стили и шаблоны").

Имейте в виду, что объект DataTemplate применяется к каждому из созданных объектов ListBoxItem (дополнительные сведений о том, как и где он применен фактически, см. страницу ItemTemplate). Объект DataTemplate отвечает только за презентацию и внешний вид объектов данных. В большинстве случаев, все другие аспекты презентации, например, как выглядит элемент при его выборе или как ListBox размещает элементы, не входят в определение DataTemplate. Пример содержится в разделе Определение стиля и шаблона ItemsControl.

Выбор DataTemplate в зависимости от свойства объекта данных

В разделе Свойство типа данных обсуждалось, что можно определить различные шаблоны данных для различных объектов данных. Это особенно полезно при наличии 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.


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
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;
        }
    }
}

Затем мы можно объявить TaskListDataTemplateSelector в качестве ресурса:

<Window.Resources>


...


<local:TaskListDataTemplateSelector x:Key="myDataTemplateSelector"/>


...


</Window.Resources>

Для использования ресурса выбора шаблона назначьте его свойству ItemTemplateSelector объекта ListBox. Объект ListBox вызывает метод SelectTemplate выбора TaskListDataTemplateSelector для каждого элемента в базовой коллекции. Вызов передает объект данных в качестве параметра элемента. Объект DataTemplate возвращается методом и затем применяется к объекту данных.

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

С помощью выбора шаблона на месте ListBox теперь выглядит следующим образом:

Снимок экрана примера шаблона данных

Это заключительный шаг нашего обсуждения данного примера. Полный код примера см. в разделе Introduction to Data Templating Sample (пример "Введение в шаблоны данных").

Определение стиля и шаблона 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

Обратите внимание, что вместо использования 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="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://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

См. также

Задачи

Практическое руководство. Поиск элементов, созданных с использованием шаблона DataTemplate

Основные понятия

Оптимизация производительности: привязка данных

Стилизация и использование шаблонов

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

Общие сведения о стилях заголовков столбцов GridView и шаблонах