Información general sobre plantillas de datos

El modelo de plantillas de datos de WPF ofrece gran flexibilidad para definir la presentación de los datos. Los controles WPF tienen funcionalidad integrada que admite la personalización de la presentación de los datos. Este tema muestra primero cómo definir DataTemplate y, luego, presenta otras características de creación de plantillas de datos, como la selección de plantillas basadas en lógica personalizada y la asistencia para la presentación de datos jerárquicos.

Prerrequisitos

Este tema se centra en las características de creación de plantillas de datos y no es una introducción a los conceptos de enlace de datos. Para información sobre los conceptos básicos de enlace de datos, vea the Información general sobre el enlace de datos.

DataTemplate trata de la presentación de datos y es una de las muchas características que brinda el modelo de estilos y plantillas de WPF. Para obtener una introducción al modelo de estilos y plantillas de WPF, por ejemplo, cómo usar Style para establecer propiedades en controles, vea el tema Estilos y plantillas en WPF.

Además, es importante comprender Resources, que son esencialmente los que habilitan que objetos como Style y DataTemplate sean reutilizables. Para más información sobre los recursos, vea Recursos XAML.

Conceptos básicos de plantillas de datos

Para mostrar la importancia de DataTemplate, analicemos un ejemplo de enlace de datos. En este ejemplo, tenemos una clase ListBox que está enlazada a una lista de objetos Task. Cada objeto Task tiene un TaskName (cadena), un Description (cadena), un Priority (int) y una propiedad de tipo TaskType, que es un Enum con valores Home y 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>

Sin un DataTemplate

Sin DataTemplate, el aspecto actual de nuestra clase ListBox es similar a lo siguiente:

Captura de pantalla de la ventana Introducción a la aplicación de plantillas de datos de ejemplo que muestra el cuadro de lista Mi lista de tareas donde aparece la representación de cadena SDKSample.Task para cada objeto de origen.

Lo que sucede es que, sin ninguna instrucción concreta, ListBox llama de forma predeterminada a ToString al intentar mostrar los objetos de la colección. Por tanto, si el objeto Task invalida el método ToString, ListBox muestra la representación de cadena de cada objeto de origen en la colección subyacente.

Por ejemplo, si la clase Task invalida el método ToString de esta manera, donde name es el campo para la propiedad TaskName:

public override string ToString()
{
    return name.ToString();
}
Public Overrides Function ToString() As String
    Return _name.ToString()
End Function

Entonces, ListBox tiene un aspecto similar al siguiente:

Captura de pantalla de la ventana Introducción a la aplicación de plantillas de datos de ejemplo que muestra el cuadro de lista Mi lista de tareas donde aparece una lista de tareas.

Pero eso resulta limitante e inflexible. Además, si enlaza a datos XML, no podrá invalidar ToString.

Definir un DataTemplate simple

La solución es definir DataTemplate. Una manera de hacerlo es establecer la propiedad ItemTemplate de ListBox en DataTemplate. Lo que especifique en DataTemplate pasa a ser la estructura visual del objeto de datos. La siguiente clase DataTemplate es bastante simple. Las instrucciones que damos determinan que cada elemento aparezca como tres elementos TextBlock dentro de StackPanel. Cada elemento TextBlock está enlazado a una propiedad de clase 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>

Los datos subyacentes de los ejemplos de este tema son una colección de objetos CLR. Si enlaza a datos XML, los conceptos fundamentales son los mismos, pero existe una ligera diferencia sintáctica. Por ejemplo, en lugar de tener Path=TaskName, establecería XPath en @TaskName (si TaskName es un atributo de su nodo XML).

Ahora, nuestra clase ListBox es similar a lo siguiente:

Captura de pantalla de la ventana Introducción a la aplicación de plantillas de datos de ejemplo que muestra el cuadro de lista Mi lista de tareas donde aparece una lista de tareas.

Crear el DataTemplate como recurso

En el ejemplo anterior, definimos la clase DataTemplate insertada. Es más frecuente definirlo en la sección de recursos para que pueda ser un objeto reutilizable, como en el ejemplo siguiente:

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

Ahora puede usar myTaskTemplate como recurso, como en el ejemplo siguiente:

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

Como myTaskTemplate es un recurso, ahora puede usarlo en otros controles que tengan una propiedad que tome un tipo DataTemplate. Como se muestra anteriormente, para objetos ItemsControl, como ListBox, es la propiedad ItemTemplate. En el caso de los objetos ContentControl, es la propiedad ContentTemplate.

La propiedad DataType

La clase DataTemplate tiene una propiedad DataType muy similar a la propiedad TargetType de la clase Style. Por lo tanto, en lugar de especificar x:Key para el objeto DataTemplate del ejemplo anterior, puede hacer lo siguiente:

<DataTemplate DataType="{x:Type local:Task}">
  <StackPanel>
    <TextBlock Text="{Binding Path=TaskName}" />
    <TextBlock Text="{Binding Path=Description}"/>
    <TextBlock Text="{Binding Path=Priority}"/>
  </StackPanel>
</DataTemplate>

Esta clase DataTemplate se aplica de forma automática a todos los objetos Task. Tenga en cuenta que en este caso el x:Key se establece implícitamente. Por lo tanto, si asigna un valor x:Key a esta clase DataTemplate, estará invalidando el valor implícito x:Key y DataTemplate no se aplicará de forma automática.

Si está enlazando ContentControl a una colección de objetos Task, ContentControl no usará la clase DataTemplate anterior de forma automática. Esto se debe a que el enlace en ContentControl requiere más información para distinguir si se quiere enlazar a toda una colección o a los objetos individuales. Si ContentControl está siguiendo la selección de un tipo ItemsControl, puede establecer la propiedad Path del enlace de ContentControl a "/" para indicar que está interesado en el elemento actual. Para obtener un ejemplo, vea Bind to a Collection and Display Information Based on Selection (Cómo: Enlazar a una colección y mostrar información basada en la selección). Si no, tendrá que especificar DataTemplate de forma explicita mediante el establecimiento de la propiedad ContentTemplate.

La propiedad DataType es particularmente útil si tiene un CompositeCollection de diferentes tipos de objetos de datos. Para obtener un ejemplo, vea Implement a CompositeCollection (Cómo: Implementar una CompositeCollection).

Agregar más elementos al DataTemplate

Actualmente los datos aparecen con la información necesaria, pero sin duda hay margen de mejora. Mejoremos la presentación añadiendo Border, Grid y algunos elementos TextBlock que describan los datos presentados.


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

En la captura de pantalla siguiente se muestra ListBox con esta clase DataTemplate modificada:

Captura de pantalla de la ventana Introducción a la aplicación de plantillas de datos de ejemplo que muestra el cuadro de lista Mi lista de tareas con la plantilla DataTemplate modificada.

Podemos establecer HorizontalContentAlignment en Stretch en ListBox para asegurar que la anchura de los elementos ocupan todo el espacio:

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

Con la propiedad HorizontalContentAlignment establecida en Stretch, ListBox tiene el aspecto siguiente:

Captura de pantalla de la ventana Introducción a la aplicación de plantillas de datos de ejemplo que muestra el cuadro de lista Mi lista de tareas extendido para ajustarse horizontalmente a la pantalla.

Usar DataTriggers para aplicar valores de propiedad

La presentación actual no nos indica si una Task es una tarea doméstica o una tarea de oficina. Recuerde que el objeto Task tiene una propiedad TaskType de tipo TaskType, que es una enumeración con valores Home y Work.

En el ejemplo siguiente, DataTrigger establece la propiedad BorderBrush del elemento denominado border en Yellow si la propiedad TaskType es 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>

Nuestra aplicación tiene ahora el aspecto siguiente. Las tareas domésticas aparecen con un borde amarillo y las tareas de oficina, con un borde aguamarina:

Captura de pantalla de la ventana Introducción a la aplicación de plantillas de datos de ejemplo que muestra el cuadro de lista Mi lista de tareas con los bordes de las tareas domésticas y de oficina resaltados en color.

En este ejemplo de DataTrigger se usa Setter para establecer un valor de propiedad. Las clases desencadenantes también tienen propiedades EnterActions y ExitActions que le permiten empezar un conjunto de acciones, como animaciones. Además, también hay una clase MultiDataTrigger que le permite aplicar cambios basados en varios valores de propiedad enlazada a datos.

Una forma alternativa de conseguir el mismo efecto consiste en enlazar la propiedad BorderBrush a la propiedad TaskType y usar un convertidor de valores para devolver el color basado en el valor TaskType. La creación del efecto anterior mediante un convertidor es ligeramente más eficiente en términos de rendimiento. Además, la creación de su propio convertidor le brinda mayor flexibilidad porque usted proporciona su propia lógica. En última instancia, la técnica que elija depende de su escenario y de sus preferencias. Para información sobre cómo escribir un convertidor, vea IValueConverter.

Lo que corresponde a un DataTemplate

En el ejemplo anterior, colocamos el desencadenador dentro de DataTemplate mediante la propiedad DataTemplate.Triggers. La clase Setter de los desencadenadores establece el valor de la propiedad de un elemento (el elemento Border) que se encuentra dentro de DataTemplate. Pero, si las propiedades con las que la función Setters está ocupada no son propiedades de elementos dentro de la clase DataTemplate actual, puede que sea más apropiado establecer las propiedades mediante Style para la clase ListBoxItem (si el control que está enlazando es ListBox). Por ejemplo, si quiere que su clase Trigger anime el valor Opacity de un elemento cuando el mouse apunte hacia él, defina los desencadenadores dentro del estilo ListBoxItem. Para obtener un ejemplo, vea Introducción a la aplicación de estilos y plantillas de ejemplo.

En general, tenga en cuenta que DataTemplate se aplica a cada uno de las clases ListBoxItem generadas (para más información sobre cómo y cuándo se aplica, vea la página ItemTemplate). DataTemplate solo afecta a la presentación y el aspecto de los objetos de datos. En la mayoría de los casos, todas las demás facetas de presentación, como el aspecto que tiene un elemento cuando se selecciona o la forma en que ListBox dispone los elementos, no corresponden a la definición de DataTemplate. Para obtener un ejemplo, vea la sección Aplicar estilos y plantillas con un ItemsControl.

Elegir un DataTemplate en función de las propiedades del objeto de datos

En la sección La propiedad DataType, explicamos que se pueden definir distintas plantillas de datos para objetos de datos diferentes. Esto resulta especialmente útil cuando se tiene una clase CompositeCollection de distintos tipos o colecciones con elementos de tipos diferentes. En la sección Usar DataTriggers para aplicar valores de propiedad, hemos mostrado que si tiene una colección del mismo tipo de objetos de datos, puede crear DataTemplate y luego usar desencadenadores para aplicar cambios basados en los valores de propiedad de cada objeto de datos. Aunque los desencadenadores le permiten aplicar valores de propiedad o iniciar animaciones, no le ofrecen la flexibilidad de reconstruir la estructura de los objetos de datos. Es posible que algunos escenarios requieran que se cree otra clase DataTemplate para objetos de datos que son del mismo tipo pero tienen propiedades diferentes.

Por ejemplo, puede que cuando un objeto Task tenga un valor Priority de 1 quiera darle un aspecto completamente distinto para que actúe como alerta para usted mismo. En ese caso, cree DataTemplate para la presentación de los objetos Task de alta prioridad. Añadamos la clase DataTemplate siguiente a la sección de recursos:

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

En este ejemplo se usa la propiedad DataTemplate.Resources. Los elementos contenidos en DataTemplate comparten los recursos que se definen en esa sección.

Para proporcionar lógica para elegir qué DataTemplate usar basado en el valor Priority del objeto de datos, cree una subclase de DataTemplateSelector e invalide el método SelectTemplate. En el ejemplo siguiente, el método SelectTemplate proporciona la lógica que devuelve la plantilla adecuada en función del valor de la propiedad Priority. La plantilla que se devuelve se encuentra en los recursos del elemento envolvente 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

Podemos declarar el TaskListDataTemplateSelector como recurso:

<Window.Resources>
<local:TaskListDataTemplateSelector x:Key="myDataTemplateSelector"/>
</Window.Resources>

Para usar el recurso de selector de plantilla, asígnelo a la propiedad ItemTemplateSelector de ListBox. ListBox llama al método SelectTemplate de TaskListDataTemplateSelector para cada uno de los elementos de la colección subyacente. La llamada pasa el objeto de datos como parámetro del elemento. Después, el elemento DataTemplate devuelto por el método se aplica a ese objeto de datos.

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

Una vez el selector de plantillas está en su sitio, ListBox se muestra como a continuación:

Captura de pantalla de la ventana Introducción a la aplicación de plantillas de datos de ejemplo que muestra el cuadro de lista Mi lista de tareas con las tareas de prioridad 1 destacadas con un borde rojo.

Con esto concluye la explicación de este ejemplo. Para obtener el ejemplo completo, vea Introducción a la aplicación de plantillas de ejemplo.

Aplicar estilos y plantillas con un ItemsControl

Aunque ItemsControl no es el único tipo de control que puede usar con DataTemplate, enlazar ItemsControl a una colección es un escenario muy común. En la sección Lo que corresponde a un DataTemplate explicamos que la definición de DataTemplate solo debe afectar a la presentación de datos. Para saber cuándo no es adecuado usar DataTemplate, es importante comprender las distintas propiedades de estilo y plantilla que ofrece ItemsControl. El ejemplo siguiente se ha diseñado para ilustrar la función de cada una de dichas propiedades. La clase ItemsControl de este ejemplo se enlaza a la misma colección Tasks del ejemplo anterior. A efectos de demostración, los estilos y las plantillas de este ejemplo se declaran todas como inline.

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

La siguiente captura de pantalla muestra el ejemplo cuando se representa:

Captura de pantalla de ejemplo ItemsControl

Tenga en cuenta que, en lugar de usar ItemTemplate, puede usar ItemTemplateSelector. Consulte la sección anterior para obtener un ejemplo. Del mismo modo, en vez de usar ItemContainerStyle, tiene la opción de usar ItemContainerStyleSelector.

Otras dos propiedades de ItemsControl relacionadas con el estilo que no se muestran aquí son GroupStyle y GroupStyleSelector.

Compatibilidad con datos jerárquicos

Hasta ahora solo hemos examinado cómo enlazar a una sola colección y mostrarla. A veces se tiene una colección que contiene otras colecciones. La clase HierarchicalDataTemplate está diseñada para usarse con tipos de HeaderedItemsControl para mostrar estos datos. En el ejemplo siguiente, ListLeagueList es una lista de objetos League. Cada objeto League tiene un Name y una colección de objetos Division. Cada Division tiene un Name y una colección de objetos Team y cada objeto Team tiene un 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>

El ejemplo muestra que, mediante HierarchicalDataTemplate, puede mostrar fácilmente datos de lista que contienen otras listas. La siguiente captura de pantalla muestra el ejemplo.

Captura de pantalla de ejemplo HierarchicalDataTemplate

Vea también