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:
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:
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:
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:
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:
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:
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:
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:
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.
Vea también
- Enlace de datos
- Find DataTemplate-Generated Elements (Cómo buscar elementos generados por una clase DataTemplate)
- Aplicar estilos y plantillas
- Información general sobre el enlace de datos
- Información general sobre plantillas y estilos de encabezado de columna en modo GridView
.NET Desktop feedback