Поделиться через


Обзор шаблонизации данных

Модель шаблонов данных 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, если свойство TaskTypeTaskType.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.

Чтобы заложить логику выбора, какой 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

См. также