Cómo crear agrupamiento utilizando listas en WinRT | XAML | C#

Intermedio

Es un tema en esencia sencillo, tengo un conjunto de datos y quiero mostrarlos de manera agrupada por un criterio. Sin embargo para implementarlo hay que tener en cuenta varios tips adicionales, más allá de la documentación de msdn.

Los pasos a seguir son:

  • Crear el origen de datos agrupados
  • Instanciar el origen de datos desde el XAML
  • Modificar el template del control para que soporte grouping
  • Modificar el template para optimizar la visualización de los grupos

Crear el origen de datos agrupados

Los primero es que debes tener ya establecido un conjunto de datos, ¿necesitas datos de prueba?

Cómo generar datos aleatorios para realizar pruebas

Trabajaremos con una lista de personas como estas, tienen implementado de una vez INotifyPropertyChanged (BindableBase) para utilizarla al hacer Binding:

 
public class Persona : BindableBase
{
    private int _cedula;
    public int Cedula
    {
        get { return _cedula; }
        set
        {
            _cedula = value;
            SetProperty(ref _cedula, value);
        }
    }

    private string _nombre;
    public string Nombre
    {
        get { return _nombre; }
        set
        {
            _nombre = value;
            SetProperty(ref _nombre, value);
        }
    }

    private string _apellido;
    public string Apellido
    {
        get { return _apellido; }
        set { SetProperty(ref _apellido, value); }
    }

    private string _profesion;
    public string Profesion
    {
        get { return _profesion; }
        set { SetProperty(ref _profesion, value); }
    }
}

Ahora tenemos que pensar en el agrupamiento, agruparemos a las personas por profesión así que creamos un objeto capaz de guardar grupos de personas, utilizo ObservableCollection solo para tener la funcionalidad completa en el binding.

 
public class GrupoPersonas
{
    public string Profesion { get; set; }
    public List< Persona> ListaPersonas { get; set; }
}

Esta lista la podemos llenar tomando como base nuestra fuente de datos, extraeremos los datos utilizando linq y para todo ello crearemos una clase encargada de administrar dichos datos.

 
public class DataSourcePersonas
{
    public ObservableCollection ListaPersonas { get; set; }

    public DataSourcePersonas()
    {
        var listaFull = Traer‌InfoDesdeOrigenDeDatos();
        ListaPersonas = new ObservableCollection(listaFull);
    }

    public ObservableCollection ListaPersonasAgrupada { get; set; }
    public void Initialize()
    {
        var lista = from persona in ListaPersonas
                                    group persona by persona.Profesion into grupo
                                    select new GrupoPersonas()
                                    {
                                        Profesion = grupo.Key,
                                        ListaPersonas = grupo.ToList()
                                    };
        ListaPersonasAgrupada = new ObservableCollection(lista);
    }
}

Analicemos el código anterior, esta clase nos permite acceder a nuestro origen de datos normal, llamado ListaPersonas, y también nos permite acceder a nuestro origen de datos agrupado llamado ListaPersonasAgrupado, los datos de este último los obtenemos consultando ListaPersonas y creando los grupos a traves de linq.

Ambas listas las hemos creado como ObservableCollectionpara así sacar máximo provecho durante el binding.

Instanciar el origen de datos desde el XAML

Ahora desde XAML debemos instanciar nuestro origen de datos, pero no tan rápido vaquero!

Para que podamos utilizar el origen de datos para presentarlos de manera agrupada debemos hacerlo a través de un objeto CollectionViewSource, así que debemos instanciar realmente dos objetos desde XAML, primero DataSourcePersonas y luego un CollectionViewSource al cual debemos asignarle como fuente de datos agrupados a DataSourcePersonas:

 
<Page
    xmlns:data="using:DemoGrouping.Data">
    <Page.Resources>
        <data:DataSourcePersonas x:Key="DataSourcePersonas"/>
        <CollectionViewSource x:Key="CvsGruposPersonas" IsSourceGrouped="True"
                              Source="{Binding Source={StaticResource DataSourcePersonas},
                                               Path=ListaPersonasAgrupada}"
                              ItemsPath="ListaPersonas" />
    </Page.Resources>
</Page>

CollectionViewSource aparenta ser complejo, pero es muy sencillo de entender, básicamente permite generar una vista sobre un conjunto de datos para aplicarles ordenamientos, agrupamientos y filtros, el atributo IsSourceGrouped es utilizado para indicar que el CollectionViewSource pose un conjunto de datos agrupados, bien podría no serlo pero para este ejemplo como adivinaras esto es requerido.

Source es la propiedad que indica de donde se deben tomar los datos, en este caso los tomamos de un recurso creado dentro del XAML que no es más que DataSourcePersonas al cual por conveniencia le hemos puesto como key el mismo nombre del objeto; es de notar que DataSourcePersonas no es el directo origen de datos, los datos están en una de sus propiedades públicas llamada ListaPersonasAgrupada, así lo definimos cuando lo creamos (más arriba),por eso en el binding del Source del CollectionViewSource hacemos Binding con DataSourcePersonas pero le indicamos por la Propiedad Path cual es el objeto que debemos considerar como origen de datos, es decir la lista de grupos de personas.

Finalmente en el CollectionViewSource asignamos el atributo ItemsPath que? luego ya no habíamos asignado los ítems?

Si, pero los items que asignamos son los grupos de Personas y cada grupo dentro de si tiene otros atributos, si revisamos nuestro GrupoPersonas podemos darnos cuenta que hay dos atributos:

 
public class GrupoPersonas
{
    public string Profesion { get; set; }
    public List< Persona> ListaPersonas { get; set; }
}

El CollectionViewSource necesita saber cual de esos atributos contiene las personas de cada grupo de personas, eso es ItemsPath.

Modificar el template del control para que soporte grouping

Estamos listos para ahora solo preocuparnos por la UI.

Para ello debemos hacer Binding de la colección de datos sobre el contenedor de lista, si no conoces del tema o no lo has hecho antes talvez te convenga leer primero este artículo:

Cómo utilizar controles de lista para mostrar colecciones de datos en WinRT? – C# – XAML

Por facilidad les dejo acá todos los estilos que se utilizaran, son solo para ponerle algo de color y de orden, no afectan en nada más nuestra tarea.

 
<Style x:Key="apptile" TargetType="StackPanel">
    <Setter Property="Margin" Value="5"/>
    <Setter Property="Background">
        <Setter.Value>
            <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                <GradientStop Color="#FF2B3E3A"/>
                <GradientStop Color="#FF4E6863" Offset="1"/>
                <GradientStop Color="#FF2D574F" Offset="0.815"/>
            </LinearGradientBrush>
        </Setter.Value>
    </Setter>
    <Setter Property="Width" Value="200"/>
    <Setter Property="Height" Value="200"/>
</Style>
<Style TargetType="TextBlock" x:Key="PersonName">
    <Setter Property="FontSize" Value="35" />
    <Setter Property="TextWrapping" Value="Wrap"/>
</Style>
<Style TargetType="TextBlock" x:Key="PersonCedula">
    <Setter Property="FontSize" Value="25" />
    <Setter Property="HorizontalAlignment" Value="Right"/>
    <Setter Property="TextWrapping" Value="Wrap"/>
    <Setter Property="Foreground" Value="Gold"/>
    <Setter Property="Margin" Value="0,20,0,20"/>
</Style>

<Style TargetType="TextBlock" x:Key="PersonProfession">
    <Setter Property="FontSize" Value="35" />
    <Setter Property="TextWrapping" Value="Wrap"/>
    <Setter Property="Foreground" Value="YellowGreen"/>
</Style>

<Style TargetType="TextBlock" x:Key="Encabezado">
    <Setter Property="FontSize" Value="45" />
    <Setter Property="TextWrapping" Value="Wrap"/>
    <Setter Property="Foreground" Value="White"/>
</Style>


<Style TargetType="Grid">
    <Setter Property="Margin" Value="5,5,0,0"/>
</Style>

Creamos un GridView que sea capaz de mostrar una lista de personas normal sin pensar en grouping, asíque le hacemos Binding con DataSourcePersonas

 
<GridView ItemsSource="{Binding Source={StaticResource DataSourcePersonas},
                                Path=ListaPersonas}">
    <GridView.ItemTemplate>
        <DataTemplate>
            <StackPanel Style="{StaticResource apptile}">
                <TextBlock Style="{StaticResource PersonName}"       Text="{Binding Nombre}"/>
                <TextBlock Style="{StaticResource PersonName}"       Text="{Binding Apellido}"/>
                <TextBlock Style="{StaticResource PersonCedula}"     Text="{Binding Cedula}"/>
                <TextBlock Style="{StaticResource PersonProfession}" Text="{Binding Profesion}"/>
            </StackPanel>
        </DataTemplate>
    </GridView.ItemTemplate>
</GridView>

De esta forma los datos se visualizan así:

Gridview Template

Una vez esta lista la UI debemos cambiar el datasource, lo establecemos en CvsGruposPersonas, y debemos configurar el GridView para utilizar la información de grupos, para ello editamos el template de grupo para el encabezado y para panel de ítems.

 
<GridView.GroupStyle>
    <GroupStyle>
        <GroupStyle.HeaderTemplate>
            <DataTemplate>
                <TextBlock Style="{StaticResource Encabezado}" Text="{Binding Profesion}"/>
            </DataTemplate>
        </GroupStyle.HeaderTemplate>
        <GroupStyle.Panel>
            <ItemsPanelTemplate>
                <VariableSizedWrapGrid Width="auto" Background="Black"
                                        Margin="0,0,30,0"/>
            </ItemsPanelTemplate>
        </GroupStyle.Panel>
    </GroupStyle>
</GridView.GroupStyle>

Lo que hemos hecho acá es crear un envoltorio para cada grupo, ese envoltorio tiene un encabezado en el cual hemos colocado solo un titulo con la profesión, aprovechando esto podemos remover la profesión de nuestro GridView.

A parte del Header hay que establecer el Panel, como no sabemos el tamaño de cada grupo utilizamos un VariableSizedWrapGrid un contenedor que solo puede ser utilizado dentro de otro Control, este control será el que contenga a todos los items de cada grupo.

Con esto queda terminado!

pero no tan rápido vaquero (jeje, sorry)

Que pasa cuando hay grupos de un tamaño diferente, digamos que el segundo grupo tiene solo 3 items, ocurre un efecto indeseable. Dentro de un GridView TODOS los ítems utilizan el tamaño del primer ítem, por ende si nuestro primer grupo tiene 10 items y el segundo 3, el segundo conserva el ancho necesario para albergar 10 y no 3.

De igual forma si el primer grupo fuera de 3 y el segundo de 10, el segundo (y subsecuentes) grupo solo mostraría 3 items y no tendría espacio para mostrar más, ejemplo.

Grouping in GridView

Grouping in GridView

Modificar el template para optimizar la visualización de los grupos

Para solucionar este último problema podemos hacer uso de un control XAML muy importante VirtualizingStackPanel. Este control aparte de muchas otras cosas, hace que el mismo y sus objetos contenidos no puedan ser mas grandes o más chicos que los objetos que contengan, de tal forma que nos soluciona el problema antes mencionado, si sobra espacio lo recorta y falta lo expande.

Pero dónde lo utilizamos?

Olvidándonos del tema Grouping un GridView permite que modifiquemos el panel dentro del cual coloca todos los items, así que modificamos ese panel remplazándolo por el VirtualizingStackPanel.

 
<GridView.ItemsPanel>
    <ItemsPanelTemplate>
        <VirtualizingStackPanel Background="Black"
                                Orientation="Horizontal"/>
    </ItemsPanelTemplate>
</GridView.ItemsPanel>

Así las cosas ahora mejora la visualización de los items:

GridView changing VirtualizingStackPanel

Como te das cuenta es un asunto sencillo, pero necesitas saber un conjunto de varias cosas.

Espero que este artículo le sea de ayuda a muchas personas, si tienen dudas no titubeen en postearlas.

chau!