WinRT. DataTemplateSelectors en aplicaciones Windows Store.

Introducción

Uno de los controles más utilizados para construir la interfaz de las aplicaciones Windows Store sin duda es el GridView. Su enorme potencia para gestionar datos asi como su versatilidad y facilidad de uso lo convierten en candidato perfecto para muchas aplicaciones. En ocasiones, nuestra aplicación requerirá que se agrupen los datos por distintos grupos lógicos. Para diferenciarlos mejor, ¿podríamos aplicar a cada elemento de cada grupo un aspecto visual diferente?

En esta tarea nos vamos a concentrar en la entrada actual y lo haremos gracias al uso de DataTemplateSelector. Vamos a crear una aplicación que muestre un listado de películas y series. Lo haremos utilizando un control GridView, dividiremos las películas y series en grupos y aplicaremos un aspecto visual distinto a cada elemento de cada grupo.

NOTA: El uso de DataTemplateSelector no esta reducido al uso exclusivamente con el control GridView. Lo podemos utilizar con cualquier control listado (FlipView, ListView, etc). Sencillamente por fines didácticos nosotros nos centraremos en su uso con el GridView.

Manos a la obra

Como siempre solemos hacer vamos a realizar un ejemplo lo más simple posible pero que nos sea válida para lograr nuestros objetivos. La plantilla selecciona para realizar el ejemplo lo más simple posible será “Blank Application”:

Lo primero que debemos hacer es definir el modelo de datos con el que vamos a trabajar. Creamos una carpeta «Models». Dentro creamos la clase que definirá nuestro modelo «Video.cs»:
    public enum VideoType
    {
        Film,
        Serie
    }

    public class Video
    {
        public string Title { get; set; }
        public string Image { get; set; }
        public VideoType Type { get; set; }
    }

Simple. Título e imágen de la película o serie y con una enumeración logramos diferenciar el tipo. Tras crear el modelo vamos a crear el view model que abastecerá nuestra interfaz. Creamos una carpeta «ViewModels». Añadimos una clase «VideoViewmodel.cs»:

public class VideoViewModel
{
     #region Privates

     public List<VideoByType> Items { get; set; }

     #endregion

     #region Constructor

     public VideoViewModel()
     {
          var films = new List<Video>
          {
               new Video {Title = "Cadena perpetua", Image = "http://pics.filmaffinity.com/Cadena_perpetua-576140557-large.jpg", Type = VideoType.Film},

               new Video {Title = "El padrino", Image = "http://pics.filmaffinity.com/El_Padrino-485345341-large.jpg", Type = VideoType.Film},

               new Video {Title = "El padrino. Parte II", Image = "http://pics.filmaffinity.com/El_Padrino_Parte_II-807355469-large.jpg", Type = VideoType.Film},

               new Video {Title = "Pulp Fiction", Image = "http://pics.filmaffinity.com/Pulp_Fiction-586496431-large.jpg", Type = VideoType.Film},

               new Video {Title = "El bueno, el feo y el malo", Image = "http://pics.filmaffinity.com/El_bueno_el_feo_y_el_malo-589330734-large.jpg", Type = VideoType.Film},

               new Video {Title = "12 hombres sin piedad", Image = "http://pics.filmaffinity.com/12_hombres_sin_piedad_Doce_hombres_sin_piedad-290572645-large.jpg", Type = VideoType.Film},

               new Video {Title = "La lista de Schindler", Image = "http://pics.filmaffinity.com/La_lista_de_Schindler-803188900-large.jpg", Type = VideoType.Film},

               new Video {Title = "El caballero oscuro", Image = "http://pics.filmaffinity.com/El_caballero_oscuro-102763119-large.jpg", Type = VideoType.Film},

               new Video {Title = "El señor de los anillos: El retorno del rey", Image = "http://pics.filmaffinity.com/National_Geographic_Beyond_the_Movie_El_Senor_de_los_Anillos_El_Retorno_del_Rey_TV-916880961-large.jpg", Type = VideoType.Film},

               new Video {Title = "Dexter", Image = "http://www.zonadvd.com/imagenes/noticias/2008_10_paramount/dexter_frontal.jpg", Type = VideoType.Serie},

               new Video {Title = "Perdidos", Image = "http://www.zonadvd.com/imagenes/noticias/2005_12_bv/perdidos1_dvd.jpg", Type = VideoType.Serie},

               new Video {Title = "Prision Break", Image = "http://www.zonadvd.com/imagenes/noticias/2007_01_fox/prisonbreak1_dvd.jpg", Type = VideoType.Serie},

               new Video {Title = "Spartacus", Image = "http://www.zonadvd.com/imagenes/noticias/2011_10_fox/spartacus1_dvd.jpg", Type = VideoType.Serie},
          };

          var videosByType = films.GroupBy(f => f.Type).Select(f => new VideoByType { Type = f.Key, Videos = f.ToList() });

          Items = videosByType.ToList();
     }

     #endregion
}

Para simplificar el ejemplo creamos una colección en memoria de objetos Video. Agrupamos el listado por tipo (utilizando LINQ para ello) y el resultado se lo asignamos a una colección pública a la que haremos binding desde la interfaz.

Nos centramos en el code behind de la vista (MainPage.xaml.cs). Vamos a vincular la vista con su correspondiente view model. Añadimos el namespace correspondiente:

using Ejemplo_DataTemplateSelector.ViewModels;

Asignamos el DataContext al view model:

DataContext = new VideoViewModel();

Bien, ya esta casi todo preparado excepto la interfaz.

<Page.Resources>
</Page.Resources>

Añadimos un CollectionViewSource que hará binding a la propiedad Items del view model:

<CollectionViewSource
     x:Name="groupedVideoViewSource"
     Source="{Binding Items}"
     IsSourceGrouped="true"
     ItemsPath="Videos"/>

Continuamos. Deseamos representar instancias de la clase Video visualmente. La clase por defecto no tiene ninguna representación visual. El resultado de vincular la lista de instancias a un control lista sería el de llamar al método ToString de la instancia. Para mejorar el resultado y conseguir un buen aspecto visual vamos a definir un DataTemplate que se aplicará a cada elemento del GridView:

<DataTemplate x:Key="VideoItemTemplate">
     <Grid HorizontalAlignment="Left" Width="175" Height="250">
          <Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}">
               <Image Source="{Binding Image}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/>
          </Border>
          <StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
               <TextBlock Text="{Binding Title}" Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}" Style="{StaticResource TitleTextStyle}" Height="60" Margin="15,0,15,0"/>
          </StackPanel>
     </Grid>
</DataTemplate>

Definimos el GridView (como detalle, resaltar que la propiedad ItemTemplate apunta al DataTemplate creado previamente en los recursos de la página):

<GridView
     Grid.Row="1"
     Margin="116,0,40,46"
     ItemsSource="{Binding Source={StaticResource groupedVideoViewSource}}"
     ItemTemplate="{StaticResource VideoItemTemplate}"
     VerticalAlignment="Center">
     <GridView.ItemsPanel>
          <ItemsPanelTemplate>
               <VirtualizingStackPanel Orientation="Horizontal"/>
          </ItemsPanelTemplate>
     </GridView.ItemsPanel>
     <GridView.GroupStyle>
          <GroupStyle>
               <GroupStyle.HeaderTemplate>
                    <DataTemplate>
                        <Grid Margin="1,0,0,6">
                             <Button
                             Content="{Binding Type}"
                             Style="{StaticResource TextButtonStyle}"/>
                        </Grid>
                    </DataTemplate>
               </GroupStyle.HeaderTemplate>
               <GroupStyle.Panel>
                    <ItemsPanelTemplate>
                         <VariableSizedWrapGrid Orientation="Vertical" Margin="0,0,80,0"/>
                    </ItemsPanelTemplate>
               </GroupStyle.Panel>
          </GroupStyle>
     </GridView.GroupStyle>
</GridView>

Sin usar DataTemplateSelectors

El resultado tras ejecutar la aplicación lo podéis ver en la captura superior. Como podéis observar tanto películas como series se representan visualmente exactamente igual.

DataTemplateSelectors

Nuestro objetivo es que la apariencia visual de las películas sea diferente al de las series. Como adelantamos en la introducción de la entrada, lo conseguiremos utilizando DataTemplateSelectors.

Un DataTemplateSelector nos permite elegir entre distintos DataTemplate dependiendo de lógica situada en el code behind. Vamos a crear una clase que derivará de la clase DataTemplateSelector. Debemos añadir la referencia al siguiente namespace:

Windows.UI.Xaml.Controls

Lo más importante a realizar es sobreescribir el método SelectTemplateCore:

protected override Windows.UI.Xaml.DataTemplate SelectTemplateCore(object item, Windows.UI.Xaml.DependencyObject container)
{
     if (item != null)
     {
          var video = item as Video;

          if (video != null)
          {
               var currentFrame = Window.Current.Content as Frame;
               var currentPage = currentFrame.Content as Page;

               if (video.Type.Equals(VideoType.Film))
                    return currentPage.Resources["FilmItemTemplate"] as DataTemplate;
               else
                    return currentPage.Resources["SerieItemTemplate"] as DataTemplate;
          }
     }

     return base.SelectTemplateCore(item, container);
}

En el método SelectTemplateCore añadiremos la lógica necesaria para elegir un DataTemplate u otro. Recibimos como primer parámetro el objeto visual que deseamos representar. En nuestro ejemplo sencillamente hacemos un casting al tipo Video y verificamos el tipo (campo Type)  para identificar si el elemento es una película o una serie. Dependiendo del tipo devolveremos un DataTemplate u otro (situado en los recursos de la página).

NOTA: Tened en cuenta que el método SelectedTemplateCore se ejecutará una vez por cada elemento de la colección. Evitad realizar tareas de peso (llamadas a servicios web por ejemplo) en el método para no penalizar la interfaz de usuario.

A continuación, tras definir el DataTemplateSelector, debemos añadirlo como recurso en la página donde lo vamos a utilizar:

<local:VideoListTemplateSelector x:Key="videoListTemplateSelector"/>

El DataTemplate que utilizaremos para películas:

<DataTemplate x:Key="FilmItemTemplate">
     <Grid HorizontalAlignment="Left" Width="175" Height="250">
          <Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}">
               <Image Source="{Binding Image}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/>
          </Border>
          <StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
               <TextBlock Text="{Binding Title}" Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}" Style="{StaticResource TitleTextStyle}" Height="60" Margin="15,0,15,0"/>
          </StackPanel>
     </Grid>
</DataTemplate>

El DataTemplate utilizado en series:

<DataTemplate x:Key="SerieItemTemplate">
     <Grid HorizontalAlignment="Left" Width="250" Height="100">
          <Grid.ColumnDefinitions>
               <ColumnDefinition Width="100"/>
               <ColumnDefinition Width="*"/>
          </Grid.ColumnDefinitions>
          <Image Grid.Column="0" Source="{Binding Image}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/>
          <StackPanel Grid.Column="1" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
               <TextBlock Text="{Binding Title}" Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}"
               Style="{StaticResource TitleTextStyle}"  Height="100" Margin="15,5"/>
          </StackPanel>
     </Grid>
</DataTemplate>

Por último, en el GridView en lugar de utilizar la propiedad ItemTemplate y asignar un DataTemplate, vamos a utilizar la propiedad ItemTemplateSelector al que asignaremos un DataTemplateSelector encargado de ejecutar la lógica para elegir un DataTemplate u otro:

<GridView
     Grid.Row="1"
     Margin="116,0,40,46"
     ItemsSource="{Binding Source={StaticResource groupedVideoViewSource}}"
     ItemTemplateSelector="{StaticResource videoListTemplateSelector}"
     VerticalAlignment="Center">
     <GridView.ItemsPanel>
          <ItemsPanelTemplate>
               <VirtualizingStackPanel Orientation="Horizontal"/>
          </ItemsPanelTemplate>
     </GridView.ItemsPanel>
     <GridView.GroupStyle>
          <GroupStyle>
               <GroupStyle.HeaderTemplate>
                    <DataTemplate>
                        <Grid Margin="1,0,0,6">
                             <Button
                             Content="{Binding Type}"
                             Style="{StaticResource TextButtonStyle}"/>
                        </Grid>
                    </DataTemplate>
               </GroupStyle.HeaderTemplate>
               <GroupStyle.Panel>
                    <ItemsPanelTemplate>
                         <VariableSizedWrapGrid Orientation="Vertical" Margin="0,0,80,0"/>
                    </ItemsPanelTemplate>
               </GroupStyle.Panel>
          </GroupStyle>
     </GridView.GroupStyle>
</GridView>

Utilizando DataTemplateSelectors

Como podemos observar en la captura superior ahora a cada grupo le asignamos un DataTemplate diferente y logramos que cada elemento de cada grupo tenga una apariencia distinta.

Puedes descargar el ejemplo realizado:

Recordar que cualquier tipo de duda o sugerencia la podéis dejar en los comentarios de la entrada.

Más información

2 pensamientos en “WinRT. DataTemplateSelectors en aplicaciones Windows Store.

  1. Muy buen post tocayo!
    Hace un tiempo publique esto: http://geeks.ms/blogs/jtorrecilla/archive/2012/04/26/winrt-establecer-un-estilo-condicional-2-metro-win8.aspx que era para estilos condicionales, y @Pablonete me planteo la opción de utilizar propiedades para definir los elementos que vas a utilizar como Templates, de está manera podrias hacer binding a recursos dentro del template y el código quedaría un poco más limpio

    • Buenas crack!

      Tras leer tu artículo y los comentarios (todo muy interesante) coincido con @plablonete. Es más limpio y reutilizable su propuesta (e incluso algo menos propensa a errores). Sería algo asi:


      public class ItemsTemplateSelector : DataTemplateSelector
      {
      public DataTemplate TemplateA { get; set; }
      public DataTemplate TemplateB { get; set; }

      protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
      {
      if (item is A)
      return TemplateA;
      else if (item is B)
      return TemplateB;
      else
      return base.SelectTemplate(item, container);
      }
      }

      Gracias por la puntualización. Un placer recibir comentarios tuyos 😉

Deja un comentario