Como implementar TemplateSelector en el ListBox de Windows Phone 7

Si solo has trabajando con Silverlight nunca has conocido el TemplateSelector de WPF, que como su nombre indica permite hacer un selector por discriminador para las plantillas de datos. En el caso que nos atañe ListBox, tiene una propiedad llamada ItemTemplate en la que se establece la plantilla de datos para cada uno de los ítems.

¿Para qué se puede querer cambiar la plantilla?

Imaginaros el escenario de estar haciendo una aplicación para mostrar una lista de noticias provenientes de un rss, podemos tener una plantilla para las noticias con imágenes, otra plantilla para las noticias sin imágenes y además podemos querer una plantilla especial para una noticia destacada. Este tipo de escenario que es el más común es bastante difícil de conseguir con Silverlight puesto que no tiene TemplateSelector.

Para conseguir esta funcionalidad tenemos que implementarlo a mano.

Hay varias maneras de llegar hasta esta aproximación la que desde mi punto de vista es la más adecuada es crear un ListBox personalizado, porque nos permite tener toda la funcionalidad y aspecto existente del ListBox pero con el selector de plantillas.

El proceso de creación de un ListBox personalizado se hace en dos partes, la primera se tiene que hacer una clase que herede de ListBox. En esta clase, que llamaremos DataTemplateListBox, tenemos que sobrescribir dos métodos virtuales:

  • GetContainerForItemOverride: este método devuelve un objeto que será el ítem container que el ListBox usará para alojar los ítems que se establezcan en el ItemSource. En el caso del ListBox, la implementación predeterminada de este método devuelve un ListBoxItem que es el contenedor predeterminado del ListBox. En nuestro ejemplo tenemos que generar un ListBoxItem personalizado que tendrá la funcionalidad de establecer el DataTemplate.
  • PrepareContainerForItemOverride: Este método es llamado cuando el ListBox está a punto de empezar a hacer la pasada de Layout, y es el momento justo para establecer el código de nuestros discriminador. Este método acepta dos parámetros, uno llamado element de tipo DependencyObject que es el contenedor del ListBox, y el otro ítem de tipo object que es el objeto que nosotros estamos estableciendo al ListBox.
public class DataTemplateListBox : ListBox
{
    protected override DependencyObject GetContainerForItemOverride()
    {
        return new DataTemplateListBoxItem();
    }
    protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
    {
        base.PrepareContainerForItemOverride(element, item);
        DataTemplateListBoxItem lbi = (DataTemplateListBoxItem)element;
        if (item is News)
        {
            News newItem = (News)item;
            if (!newItem.IsHighLighted)
            {
                lbi.CustomTemplate = (DataTemplate)App.Current.Resources["NewsDataTemplate"];
            }
            else
            {
                lbi.CustomTemplate = (DataTemplate)App.Current.Resources["HighLightedNewsDataTemplate"];
            }
        }
    }
}

Como hemos dicho anteriormente en el proceso de creación del ListBox tenemos que crear también un ListBoxItem que será el contenedor neutro que tendrá una propiedad de tipo DataTemplate, que será el DataTemplate usado para dibujar ese elemento con una plantilla especifica.

[TemplatePart(Name = "DisplayContent", Type = typeof(ContentControl))]
public class DataTemplateListBoxItem : ListBoxItem
{
    #region Properties
    
    public DataTemplate CustomTemplate
    {
        get { return (DataTemplate)GetValue(CustomTemplateProperty); }
        set { SetValue(CustomTemplateProperty, value); }
    }

    public static readonly DependencyProperty CustomTemplateProperty =
        DependencyProperty.Register(
            "CustomTemplate", 
            typeof(DataTemplate), 
            typeof(DataTemplateListBoxItem), 
            new PropertyMetadata(null));

    #endregion

    #region Constructors
    public CategoryItemsListBoxItem()
    {
        DefaultStyleKey = typeof(DataTemplateListBoxItem);
    }
    #endregion
}

Este es el código de ejemplo de un DataTemplateListBoxItem, pero también necesitamos generar un estilo predeterminado que contenga un elemento de tipo ContentControl que será el elemento que tendrá la interfaz del usuario del elemento.

<Style TargetType="controls:DataTemplateListBoxItem" >
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="controls:DataTemplateListBoxItem">
                <ContentControl x:Name="DisplayContent" Content="{TemplateBinding DataContext}" 
                                ContentTemplate="{TemplateBinding CustomTemplate}"  />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Como se puede apreciar en el código xaml lo que se hace es agregar a la plantilla un ControlTemplate y en ese ControlTemplate se establecen dos propiedades Content con un binding de plantilla (TemplateBinding) a la propiedad DataContext, con eso conseguirnos establecer el objeto como contenido y que se dibuje. La otra propiedad que falta por establecer es justamente la propiedad ContentTemplate que en este caso se hace lo mismo, hacer un binding de plantilla con la propiedad CustomTemplate que es de la clase DataTemplateListBoxItem.

Ahora lo que tenemos que hacer es insertar nuestro DataTemplateListBox en el control MainPage y enlazarle un ViewModel con datos para mostrar un par de noticias generadas por código:

<Grid x:Name="ContentPanel" Grid.Row="1">
   <DataTemplateDemo_Controls:DataTemplateListBox ItemsSource="{Binding Items}"/>
</Grid>

Con eso conseguimos tener un ListBox en el nosotros decidimos en cada uno de los elementos como queremos aplicarle una plantilla de datos. Este es el resultado:

image

La demo no es muy impresionante en sí, pero permite tener la flexibilidad de poder elegir elemento por elemento cual es la plantilla que vamos a usar de manera programática.

La demo completa la podéis descargar de aquí.

Luis Guerrero.

Gestión de proxies de WCF en Silverlight 4

Una de las cosas que más esfuerzo me cuestan cuanto estoy desarrollando aplicaciones en Silverlight es la gestión del ciclo de vida de un proxy de WCF, es decir, crear la instancia del proxy con la configuración del binding correcta, subscribirme a los eventos complete de las operaciones que quiero invocar y en caso de falta en el proxy volver a hacer todo ese trabajo, de-subscribirme de los eventos complete y volver a repetir el proceso de nuevo.

Es por eso que en el día a día del desarrollo de aplicaciones de Silverlight siempre intento hacer que las cosas sean muy fáciles para mí y que pueda manejar el ciclo de vida de un proxy de una manera muy cómoda. Viendo un poco cuales son las limitaciones que tengo con los proxy de wcf, me encuentro con las que he comentado anteriormente:

  • Creación del proxy con la configuración del binding adecuada.
  • Invocación asíncrona de las operaciones
  • Ciclo de vida del proxy

La primera de las limitaciones se soluciona de manera fácil, con una factoría. Una clase helper o factoría que me permita instancias los proxy de manera cómoda con un sistema centralizado de configuración de bindings y demás opciones. Normalmente en un proyecto de Silverlight casi todos los proxy, por no decir todos, serán del mismo tipo, así que no tenemos que preocuparnos mucho por dar soporte a varios tipos de configuraciones.

public class ProxyFactory
{
    public static T CreateInstance<T, K>(Uri uri)
        where K : class
        where T : ClientBase<K>
    {
        T result = default(T);

        CustomBinding custom = new CustomBinding();
        custom.Elements.Add(new BinaryMessageEncodingBindingElement());
        custom.Elements.Add(new HttpTransportBindingElement()
        {
            MaxReceivedMessageSize = 2147483647,
            MaxBufferSize = 2147483647
        });
        SetDebugTimeouts(custom);

        EndpointAddress address = new EndpointAddress(uri.ToString());

        result = (T)(object)Activator.CreateInstance(typeof(T), custom, address);
        
        return result;
    }

    private static void SetDebugTimeouts(CustomBinding custom)
    {
        if (custom != null)
        {
            TimeSpan timeout = TimeSpan.FromSeconds(60 * 3);
            timeout = TimeSpan.MaxValue;
            custom.CloseTimeout = timeout;
            custom.OpenTimeout = timeout;
            custom.ReceiveTimeout = timeout;
            custom.SendTimeout = timeout;
        }
    }
}

Aquí tenemos el código de la factoría de proxy.

Si nos fijamos acepta dos parámetros genéricos, que son: K es la interfaz del contrato del canal de comunicaciones es decir el contrato de operaciones y T es la clase que hereda de ServiceBase<K>, es decir la clase que implementa el contrato de operaciones en cliente.

Así de esta manera en el cuerpo de la creación podemos crear a mano un custombinding y añadir los elementos que necesitamos, como por ejemplo: BinaryMessageEncondingBindingElement y HttpTransportBindingElement, además de configurar parámetros de esas clases. Creamos también una instancia de la clase EndpointAddress que contiene la dirección url del servicio wcf.

Una vez que tenemos todo esto podemos utilizar la clase Activator para crear una instancia dinámicamente del proxy (a través del tipo de T) pasándoles los parámetros previamente establecidos.

Para consumir el proxy sería de esta manera:

KeyPlayerServiceClient proxy = 
ProxyFactory.CreateInstance<KeyPlayerServiceClient, IKeyPlayerService>(ProxyUrl.KeyPlayerUrl);

Así podemos tener un sistema centralizado de creación de todos los proxies que queramos consumir desde Silverlight.

Manejar el resultado y el ciclo de vida

Pero esta manera de crear las instancias no soluciona el problema que tenemos de manejar el ciclo de vida del proxy, subscribirnos al evento complete de la operación y obtener el resultado de la operación. Es por eso que necesitamos una clase que nos permita invocar una operación asíncrona de un servicio web y que de alguna manera esa clase helper, cree la instancia de la clase, se subscriba al evento de complete, obtenga el resultado de la operación y llame a un delegado que nosotros le hemos especificado. Esto sería lo ideal.

Pues bien podemos hacer todo eso de manera cómoda con esta clase ProxyManager. Antes de centrarnos en cuál es la implementación concreta de la clase y los detalles vamos a ver cómo podemos consumir una operación típica de un servicio de wcf con esta clase ProxyManager.

Si tenemos un servicio con esta operación:

[ServiceContract]
public interface IServiceDemo
{
   [OperationContract]
   DateTime GetDateTime();
}

Tenemos una única operación que se llama GetDateTime y que obtenemos un objeto de tipo DateTime.

Una vez que hemos agregado la referencia al proxy de WCF desde nuestro proyecto de Silverlight, podemos hacer esto:

// manera directa de invocar un servicio web y solo centrarse en la respuesta segun su tipo
ProxyManager<ServiceDemoClient, IServiceDemo> manager = new ProxyManager<ServiceDemoClient, IServiceDemo>(ProxyUrl.DemoService);
// como sabemos que el servicio web devuelve un resultado de tipo DateTime, podemos
// agregar un Action<DateTime> porque DAteTiem es el tipo de devuelve
manager.InvokeOperation<DateTime, GetDateTimeCompletedEventArgs>(
    "GetDateTime",
    new Action<DateTime>(OnGetDateTimeCompleted),
    null);
private void OnGetDateTimeCompleted(DateTime dateTime)
{
}

En este ejemplo fijaros que utilizando la genericidad y la reflexión podemos hacer que la invocación del servicio sea mucho más cómoda, podemos resumirlo a una simple línea de código.

Requisitos tiene la clase ProxyManager

Para crear una instancia de la clase ProxyManager tenemos que pasar los mismos argumentos de tipos que el ProxyFactory, de hecho internamente utiliza el mismo mecanismo, además de la uri de la dirección del servicio web.

Una vez que tenemos el objeto creado tenemos que llamar al único método de la clase ProxyManager, InvokeOperation.

InvokeOperation acepta dos parámetros de tipo que son el tipo que devuelve la operación del servicio web, si nos acordamos era un DateTime, y el otro parámetro genérico es la clase que hereda de AsyncCompletedEventArgs que contiene el resultado de la invocación, en nuestro ejemplo, GetDateTimeCompletedEventArgs. De esta clase es donde el ProxyManager obtendrá el resultado de la operación.

Como parámetros de la función tenemos que suplir el nombre de la operación como un string, en nuestro caso GetDateTime (pero sin Async al final), después un delegado de tipo Action<T> con el resultado, y al final de todos los parámetros un array de tipo object con la palabra params que son los argumentos de la invocación, en nuestro caso null.

Lo bueno que tiene esta clase ProxyManager es que automáticamente por nosotros crea la instancia del proxy, se subscribe al evento complete de la operación que queremos invocar y automáticamente invoca nuestro delegado de tipo Action<T> con el resultado, además de eso cuando termina la invocación del objeto de tipo Action<T> se de-subscribe del evento y cierra el proxy. Toda una gozada.

Además de eso tenemos la posibilidad de utilizar una sobrecarga de la función InvokeOperation que nos permite tener también acceso al objeto de UserState de la invocación por si invocamos más de una vez la operación y necesitamos acceder a un objeto de usuario en la devolución de llamada:

// manera directa de invocar un servicio web y solo centrarse en la respuesta segun su tipo
ProxyManager<ServiceDemoClient, IServiceDemo> manager = new ProxyManager<ServiceDemoClient, IServiceDemo>(ProxyUrl.DemoService);
// como sabemos que el servicio web devuelve un resultado de tipo DateTime, podemos
// agregar un Action<DateTime> porque DAteTiem es el tipo de devuelve
manager.InvokeOperation<DateTime, GetDateTimeCompletedEventArgs>(
    "GetDateTime",
    new Action<DateTime>(OnGetDateTimeCompleted),
    null);

manager.InvokeOperation<DateTime, object, GetDateTimeCompletedEventArgs>(
    "GetDateTime",
    new Action<DateTime, object>(OnGetDateTimeWithArgumentsCompleted),
    null);
private void OnGetDateTimeWithArgumentsCompleted(DateTime result, object userState)
{

}

Con esto podemos hacer que la gestión de los proxies de WCF sea mucho más eficiente que cómoda para el programador que antes.

Codigo fuente completo de la clase ProxyManager:

using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.ServiceModel;

namespace ViewModelBinding.Services
{
    public class ProxyManager<TService, KServiceInterface>
        where KServiceInterface : class
        where TService : ClientBase<KServiceInterface>
    {
        public ProxyManager(Uri url)
        {
            proxy = ProxyFactory.CreateInstance<TService, KServiceInterface>(url);
        }

        public void InvokeOperation<TResult, KEventArgs>(string operationName, Action<TResult> action, params object[] args)
            where KEventArgs : AsyncCompletedEventArgs
        {
            if (!string.IsNullOrWhiteSpace(operationName))
            {
                InternalActionOf<KEventArgs, TResult> internalAction = new InternalActionOf<KEventArgs, TResult>(action);
                internalAction.EventInfo = proxy.GetType().GetEvent(string.Format("{0}Completed", operationName));
                internalAction.Delegate = new EventHandler<KEventArgs>(internalAction.OnActionExecuted);
                internalAction.EventInfo.AddEventHandler(proxy, internalAction.Delegate);
                internalAction.Proxy = proxy;

                if (args == null)
                {
                    args = new object[] { };
                }

                MethodInfo[] methods = proxy.GetType().GetMethods();
                string methodName = string.Format("{0}Async", operationName);
                var found = (from p in methods
                             where p.Name == methodName && p.GetParameters().Length == args.Length
                             select p).FirstOrDefault();



                if (found != null)
                {
                    found.Invoke(proxy, args);
                }
            }
        }

        public void InvokeOperation<TResult, TUSerState, KEventArgs>(string operationName, Action<TResult, TUSerState> action, params object[] args)
            where KEventArgs : AsyncCompletedEventArgs
        {
            if (!string.IsNullOrWhiteSpace(operationName))
            {
                InternalActionOf<KEventArgs, TResult, TUSerState> internalAction = new InternalActionOf<KEventArgs, TResult, TUSerState>(action);
                internalAction.EventInfo = proxy.GetType().GetEvent(string.Format("{0}Completed", operationName));
                internalAction.Delegate = new EventHandler<KEventArgs>(internalAction.OnActionExecuted);
                internalAction.EventInfo.AddEventHandler(proxy, internalAction.Delegate);
                internalAction.Proxy = proxy;

                if (args == null)
                {
                    args = new object[] { };
                }

                MethodInfo[] methods = proxy.GetType().GetMethods();
                string methodName = string.Format("{0}Async", operationName);
                var found = (from p in methods
                             where p.Name == methodName && p.GetParameters().Length == args.Length
                             select p).FirstOrDefault();



                if (found != null)
                {
                    found.Invoke(proxy, args);
                }
            }
        }

        private TService proxy;
    }
}
internal class InternalActionOf<EventArgs, TResult, TUserState>
       where EventArgs : AsyncCompletedEventArgs
{
   public InternalActionOf(Action<TResult, TUserState> action)
   {
       this.action = action;
   }

   internal void OnActionExecuted(object args, EventArgs e)
   {
       PropertyInfo property = e.GetType().GetProperty("Result", BindingFlags.Public | BindingFlags.Instance);
       if (property != null)
       {
           object result = property.GetValue(e, null);
           if (result != null)
           {
               if (typeof(TResult).IsAssignableFrom(property.PropertyType))
               {
                   action((TResult)result, (TUserState)e.UserState);
                   EventInfo.RemoveEventHandler(Proxy, Delegate);
               }
           }
       }
   }

   public EventHandler<EventArgs> Delegate { get; set; }
   public EventInfo EventInfo { get; set; }
   public object Proxy { get; set; }

   private Action<TResult, TUserState> action;
}
internal class InternalActionOf<EventArgs, TResult>
            where EventArgs : AsyncCompletedEventArgs
{
    public InternalActionOf(Action<TResult> action)
    {
        this.action = action;
    }

    internal void OnActionExecuted(object args, EventArgs e)
    {
        PropertyInfo property = e.GetType().GetProperty("Result", BindingFlags.Public | BindingFlags.Instance);
        if (property != null)
        {
            object result = property.GetValue(e, null);
            if (result != null)
            {
                if (typeof(TResult).IsAssignableFrom(property.PropertyType))
                {
                    action((TResult)result);
                    EventInfo.RemoveEventHandler(Proxy, Delegate);
                }
            }
        }
    }

    public EventHandler<EventArgs> Delegate { get; set; }
    public EventInfo EventInfo { get; set; }
    public object Proxy { get; set; }

    private Action<TResult> action;
}

Luis Guerrero.