Jump List con LongListSelector Control

Hola a todos!

Siguiendo con mis desarrollos de aplicaciones para Windows Phone, en la versión 7.x, quise aplicar en las listas la opción para agrupar los valores por letra, como hace en la lista de aplicaciones o en los contactos.

Luego de investigar y probar bastante encuentro que no se puede hacer con los controles de Windows Phone, y lo mejor que encontré para realizarlo es Silverlight for Windows Phone Toolkit, con el release de Noviembre de 2010. Puede bajarse de http://silverlight.codeplex.com/releases/view/55034.

Voy a utilizar un proyecto Aplicación de Panorama de Windows Phone para realizar este ejemplo.

Bien, para comenzar agregamos la referencia al ensamblado Microsoft.Phone.Controls.Toolkit.dll que se encuentra en la carpeta C:\Program Files (x86)\Microsoft SDKs\Windows Phone\v7.0\Toolkit\Nov10\Bin

El control que vamos a utilizar del Toolkit se llama LongListSelector, abajo escribo el xaml del archivo MainPage.xaml de mi aplicación:

<phone:PhoneApplicationPage
    x:Class="Tornado.PanoramaWindowsPhone.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:controls="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"
    xmlns:data="clr-namespace:Tornado.PanoramaWindowsPhone"
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="800"
    d:DataContext="{d:DesignData SampleData/MainViewModelSampleData.xaml}"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait"  Orientation="Portrait"
    shell:SystemTray.IsVisible="False">

    <phone:PhoneApplicationPage.Resources>
        <data:ClientesPorDenominacion x:Key="clientes"/>
        <data:FondoGrupoBrushValueConverter x:Key="grupoBrush"/>
    </phone:PhoneApplicationPage.Resources>

    <!--LayoutRoot es la cuadrícula raíz donde se coloca todo el contenido de la página-->
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <!--Control Panorama-->
        <controls:Panorama Title="Jump List">
            <controls:Panorama.Background>
                <ImageBrush ImageSource="PanoramaBackground.png"/>
            </controls:Panorama.Background>
 
            <!--Elemento Panorama uno-->
            <controls:PanoramaItem Header="clientes">
                <!--Clientes-->
                <toolkit:LongListSelector Name="ClientesListBox" Background="Transparent"
                                          ItemsSource="{StaticResource clientes}">
                    <toolkit:LongListSelector.GroupItemsPanel>
                        <ItemsPanelTemplate>
                            <toolkit:WrapPanel Orientation="Horizontal"/>
                        </ItemsPanelTemplate>
                    </toolkit:LongListSelector.GroupItemsPanel>
                   
                    <toolkit:LongListSelector.GroupItemTemplate>
                        <DataTemplate>
                            <Border Background="{Binding Converter={StaticResource grupoBrush}}" Width="99" Height="99" Margin="6" IsHitTestVisible="{Binding HasItems}">
                                <TextBlock Text="{Binding Key}"
                                       FontFamily="{StaticResource PhoneFontFamilySemiBold}"
                                       FontSize="36"
                                       Margin="{StaticResource PhoneTouchTargetOverhang}"
                                       Foreground="{StaticResource PhoneForegroundBrush}"                                       
                                       VerticalAlignment="Bottom"/>
                            </Border>
                        </DataTemplate>
                    </toolkit:LongListSelector.GroupItemTemplate>
 
                    <toolkit:LongListSelector.GroupHeaderTemplate>
                        <DataTemplate>
                            <Border Background="Transparent">
                                <Border Background="{StaticResource PhoneAccentBrush}" Width="75" Height="75" HorizontalAlignment="Left">
                                    <TextBlock Text="{Binding Key}"
                                           Foreground="{StaticResource PhoneForegroundBrush}"
                                           Style="{StaticResource PhoneTextExtraLargeStyle}"
                                           VerticalAlignment="Bottom"/>
                                </Border>
                            </Border>
                        </DataTemplate>
                    </toolkit:LongListSelector.GroupHeaderTemplate>
 
                    <toolkit:LongListSelector.ItemTemplate>
                        <DataTemplate>
                            <Grid Margin="{StaticResource PhoneTouchTargetOverhang}">
                                <StackPanel VerticalAlignment="Top">
                                    <TextBlock Text="{Binding Denominacion}" Style="{StaticResource PhoneTextLargeStyle}" FontFamily="{StaticResource PhoneFontFamilySemiBold}" Margin="12,-12,12,6"/>
                                    <TextBlock Text="{Binding Domicilio}" Style="{StaticResource PhoneTextNormalStyle}" TextWrapping="Wrap" FontFamily="{StaticResource PhoneFontFamilySemiBold}"/>
                                    <StackPanel Orientation="Horizontal">
                                        <TextBlock Text="Teléfono:" Style="{StaticResource PhoneTextSmallStyle}"/>
                                        <TextBlock Text="{Binding Telefono}" Style="{StaticResource PhoneTextSmallStyle}" FontFamily="{StaticResource PhoneFontFamilySemiBold}"/>
                                    </StackPanel>
                                    <StackPanel Orientation="Horizontal">
                                        <TextBlock Text="Email:" Style="{StaticResource PhoneTextSmallStyle}"/>
                                        <TextBlock Text="{Binding Email}" Style="{StaticResource PhoneTextSmallStyle}" FontFamily="{StaticResource PhoneFontFamilySemiBold}"/>
                                    </StackPanel>
                                </StackPanel>
                            </Grid>
                        </DataTemplate>
                    </toolkit:LongListSelector.ItemTemplate>
 
                </toolkit:LongListSelector>
            </controls:PanoramaItem>
 
            <!--Elemento Panorama dos-->
            <!--Use 'Orientation="Horizontal"' para habilitar un panel que dispuesto horizontalmente-->
            <controls:PanoramaItem Header="dos">
            </controls:PanoramaItem>
        </controls:Panorama>
    </Grid>
</phone:PhoneApplicationPage>

Como se ve en el ejemplo, lo primero que tenemos que agregar es la declaración al namespace "toolkit":

xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"

Luego vemos las siguientes líneas:

   <phone:PhoneApplicationPage.Resources>
        <data:ClientesPorDenominacion x:Key="clientes"/>
        <data:FondoGrupoBrushValueConverter x:Key="grupoBrush"/>
    </phone:PhoneApplicationPage.Resources>

La primer declaración data corresponde al objeto IEnumerable que asociaremos al ItemsSource del control. La segunda la vamos a utilizar para convertir la visualización de los grupos dependiendo si tiene o no clientes. A continuación vemos las diferentes clases.

Para realizar las siguientes clases voy a utilizar una entidad Cliente que creo que en post anterior: http://martincomparetto.blogspot.com.ar/2013/03/bases-de-datos-locales-en-windows-phone.html, pero voy a agregar el atributo Key, que me va a devolver la letra con la que empieza la denominación del cliente:

    [Table(Name="Clientes")]
    public class Cliente
    {
        [Column(IsPrimaryKey = true, IsDbGenerated = true)]
        public int Id { get; set; }
 
        [Column(CanBeNull = false)]
        public string Codigo { get; set; }
 
        [Column(CanBeNull = false)]
        public string Denominacion { get; set; }
 
        [Column(CanBeNull = true)]
        public string Domicilio { get; set; }
 
        [Column(CanBeNull = true)]
        public string Telefono { get; set; }
 
        [Column(CanBeNull = true)]
        public string Email { get; set; }
 
        [Column(CanBeNull = false)]
        public float PorcentajeDescuento { get; set; }
 
        private string _key;
        public string Key
        {
            get
            {
                if (string.IsNullOrEmpty(_key))
                {
                    char key = char.ToLower(this.Denominacion[0]);
 
                    if (key < 'a' || key > 'z')
                    {
                        key = '#';
                    }
                    _key = key.ToString();
                }
 
                return _key;
            }
            set { _key = value; }
        }
    }

Entonces, la primer clase que creo es la que el control utiliza para agrupar los valores de la lista. A esta clase se van a agregar los clientes pertenecientes a un grupo:

    public class ClientesEnGrupo : List<Cliente>
    {
        public string Key { get; set; }
 
        public bool HasItems { get { return Count > 0; } }
 
        public ClientesEnGrupo(string grupo)
        {
            Key = grupo;           
        }
    }

La clase es una List<T> de entidades Cliente. Tiene una propiedad Key que identifica al grupo que pertenecen cada Cliente y otra propiedad HasItem que vamos a utilizar para habilitar los grupos que contiene clientes y deshabilitar los que no tienen, esto lo vemos en GroupItemTemplate, en la propiedad IsHitTestVisible del control Border:

<toolkit:LongListSelector.GroupItemTemplate>
    <DataTemplate>
        <Border Background="{Binding Converter={StaticResource grupoBrush}}" Width="99" Height="99" Margin="6" IsHitTestVisible="{Binding HasItems}">
            <TextBlock Text="{Binding Key}"
                    FontFamily="{StaticResource PhoneFontFamilySemiBold}"
                    FontSize="36"
                    Margin="{StaticResource PhoneTouchTargetOverhang}"
                    Foreground="{StaticResource PhoneForegroundBrush}"                                       
                    VerticalAlignment="Bottom"/>
        </Border>
    </DataTemplate>
</toolkit:LongListSelector.GroupItemTemplate>

Bien, la segunda clase que vamos a crear es la que va a completar los datos y se va a asociar a la lista. Esta clase también es una List<T> de la clase ClientesEnGrupo.

    public class ClientesPorDenominacion : List<ClientesEnGrupo>
    {
        private static readonly string Grupos = "#abcdefghijklmnopqrstuvwxyz";
 
        public ClientesPorDenominacion()
        {
            using (TornadoDbContext context = new TornadoDbContext("Data Source='isostore:/Datos.sdf'"))
            {
                List<Cliente> clientesList = context.Clientes.OrderBy(o => o.Denominacion).ToList();
 
                // Creo esta variable para almacenar los diferentes grupos para despues agregarles los clientes correspondientes a cada uno.
                Dictionary<string, ClientesEnGrupo> grupos = new Dictionary<string, ClientesEnGrupo>();
 
                // Agregamos los grupos a la colección
                foreach (char c in Grupos)
                {
                    ClientesEnGrupo grupo = new ClientesEnGrupo(c.ToString());
                    this.Add(grupo);
                    grupos[c.ToString()] = grupo;
                }

                foreach (Cliente cliente in clientesList)
                {
                    grupos[cliente.Key].Add(cliente);
                }
            }
        }
    }

En esta clase recupero los datos de mi DB local (como muestro en el post http://martincomparetto.blogspot.com.ar/2013/03/bases-de-datos-locales-en-windows-phone.html) y voy agregando los diferentes grupos.

El primer foreach simplemente recorre la propiedad Grupos, que contiene un string con los grupos que quiero mostrar (en este caso son las letras, pero tranquilamente podría poner otro tipo de agrupamiento, por ejemplo zonas geográficas de los clientes).

El segundo foreach agrega cada cliente al grupo que corresponde (los que no empiezan con una letra van al grupo "#", los que empiezan con "a" van al grupo de las "a", etc.)
 
Por último queda hacer la clase Converter, que nos va a mostrar de color rojo los grupos que contiene clientes y de color negro los que no tienen datos, como vemos a continuación en la imagen:


La clase es la siguiente:

    public class FondoGrupoBrushValueConverter : IValueConverter
    {
        public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            ClientesEnGrupo grupo = value as ClientesEnGrupo;
            object result = null;
 
            if (grupo != null)
            {
                if (grupo.Count == 0)
                {
                    result = (SolidColorBrush)Application.Current.Resources["PhoneChromeBrush"];
                }
                else
                {
                    result = (SolidColorBrush)Application.Current.Resources["PhoneAccentBrush"];
                }
            }
 
            return result;
        }

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

Esta clase va a devolver un estilo dependiente si el grupo tiene o no clientes asociados.
Como vimos al comienzo del post, esta clase la declaramos en la línea:

<data:FondoGrupoBrushValueConverter x:Key="grupoBrush"/>

y la vamos a utilizar en GroupItemTemplate, en la propiedad Background del control Border:

<toolkit:LongListSelector.GroupItemTemplate>
    <DataTemplate>
        <Border Background="{Binding Converter={StaticResource grupoBrush}}" Width="99" Height="99" Margin="6" IsHitTestVisible="{Binding HasItems}">
            <TextBlock Text="{Binding Key}"
                    FontFamily="{StaticResource PhoneFontFamilySemiBold}"
                    FontSize="36"
                    Margin="{StaticResource PhoneTouchTargetOverhang}"
                    Foreground="{StaticResource PhoneForegroundBrush}"                                       
                    VerticalAlignment="Bottom"/>
        </Border>
    </DataTemplate>
</toolkit:LongListSelector.GroupItemTemplate>
 
Bien, teniendo todo esto, solamente queda recordar que agregamos la referencia a la clase ClientesPorDenominacion en la línea:

<data:ClientesPorDenominacion x:Key="clientes"/>

y el x:Key "clientes" lo asignamos a la propiedad ItemsSource del control LongListSelector.

El resto del código es estética y bindeos de los campos de la entidad Cliente a los TextBlock.

Espero que sea de utilidad y hasta la próxima!

Comentarios

Facundo Barboza ha dicho que…
Muy Útil. Gracias por el aporte!!!

Entradas populares de este blog

Integration Services y Visual FoxPro Databases

Bases de datos locales en Windows Phone

Agregar proyecto existente al Project Explorer de Eclipse