[Windows 8] GridView

Hola a todos!

Hoy quiero que hablemos de uno de los nuevos controles que tenemos disponibles en Windows 8. Estamos hablando del control GridView, que es responsable de gran parte del look & feel de las aplicaciones metro:

image

Tanto es así que incluso la tienda oficial de Windows 8 se basa en un GridView para presentarnos la información. Como podemos observar en la imagen superior, tenemos grupos de datos (spotlight, games,…) que contienen elementos. Además todo se organiza con un scroll horizontal en vez del típico scroll vertical al que estamos acostumbrados cuando los datos no entran en la pantalla. La estructura de este elemento es algo más compleja que un simple ListBox:

image

Como podemos observar, en el GridView la información se organiza en grupos que contienen una cabecera y elementos. Lo mejor de esto es que podemos definir el aspecto de la cabecera y el aspecto de los elementos, de hecho, según podemos ver en la imagen anterior, incluso podemos definir distintos aspectos para elementos dentro de un mismo grupo. Esto nos exige que los datos que enviemos a nuestra GridView estén agrupados. Vamos a definir unos datos de ejemplo, primero una clase que contendrá cada item y otra que creará los grupos:

  1:     public class ExampleData
  2:     {
  3:         public ExampleData()
  4:         {
  5:         }
  6: 
  7:         public string Title { get; set; }
  8: 
  9:         public string Image { get; set; }
 10: 
 11:         public string Description { get; set; }
 12:     }
 13: 
 14:     public class DataGroup
 15:     {
 16:         public DataGroup()
 17: 	    {
 18: 	    }
 19: 
 20:         public string GroupTitle { get; set; }
 21: 
 22:         public ObservableCollection<ExampleData> Items { get; set; }
 23:     }

Como podemos ver, ExampleData contiene un título, una descripción y una imagen, mientras que DataGroup nos ofrece un título para el grupo (que usaremos en la cabecera del mismo) y una colección de ExampleData.

Ahora en nuestra ViewModel vamos a crear datos de ejemplo:

  1:             Data = new ObservableCollection<DataGroup>()
  2:             {
  3:                 new DataGroup()
  4:                 {
  5:                     GroupTitle = "SciFi Movies",
  6:                     Items = new ObservableCollection<ExampleData>()
  7:                     {
  8:                         new ExampleData() 
  9:                         {
 10:                             Title = "Blade Runner",
 11:                             Description = "Deckard, a blade runner, ...",
 12:                             Image = "http://pics.filmaffinity.com/Blade_Runner-351607743-large.jpg"
 13:                         },
 14:                         new ExampleData()
 15:                         {
 16:                             Title = "Terminator 2",
 17:                             Description = "The cyborg who once tried to kill ...",
 18:                             Image = "http://pics.filmaffinity.com/Terminator_2_el_juicio_final-825143697-large.jpg"
 19:                         },
 20:                         new ExampleData()
 21:                         {
 22:                             Title = "Judge Dreed",
 23:                             Description = "In a dystopian future, ...",
 24:                             Image = "http://pics.filmaffinity.com/Juez_Dredd-447589318-large.jpg"
 25:                         }
 26:                     }
 27:     
 28:                 },
 29:                 new DataGroup()
 30:                 {
 31:                     GroupTitle = "SciFi Books",
 32:                     Items = new ObservableCollection<ExampleData>()
 33:                     {
 34:                         new ExampleData() 
 35:                         {
 36:                             Title = "Foundation",
 37:                             Description = "The Foundation Series is ...",
 38:                             Image = "http://isaac-asimov.com/wp-content/uploads/2012/01/Isaac-Asimov_1951_Foundation.jpg"
 39:                         },
 40:                         new ExampleData()
 41:                         {
 42:                             Title = "Ender's Game",
 43:                             Description = "Set in Earth's future, ...",
 44:                             Image = "http://www.hung-truong.com/blog/wp-content/uploads/2009/11/ender.gif"
 45:                         }
 46:                     }
 47:                 }
 48:             };

Con estos datos tenemos dos grupos, uno para películas y otro para libros, ambos de ciencia ficción. Para que el GridView pueda usar estos grupos tenemos que usar como fuente de datos un CollectionViewSource que podemos definir en nuestra página XAML:

  1: <Grid.Resources>
  2:     <CollectionViewSource x:Name="groupedItemsViewSource" Source="{Binding Data}" 
  3:                             IsSourceGrouped="true" ItemsPath="Items" />
  4: </Grid.Resources>

Simplemente le asignamos un nombre, le indicamos el binding a la propiedad Data de nuestra ViewModel y le decimos que está agrupado y cual es la propiedad que contiene los items de cada grupo. Ahora ya solo tenemos que añadir a nuestra página un GridView e indicarle que obtenga los datos del CollectionViewSource que hemos llamado groupedItemsViewSource:

  1: <GridView ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}">
  2: </GridView>

Si ejecutamos la aplicación el resultado será bastante pobre, una lista de nuestros items sin división y sin ningún formato. Esto es así por que necesitamos indicarle como debe crear y presentar los grupos mediante tres propiedades:

ItemsPanel, que definirá el modo de colocar los grupos:

  1: <GridView.ItemsPanel>
  2:     <ItemsPanelTemplate>
  3:         <StackPanel Orientation="Horizontal"/>
  4:     </ItemsPanelTemplate>
  5: </GridView.ItemsPanel>

GroupStyle.HeaderTemplate, que definirá el estilo de la cabecera de cada grupo:

  1: <GroupStyle.HeaderTemplate>
  2:     <DataTemplate>
  3:         <Grid Margin="1,0,0,6">
  4:             <Button AutomationProperties.Name="Group Title"
  5:                     Content="{Binding GroupTitle}"
  6:                     Style="{StaticResource TextButtonStyle}"/>
  7:         </Grid>
  8:     </DataTemplate>
  9: </GroupStyle.HeaderTemplate>

Y por último, GroupStyle.Panel, que definirá el panel donde se colocarán los elementos del grupo:

  1: <GroupStyle.Panel>
  2:     <ItemsPanelTemplate>
  3:         <VariableSizedWrapGrid Orientation="Vertical" Margin="0,0,80,0"/>
  4:     </ItemsPanelTemplate>
  5: </GroupStyle.Panel>

Si una vez definidas estas propiedades ejecutamos nuestra aplicación ya podremos observar los grupos conteniendo sus respectivos elementos:

image

 

Algo de estilo…

Aunque todavía no se puede decir que sea demasiado espectacular, nos falta definir la propiedad ItemTemplate que indicará al GridView como mostrar cada elemento:

  1: <GridView.ItemTemplate>
  2:     <DataTemplate>
  3:         <Grid Width="200" Height="300">
  4:             <Rectangle Fill="White"></Rectangle>
  5:             <Image Source="{Binding Image}" Opacity=".5" Stretch="Fill"></Image>
  6:             <TextBlock Foreground="White" Text="{Binding Title}"
  7:                         VerticalAlignment="Bottom"></TextBlock>
  8:         </Grid>
  9:     </DataTemplate>
 10: </GridView.ItemTemplate>

Vamos a usar las propiedades Image y Title para mostrar información sobre nuestro elemento, el resultado final ahora:

image

Sin duda mucho mejor. Como hemos visto es muy fácil poder usar un elemento GridView para mostrar nuestros datos, pero… en el ejemplo de la Store, los elementos tenían diferentes aspectos, tamaño, etc… y en nuestro ejemplo son todos iguales. Esto podemos resolverlo usando una propiedad del GridView llamada ItemTemplateSelector la cual recibirá cada elemento y devolverá un DataTemplate que se usará para mostrarlo. lo primero que vamos a hacer es modificar nuestra clase ExampleData para que tenga una propiedad ItemType de tipo entero y que indicará el tipo 1 para las películas y el tipo 2 para los libros.

Ahora vamos a crear dos DataTemplate diferentes para nuestros elementos, una para películas y otra para libros:

  1: <DataTemplate x:Key="MoviesTemplate">
  2:     <Grid Width="300" Height="250" Background="#33CCCCCC">
  3:         <Grid.ColumnDefinitions>
  4:             <ColumnDefinition Width="160"></ColumnDefinition>
  5:             <ColumnDefinition></ColumnDefinition>
  6:         </Grid.ColumnDefinitions>
  7:                     
  8:         <Image Grid.Column="0" Source="{Binding Image}" Stretch="Fill"></Image>
  9: 
 10:         <TextBlock Grid.Column="1" Margin="10,10,0,0" Foreground="White" Text="{Binding Title}"
 11:                         FontWeight="Bold" FontSize="20"
 12:                         TextWrapping="Wrap">
 13:         </TextBlock>
 14:         <TextBlock Grid.Column="1" Margin="10,80,0,0" Foreground="White" Text="{Binding Description}"
 15:                         FontWeight="Light" FontSize="18"
 16:                         TextWrapping="Wrap" TextTrimming="WordEllipsis">
 17:         </TextBlock>
 18:     </Grid>
 19: </DataTemplate>
 20:             
 21: <DataTemplate x:Key="BooksTemplate">
 22:     <Grid Width="600" Height="180" Background="#CCFFD073">
 23:         <Grid.ColumnDefinitions>
 24:             <ColumnDefinition Width="100"></ColumnDefinition>
 25:             <ColumnDefinition></ColumnDefinition>
 26:         </Grid.ColumnDefinitions>
 27: 
 28:         <Image Grid.Column="0" Source="{Binding Image}" Stretch="Fill"></Image>
 29:                     
 30:         <TextBlock Grid.Column="1" Margin="10,10,0,0" Foreground="White" Text="{Binding Title}"
 31:                         FontWeight="Bold" FontSize="20"
 32:                         TextWrapping="Wrap">
 33:         </TextBlock>
 34:         <TextBlock Grid.Column="1" Margin="10,45,0,0" Foreground="White" Text="{Binding Description}"
 35:                         FontWeight="Light" FontSize="18"
 36:                         TextWrapping="Wrap" TextTrimming="WordEllipsis">
 37:         </TextBlock>
 38:     </Grid>
 39: </DataTemplate>

En la primera DataTemplate, MoviesTemplate, vamos a darle más importancia a la imagen, dejando menos espacio para texto. En la segunda DataTemplate, BooksTemplate, vamos a mostrar más información del texto del libro. Para decidir que DataTemplate asignar a cada elemento, vamos a crear una clase que herede de la base DataTemplateSelector del namespace WIndows.UI.Xaml.Controls y sobre escribimos el método SelectTemplateCore:

  1: public class TemplateSelector : DataTemplateSelector
  2: {
  3:     public DataTemplate MovieTemplate { get; set; }
  4:     public DataTemplate BookTemplate { get; set; }
  5: 
  6:     protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
  7:     {
  8:         if (item == null)
  9:             return null;
 10: 
 11:         ExampleData element = (ExampleData)item;
 12: 
 13:         if (element.ItemType == 1)
 14:             return MovieTemplate;
 15:         else
 16:             return BookTemplate;
 17:     }
 18: }

Lo primero que hacemos en esta clase es crear dos propiedades públicas de tipo DataTemplate en las que podemos indicar las DataTemplates que hemos creado previamente, luego en el método SelectTemplateCore recibimos el elemento que necesita una plantilla y el contenedor de ese elemento, es en este método en el que podemos decidir que plantilla usar.

¿El resultado? A continuación…

image

Mucho mejor así, cada tipo de elemento tiene su representación única, que podemos ajustar para resaltar las propiedades más importantes.

Respondiendo al usuario

Ahora que ya sabemos como trabajar con el estilo de nuestros elementos, es hora de ver como recibir órdenes desde los elementos. Tenemos que tener en cuenta dos situaciones: que un usuario haga un “Tap” (un click) en un elemento o que lo seleccione (con un click derecho o con el gesto de pulsar y arrastrar que usamos en el menú inicio). Es importante tener en cuenta que debemos manejar los eventos tanto con el ratón como con entrada táctil.

En este aspecto hay mucha controversia por internet en los foros sobre WinRT. El elemento GridView (y el ListView también) tiene algunos comportamientos extraños entre el evento click, la propiedad SelectedItem y el evento tapped/righttapped. Después de buscar y buscar, la solución pasa por un poco de code behind.

En primer lugar podemos indicar la propiedad SelectedItem del GridView para obtener el elemento seleccionado por el usuario haciendo uso del gesto de seleccionar o del click derecho del ratón:

  1: <GridView x:Name="grdView" ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}" SelectionMode="Single"
  2:             ItemTemplateSelector="{StaticResource GridViewTemplateSelector}"
  3:             SelectedItem="{Binding SelectedData, Mode=TwoWay}"
  4:             Margin="120,140,20,20">

De esta forma si probamos nuestra aplicación veremos que, tanto si hacemos click como click derecho, se selecciona un elemento y llega el mismo a nuestra propiedad SelectedData. Pero nosotros ahora queremos que al hacer click sobre un elemento no se seleccione, queremos que nos permita ejecutar, por ejemplo la navegación a los detalles de ese elemento. Aquí entra en funcionamiento el code behind, debemos manejar el evento Tapped de la grid del elemento (DataTemplate), ejecutar el código que deseemos y marcar el evento como Handled, de esta forma no se seguirá propagando y no se seleccionará ese mismo elemento:

  1: private async void element_tapped(object sender, TappedRoutedEventArgs e)
  2: {
  3:     e.Handled = true;
  4: 
  5:     MessageDialog dlg = new MessageDialog("Clicked!");
  6:     await dlg.ShowAsync();
  7: }

De esta forma al hacer Tap, tanto con ratón como con interface táctil obtendremos este mensaje y no se seleccionará el elemento:

image

Podemos evitar el uso de Code behind en cierta medida implementando un EventToCommand. En Silverlight o WPF era realmente sencillo mediante el uso de Behaviors y Triggers, pero en WinRT nada de esto está disponible. Esto hace que la implementación de EventToCommand sea algo más complicada, pero podemos encontrar un ejemplo muy bueno en el blog de Joost van Schaik, NET By example, que nos permitirá enlazar a un comando de nuestra ViewModel y pasar el elemento pulsado como parámetro del comando. Aunque tendremos que modificarlo para que controle que el evento que llegue sea un evento Tapped y lo marque como manejado.

Conclusión

El elemento GridView es la piedra central sobre la que podremos construir nuestras aplicaciones metro, es importante implementarlo y obtener todo su potencial, espero que este artículo os sirva para ello, aunque todavía quedan cosas muy potentes como es el zoom semántico que no hemos visto y que está íntimamente relacionado con el GridView. Como siempre, aquí tenéis un ejemplo de todo lo que hemos visto para que podáis jugar con el y tenerlo como base y referencia para vuestros desarrollos.

Un saludo y Happy Coding!

Published 28/7/2012 11:35 por Josué Yeray Julián Ferreiro
Comparte este post:

Comentarios

# re: [Windows 8] GridView

Monday, July 30, 2012 9:44 AM por Javier Torrecilla

Genial Artículo, como siempre.

Crack!

# re: [Windows 8] GridView

Saturday, August 11, 2012 12:28 PM por Alberto789

Hola Yeray, lo primero felicitarte por esta serie de artículos tan interesantes.

Estoy trabajando con el control GridView, pero hay una cosa que no consigo hacer haber si me puedes orientar que es lo que estoy haciendo mal. Megustaria que uno de los "groups" del gridView tubiera un template diferente para ello he creado una clase que hereda de GroupStyleSelector con un funcionamiento similar a tu template selector, pero me cambia el template de todos los groups no consigo que solo me lo cambie en uno.

Un saludo y gracias de antemano