[WPF] Plantillas de datos dinámicas.

En muchas ocasiones podemos mostrar en una misma lista datos con diferentes significados o propiedades y podemos querer que sea fácil para el usuario distinguir entre los distintos tipos.

Por ejemplo, a la hora de mostrar una lista de los empleados de una empresa, tenemos diferentes puestos y habilidades, sería ideal que de un simple vistazo pudiésemos localizar los empleados que cumplen con lo que necesitamos para poder abrir su ficha y asignarles un trabajo.

En WPF podemos realizar esto de una forma bastante sencilla usando el interface IValueConverter y las clases DataTemplate y DataTemplateSelector.

IValueConverter

En muchas ocasiones el tipo de dato guardado en nuestra base de datos no es exactamente el mismo que deseamos presentar al cliente. Por ejemplo guardamos un código de país en nuestros datos pero queremos mostrar la bandera del país, no su código. También puede darse el caso de que queramos presentar sutiles diferencias entre diferentes valores de un campo, por ejemplo cambiando el color de letra o resaltando en negrita ante ciertos valores.

Esto, que se podría decir es una lógica de presentación propia de nuestros datos, podemos realizarlo de forma automática y muy sencilla con el interface IValueConverter, en tres pasos: Crear una clase que implemente el interface IValueConverter , referenciar esta clase como un recurso en nuestro XAML y usar la opción Converter en el binding de nuestro dato:

1º Crear una clase que implemente IValueConverter, debemos crear dos métodos Convert y ConvertBack:

class BoolToColorConverter : System.Windows.Data.IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, 
                            System.Globalization.CultureInfo culture)
    {
        bool Valor = (bool)value;

        if (Valor == true)
        {
            return new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Green);
        }
        else
        {
            return new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Red);
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, 
                                System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

 

En este ejemplo podemos ver un conversor que pasa un valor boolean a una interpretación en color (verde para verdadero, rojo para falso) con lo que conseguimos una interpretación visual más sencilla para el usuario. En la estructura de la clase que implementa el interface IValueConverter, los parámetros de ambos métodos son los mismos aunque su significado varía:

  • Convert:
    • Value: es un objeto con el valor de la propiedad original que queremos convertir.
    • targetType: es el tipo de la propiedad que queremos devolver.
    • parameter: un parámetro adicional que podemos enviar al método para la conversión.
    • culture: la cultura actual de la aplicación, para conversiones que puedan depender de ella.
  • ConvertBack:
    • Value: es un objeto con el valor de la propiedad que hemos convertido.
    • targetType: es el tipo de nuestra propiedad original.
    • parameter: un parámetro adicional que podemos enviar al método para la conversión.
    • culture: la cultura actual de la aplicación, para conversiones que puedan depender de ella.

2º Referenciar esta clase como un recurso en nuestro XAML:

Una vez creada nuestra clase deberemos referenciarla en XAML como un recurso para poder utilizarla.

Si no lo hemos hecho ya, primero deberemos referenciar nuestro namespace en el XAML:

xmlns:converters="clr-namespace:DynamicTemplates"

A continuación creamos un recurso con nuestro conversor:

<Window.Resources>
    <converters:BoolToColorConverter x:Key="ColorConverter"></converters:BoolToColorConverter>
</Window.Resources>  

Le asignamos una Key como a cualquier otro recurso para poder acceder más tarde a él.

3º Usamos nuestro conversor en la propiedad que queramos afectar.

Por ejemplo el Foreground de un TextBlock enlazado a una propiedad Disponible de nuestras clases de datos:

<TextBlock Name="txtDisponibilidad" 
            Foreground="{Binding Path=Disponibilidad,
                                Converter={StaticResource ColorConverter}}" 
            Text="{Binding Path=UserName}" Margin="0,0,192,285">
</TextBlock>

Con esto, cuando la propiedad Disponibilidad tenga el valor True, el Foreground será verde, mientras que cuando tenga el valor False será rojo:

imageimage

DataTemplate

Ahora que ya sabemos como modificar valores para presentarlos de formas diferentes a como se representarían de forma normal, vamos a ver como modificar el aspecto general de nuestros datos en un control de lista, por ejemplo, un ListBox o un ComboBox, creando una plantilla visual específica que muestre nuestros datos al usuario de forma más atractiva y más sencilla para su entendimiento.

Esta operación se realiza en dos pasos: Definir nuestro DataTemplate y asignarlo al control que queramos que lo use.

1º Definir nuestro DataTemplate

En XAML debemos especificar un DataTemplate como recurso, y dentro de este, crear el árbol visual que compondrá a los Items de nuestra lista. De esta forma una vez que especifiquemos al control de lista el DataTemplate que queremos que use, se encargará de formatear nuestros datos visualmente como le hayamos indicado. Vamos a Crear una plantilla sencilla, que muestre los datos de un empleado usando un estilo de tarjeta de visita:

<DataTemplate x:Key="MiTemplate">
    <Grid Height="74" Width="250" HorizontalAlignment="Stretch" Margin="5">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="64"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height=".3*"></RowDefinition>
            <RowDefinition Height=".3*"></RowDefinition>
            <RowDefinition Height=".3*"></RowDefinition>
        </Grid.RowDefinitions>
        <Border Grid.ColumnSpan="2" Grid.RowSpan="3" BorderBrush="Black" BorderThickness="2" CornerRadius="5">
            <Border.Background>
                <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                    <GradientStop Offset="0" Color="White"></GradientStop>
                    <GradientStop Offset="1" Color="LightGray"></GradientStop>
                </LinearGradientBrush>
            </Border.Background>
        </Border>
        <Image Name="imgUserPhoto" Grid.Column="0" Grid.RowSpan="3" Margin="10" Source="{Binding Path=UserPhotoFile}"></Image>
            
        <TextBlock Name="tblUserName" Grid.Column="1" Grid.Row="0" 
                    FontSize="14" FontWeight="Bold"
                    HorizontalAlignment="Left" VerticalAlignment="Center"
                    Text="{Binding Path=UserName}">
        </TextBlock>
        <TextBlock Name="tblUserFullName" Grid.Column="1" Grid.Row="1" 
                    HorizontalAlignment="Left" VerticalAlignment="Center"
                    Text="{Binding Path=UserFullName}">
        </TextBlock>
        <TextBlock Name="tblUserArea" Grid.Column="1" Grid.Row="2" 
                    HorizontalAlignment="Left" VerticalAlignment="Center"
                    Text="{Binding Path=UserArea}">
        </TextBlock>
    </Grid>
</DataTemplate>

Dentro de la propia DataTemplate hemos incluido los Bindings a las propiedades para cada control, al insertar la plantilla en cada Item, el DataContext que usará será el de la ventana que lo contenga.

2º Asignarlo al control

Una vez que hemos creado nuestra DataTemplate y le hemos asignado un nombre (x:Key) simplemente tenemos que especificar la propiedad ItemTemplate en el control que queremos que use nuestra plantilla, apuntando hacia el nombre de la misma:

<ListBox Name="lstUsers" Margin="0,10,0,10" 
            HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
            ItemTemplate="{StaticResource MiTemplate}"
</ListBox>  

Como puedes ver simplemente hemos definido un Layout de elementos, usando una grid, un Border, un Image y varios Textblocks para mostrar los datos, en vez de uno al lado del otro como se hacia tradicionalmente, en forma de ficha o tarjeta, y después hemos indicado a nuestro ListBox que use este Layout, por medio de la propiedad ItemTemplate. El resultado al enlazar con datos sería este:

image

Como puedes ver, es mucho más atractivo que simplemente mostrar la información del usuario en una línea solo con texto.

DataTemplateSelector

Bien, ya hemos visto como usar el interface IValueConverter para cambiar la apariencia visual de nuestros datos y como usar la clase DataTemplate para personalizar el árbol visual de nuestros controles de listas.

Pero, que pasa si, siguiendo el ejemplo anterior de la lista de empleados, tenemos diferentes perfiles de empleados, con diferentes habilidades y queremos que sea más sencillo distinguir entre tipos de empleados y entre habilidades de los mismos? Bueno podríamos usar clases con el interface IValueConverter para mostrar, ocultar datos o modificar colores… Pero eso nos exigiría tener varias clases para poder personalizar la plantilla. ¿Que tal si pudiésemos tener varias DataTemplate independientes y la lista fuese capaz, en base a los datos de cada Item, elegir cual es la que debe aplicar? Eso podemos realizarlo con la clase DataTemplateSelector en tres sencillos pasos: crear las plantillas que queramos usar, crear una clase que herede de DataTemplateSelector y establecer la propiedad ItemTemplateSelector de nuestro control de lista.

1º Crear las plantillas que queramos usar

En nuestro ejemplo anterior hemos creado una plantilla para mostrar datos de usuarios, ahora vamos a crear dos más, una específica para desarrolladores y otra para diseñadores, dejando la que hemos creado anteriormente para directores.

<DataTemplate x:Key="TemplateDevelopers">
    <Grid Height="74" Width="250" HorizontalAlignment="Stretch" Margin="5">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="64"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height=".25*"></RowDefinition>
            <RowDefinition Height=".25*"></RowDefinition>
            <RowDefinition Height=".25*"></RowDefinition>
            <RowDefinition Height=".25*"></RowDefinition>                
        </Grid.RowDefinitions>
        <Border Grid.ColumnSpan="2" Grid.RowSpan="4" BorderBrush="DarkBlue" BorderThickness="2"  CornerRadius="5">
            <Border.Background>
                <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                    <GradientStop Offset="0" Color="Azure"></GradientStop>
                    <GradientStop Offset="1" Color="LightBlue"></GradientStop>
                </LinearGradientBrush>
            </Border.Background>
        </Border>
        <Image Name="imgUserPhoto" Grid.Column="0" Grid.RowSpan="4" Margin="10" Source="{Binding Path=UserPhotoFile}"></Image>

        <TextBlock Name="tblUserName" Grid.Column="1" Grid.Row="0" 
                    FontSize="14" FontWeight="Bold"
                    HorizontalAlignment="Left" VerticalAlignment="Center"
                    Text="{Binding Path=UserName}">
        </TextBlock>
        <TextBlock Name="tblUserFullName" Grid.Column="1" Grid.Row="1" 
                    HorizontalAlignment="Left" VerticalAlignment="Center"
                    Text="{Binding Path=UserFullName}">
        </TextBlock>
        <TextBlock Name="tblUserArea" Grid.Column="1" Grid.Row="2" 
                    HorizontalAlignment="Left" VerticalAlignment="Center"
                    Text="{Binding Path=UserArea}">
        </TextBlock>
        <TextBlock Name="tblDevLanguaje" Grid.Column="1" Grid.Row="3" 
                    HorizontalAlignment="Left" VerticalAlignment="Center"
                    Text="{Binding Path=DevLang}" FontWeight="Bold"
                    Foreground="{Binding Path=DevLang,Converter={StaticResource DevConv}}">
        </TextBlock>
    </Grid>
</DataTemplate>

<DataTemplate x:Key="TemplateDesigners">
    <Grid Height="74" Width="250" HorizontalAlignment="Stretch" Margin="5">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="64"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height=".25*"></RowDefinition>
            <RowDefinition Height=".25*"></RowDefinition>
            <RowDefinition Height=".25*"></RowDefinition>
            <RowDefinition Height=".25*"></RowDefinition>
        </Grid.RowDefinitions>
        <Border Grid.ColumnSpan="2" Grid.RowSpan="4" BorderBrush="LimeGreen" BorderThickness="2"  CornerRadius="5">
            <Border.Background>
                <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                    <GradientStop Offset="0" Color="LightYellow"></GradientStop>
                    <GradientStop Offset="1" Color="LightGreen"></GradientStop>
                </LinearGradientBrush>
            </Border.Background>
        </Border>
        <Image Name="imgUserPhoto" Grid.Column="0" Grid.RowSpan="4" Margin="10" Source="{Binding Path=UserPhotoFile}"></Image>

        <TextBlock Name="tblUserName" Grid.Column="1" Grid.Row="0" 
                    FontSize="14" FontWeight="Bold"
                    HorizontalAlignment="Left" VerticalAlignment="Center"
                    Text="{Binding Path=UserName}">
        </TextBlock>
        <TextBlock Name="tblUserFullName" Grid.Column="1" Grid.Row="1" 
                    HorizontalAlignment="Left" VerticalAlignment="Center"
                    Text="{Binding Path=UserFullName}">
        </TextBlock>
        <TextBlock Name="tblUserArea" Grid.Column="1" Grid.Row="2" 
                    HorizontalAlignment="Left" VerticalAlignment="Center"
                    Text="{Binding Path=UserArea}">
        </TextBlock>
        <TextBlock Name="tblDesignEnvironment" Grid.Column="1" Grid.Row="3" 
                    HorizontalAlignment="Left" VerticalAlignment="Center"
                    Text="{Binding Path=DesEnv}" FontWeight="Bold"
                    Foreground="{Binding Path=DesEnv,Converter={StaticResource DesConv}}">
        </TextBlock>
    </Grid>
</DataTemplate>

Estas plantillas son muy parecidas a la primera, hemos añadido una fila más de información con el lenguaje de desarrollo o software de diseño que mejor usa el empleado y en este texto hemos usado un converter para modificar el color del texto dependiendo del texto en cuestión.

2º Crear una clase que herede de DataTemplateSelector

La clase DataTemplateSelector se encuentra en el namespace System.Windows.Controls, tenemos que sobreescribir el método SelectTemplate, que toma dos parámetros item (el item actual de la lista) y container (el objeto en cuestión que contiene nuestro item), debemos devolver el DataTemplate a aplicar en este item en concreto:

class UsersTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        if (item != null && item is UserClass)
        {
            UserClass Usuario = item as UserClass;
            switch (Usuario.UserArea)
            {
                case "director":
                    {
                        return App.Current.Resources["TemplateDirectors"] as DataTemplate;
                    }
                case "developer":
                    {
                        return App.Current.Resources["TemplateDevelopers"] as DataTemplate;
                    }
                case "designer":
                    {
                        return App.Current.Resources["TemplateDesigners"] as DataTemplate;
                    }
                default:
                    {
                        return App.Current.Resources["TemplateDirectors"] as DataTemplate;
                    }
            }
        }

        return null;
    }

}

Es muy sencillo, simplemente convertimos el item a una instancia de nuestra clase UserClass que es la que está enlazada a la lista y usamos el campo UserArea para saber que plantilla debemos devolver, luego como nuestras plantillas están definidas en un ResourceDictionary añadido a nuestro app.xaml simplemente buscamos el recurso y lo convertimos a un DataTemplate para devolverlo.

3º Establecer la propiedad ItemTemplateSelector de nuestro control de lista

Una vez que tenemos nuestra clase creada, debemos referenciarla como un recurso de XAML, añadiendo primero el namespace donde se encuentra a la ventana o usercontrol:

xmlns:cls="clr-namespace:DynamicTemplates"

Y creando después un recurso con nuestra clase de selección de plantillas:

<Window.Resources>
    <cls:UsersTemplateSelector x:Key="TempSelector"></cls:UsersTemplateSelector>
</Window.Resources>

Una vez hecho esto, debemos eliminar la propiedad ItemTemplate de nuestro control de listas y sustituirla por la propiedad ItemTemplateSelector apuntando a nuestro nuevo recurso de selección de plantillas:

<ListBox Name="lstUsers" Margin="0,10,0,10" 
            HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
            ItemTemplateSelector="{StaticResource TempSelector}">
</ListBox>

Una vez terminado todo, si añadimos distintos tipos de usuarios a la colección y ejecutamos tendremos un resultado parecido a este:

image

Como podemos ver cada tipo de usuario ha usado una plantilla diferente y además usando las clases que implementan IValueConverter hemos cambiado el color del texto de algunas opciones dependiendo del valor del texto.

De esta manera es muy fácil saber cuantos diseñadores tenemos disponibles o ver que hay menos directores disponibles que el resto de tipos de empleados, sin tener que leer o buscar de forma más complicada y , por supuesto, nuestro interface de usuario es mucho más agradable que una simple lista de texto en líneas.

Conclusión

Espero que os haya parecido instructivo este pequeño artículo y que podáis dotar de mayor dinamismo a vuestras aplicaciones WPF a la hora de mostrar datos al usuario, la era de las listas de texto en filas interminables ha terminado, tenemos el poder de cambiar la forma en la que los usuarios consumen datos, y podemos hacer que sea más sencillo y agradable.

Os dejo el proyecto para descarga y como siempre, estoy a vuestra entera disposición para cualquier cosa que necesitéis, duda, crítica, sugerencia o simplemente tomar un café y conocernos!

Un saludo a todos, gracias por leerme y HAPPY CODING!

5 comentarios sobre “[WPF] Plantillas de datos dinámicas.”

  1. Hola esta opcion he estado buscado desde hace rato, la he implementado pero no me funciona, mi escenario es el siguiente:
    desde la vista principal de la aplicacion demiente un check hago el llamado a un usercontrol, este user control se llenara de formulas, hay es donde utilizo los template, ya que hay fomulas con siertas caracteristicas, el problema esque cuando se muestra no se sale la descripcion de la formula, sino que me sale es el nombre del ViewModel, algo como esto
    Mantiz40.presetatation.ViewModelS.FormulaViewModel.

    cual se mi problema. gracias

  2. Hola Wilson, en ese Hilo del foro te ha contestado David, lo que te propone es lo mismo que pienso yo, hazle caso y pegale un vistazo.

    Un saludo!

Responder a jyeray Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *