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.

Un comentario en “Gestión de proxies de WCF en Silverlight 4”

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *