He estado usando ContentPresenter en muchos escenarios y quiero compartir algunos tips acerca de como usarlo y de la flexibilidad que este ofrece para crear controles multi-contenido. Para demostrarlo he creado un control que soporta una contenido ‘Lista’ y otro ‘Detalle o Editor’ muy común en aplicaciones de edición de datos. Al final del articulo se encuentra el vinculo de descarga.
Este es un screenshot de una aplicación de ejemplo que he creado para demostrar el control multi-contenido:
Soluciones con ContentPresenter
Al igual que un control HeaderedItemsControl el cual tiene propiedades Content y Header, nosotros estaremos usando el mismo ContentPresenter pero para mostrar distinta información del control establecidas en las siguientes propiedades:
List(object): En esta propiedad se coloca el IEnumerable(Array,List<>,etc) a mostrar como lista navegable. CurrentItem(object): Esta propiedad se enlaza para mostrar el elemento seleccionado actualmente en la lista. Como el control debe ser full personalizable usamos las siguientes propiedades para mejorar la presentación:
ListTemplate(DataTemplate) CurrentItemTemplate(DataTemplate) Podemos imaginarnos dos partes visuales en el control de la siguiente forma:
Para usarlo aplicamos un ControlTemplate,ListTemplate y CurrentItemTemplate de la siguiente forma:
1: <listeditors:ListEditor List="{Binding OrderDetails}">
2: <listeditors:ListEditor.ListTemplate>
3: <DataTemplate>
4: <ListView Width="220" ItemsSource="{Binding}"
5: SelectedItem="{Binding CurrentItem,Mode=TwoWay,RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type listeditors:ListEditor}}}">
6: <ListView.View>
7: <GridView>
8: <GridView.Columns>
9: <GridViewColumn Width="100" Header="Product" DisplayMemberBinding="{Binding ProductCode}"/>
10: <GridViewColumn Width="100" Header="Quantity" DisplayMemberBinding="{Binding Quantity}"/>
11: </GridView.Columns>
12: </GridView>
13: </ListView.View>
14: </ListView>
15: </DataTemplate>
16: </listeditors:ListEditor.ListTemplate>
17: <listeditors:ListEditor.CurrentItemTemplate>
18: <DataTemplate>
19: <StackPanel>
20: <DockPanel>
21: <TextBlock Text="Product Code" Width="100"/>
22: <TextBox Text="{Binding ProductCode}"></TextBox>
23: </DockPanel>
24: <DockPanel>
25: <TextBlock Text="Quantity" Width="100"/>
26: <TextBox Text="{Binding Quantity}"></TextBox>
27: </DockPanel>
28: <DockPanel>
29: <TextBlock Text="Comments" Width="100"/>
30: <TextBox Height="30" Text="{Binding Comment}"></TextBox>
31: </DockPanel>
32: </StackPanel>
33: </DataTemplate>
34: </listeditors:ListEditor.CurrentItemTemplate>
35: <listeditors:ListEditor.Template>
36: <ControlTemplate TargetType="listeditors:ListEditor">
37: <DockPanel>
38: <ContentPresenter Margin="4" DockPanel.Dock="Left" ContentSource="List" />
39: <ContentPresenter Margin="4" ContentSource="CurrentItem" />
40: </DockPanel>
41: </ControlTemplate>
42: </listeditors:ListEditor.Template>
43: </listeditors:ListEditor>
Uso de ContentPresenter
Preste atencion a los ContentPresenter que estan en las lineas 38 y 39. El primero mostrara los datos de la propiedad ‘List’ usando el DataTemplate configurado entre las lineas 2-16 y el Segundo ContentPresenter mostrara los datos de la propiedad ‘CurrentItem’ usando el DataTemplate configurado entre las lineas 17-34.
Realmente quien hace todo el trabajo es el ContentPresenter con sus propiedades Content,ContentTemplate y ContentTemplateSelector que se asignan automáticamente con las propiedades del control donde pertenece el ControlTemplate basándose en el nombre establecido en la propiedad ContentSource.
Ejemplo. Esta es una configuración correcta de ContentPresenter en un ControlTemplate:
<ContentPresenter ContentSource="CurrentItem" ContentTemplate="{TemplateBinding CurrentItemTemplate}" ContentTemplateSelector="{TemplateBinding CurrentItemTemplateSelector}" />
Sin embargo se puede resumir con ContentSource:
<ContentPresenter Margin="4" ContentSource="CurrentItem" />
Desestimare de algunos ‘Mitos’ sobre ContentPresenter:
- ContentPresenter PUEDE usarse en cualquier ControlTemplate para mostrar cualquier propiedad del Control donde aplicamos el ControlTemplate.
- ContentPresenter busca AUTOMATICAMENTE las propiedades ‘Content’,‘ContentTemplate’ y ‘ContentTemplateSelector’ en el control donde se aplica el ControlTemplate a menos que se asignen directamente via TemplateBinding o usando ContentSource. Esto es para minimizar la configuración en Controles de tipo ContentControl.
- ContentPresenter NO REQUIERE que el Control al que le estamos aplicando el Template herede de la clase ContentControl. Sin embargo, es necesario configurar ‘ContentSource’ para la propiedad que necesitemos mostrar como fue el caso de nuestro control.
- ContentPresenter SOLAMENTE REQUIERE una propiedad para Content. Tanto ContentTemplate como ContentTemplateSelector son opcionales y se usan automaticamente(via ContentSource) solo si están presentes.
- La funcionalidad de ContentSource SOLO ESTA DISPONIBLE en ControlTemplate’s y no en DataTemplate’s esto se debe a que el mecanismo de asignación automática de propiedades se hace atreves de TemplatedParent que se asigna solo dentro del contexto de un DataTemplate.
DataContext y Enlace de Datos
El DataContext es asignado automaticamente dentro del DataTemplate por el ContentPresenter, es por eso que en la linea 4 podemos ver como se enlaza el ItemsSource del ListView usando la extension Binding de Forma sencilla. Recordemos que este DataTemplate es usado por el ContentPresenter de la propiedad ‘List’ asi que el DataContext contiene el mismo valor asignado a la propiedad ‘List’ del control que ha sido enlazada en la linea 1.
El mismo caso sucede con el DataTemplate de ‘CurrentItemTemplate’ el cual en su DataContext tiene el valor del la propiedad CurrentItem(nuevamente asignado automaticamente por el ContentPresenter).
Actualizando propiedades del Control desde el DataTemplate
Además de Visualizar la lista de elementos, El ListTemplate debe establecer en la propiedad CurrentItem cual es el elemento seleccionado en la lista. Si en vez de trabajar con DataTemplate usáramos ControlTemplate podríamos usar TemplateBinding para hacer esto de forma sencilla pero TemplateBinding solo funciona en ControlTemplate por lo que debemos usar Binding + Relative Source/Ancestor en modo TwoWay como se mostro en la línea 5.
SelectedItem="{Binding CurrentItem,Mode=TwoWay,RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type listeditors:ListEditor}}}"
Esto causa que cuando se cambie el elemento seleccionado en el ListView se actualice la propiedad CurrentItem.
Nota: Inicialmente antes de estar consiente del uso tan practico del ContentPresenter yo solía usar propiedades de tipo ControlTemplate. Esta practica no tiene ningún sentido ya que aunque puedo cargar los elementos visuales del ControlTemplate usando su metodo LoadContent() el TemplatedParent de los controles internos siempre estaba en null(Nothing en Visual Basic) ya que solo los Controles deberian usar ControlTemplate directamente.
Conclusión
ContentPresenter es una excelente solución para desarrollar controles multi-contenido. Otros controles que hacen uso de este son los siguientes: