martes, 4 de agosto de 2009

Ajax: C# .Net 3.5 + jQuery (VI) – Proxies de objetos para javascript, el lado del cliente.

Con esta entrada finalizamos esta serie en la que hemos prototipado diferentes proxies en javascript: para llamar a métodos web, a servicios, y para utilizar objetos de C# “como si fueran de javascript”.

Para el final de la entrada anterior teníamos resuelto un servicio (InvokerService.asmx en nuestro código de ejemplo) que nos permitía recibir un objeto de cualquier tipo en JSON, deserializarlo, ejecutar un método indicado por un parámetro y devolver tanto el resultado del método como una representación del estado final del objeto. Quedó entonces pendiente para esta entrada la parte del cliente, es decir, un objeto javascript que “simule” ser ese objeto de C#.

Creo que lo más fácil es primero hacer un proxy para un tipo específico a mano y luego utilizar ese código como modelo para codificar un procedimiento que genere un proxy similar, pero ya tomando el tipo como parámetro.

Recordemos lo que este proxy debe hacer:

  • Al invocar un método en este objeto de javascript, internamente  pasa la llamada a un único web service (InvokerService.asmx) que recibe el objeto serializado, el tipo equivalente en C# y la orden de ejecutar un método con los parámetros correspondientes, que también recibe serializados.
  • El servidor devuelve el resultado del método y el estado final del objeto serializado, ya que esa ejecución pudo haber modificado alguna de sus propiedades.
  • De vuelta en el cliente copiamos las propiedades del objeto que recibimos a sus correspondientes en el de javascript y devolvemos el resultado del método.

Empecemos entonces con el código. Buen momento para recordarles que la solución de VS2008 completa está subida a Google Code.

Para que la creación del proxy por código sea más sencilla tenemos que trasladar toda la funcionalidad que podamos fuera del proxy, a una función estática que podamos incluir en nuestro archivo JSProxy.js.

Así, vamos a crear primero una función auxiliar, Page.Ajax.Invoke, que es la que hará la mayor parte del trabajo:

Page.Ajax.Invoke = function(type,obj,method)
{
    var params = null;
    
    if(arguments.length > 3)
    {
        var params = new Array();
        for(var i=3;i<arguments.length;i++)
            params.push(JSON.stringify(arguments[i]));
    }
    
    //función para pasar a JSON.stringify que excluye __type__ de la serialización del 
    //objeto ya que el tipo se pasa en type.
    var excludeType = function(key,value){
        if(key!="__type__")
            return value;                                
    };
    
    var jsonObj = Page.Utils.JSON.stringify(obj, excludeType);
    var jsonParams=Page.Utils.JSON.stringify(params);

    var ret = Page.InvokerService.Invoke(type, jsonObj, method, jsonParams);

    for(var i in ret.TargetObject)
        obj[i] = ret.TargetObject[i];
    
    return ret.ReturnValue;                            
}

La declaración de esta función es un poco rara, porque la complica el hecho de que no sabemos cuántos parámetros requiere el método a invocar. Podríamos pasar un array, pero es más fácil para la codificación si los pasamos como argumentos adicionales, accediéndolos a través del array especial de javascript arguments.

Así, sólo están especificados los tres primeros parámetros, que son siempre los mismos: type (el nombre del tipo en C# a invocar), obj (una referencia al proxy javascript que inicia la invocación) y method (el método a invocar). A partir de allí, debemos pasarle los parámetros para el método a invocar, tantos como sean necesarios. ¿Enrevesado? Es más fácil codificarlo que decirlo, créanme.

Tomemos como modelo la clase Calc, el ejemplo de nuestro post anterior:

namespace AjaxConJQueryObjectProxy
{
    public class Calc
    {
        public Calc() { }

        public decimal Op1 { get; set; }
        
        public decimal Op2 { get; set; }

        public decimal Sum()
        {
            this.Op1 = this.Op1 + this.Op2;
            return this.Op1;
        }

        public decimal SumNumbers(decimal op1, decimal op2)
        {
            this.Op1 = op1;
            this.Op2 = op2;

            return this.Sum();
        }
    }
}

El proxy javascript queda como sigue:

Page.Ajax.Namespace("AjaxConJQueryObjectProxy");

AjaxConJQueryObjectProxy.Calc = function(){
   this.__type__ = "AjaxConJQueryObjectProxy.Calc";        

   this.Op1 = 0;
   this.Op2 = 0;

   this.Sum = function()
   {
       return Page.Ajax.Invoke(this.__type__, this, "Sum");
   }

   this.SumNumbers = function(op1, op2)
   {
       return Page.Ajax.Invoke(this.__type__, this, "SumNumbers", op1, op2);
   }
}

Como verán, quedó bastante simple. Es apenas un pasamanos estético entre el programador final y Page.Ajax.Invoke. Con eso como guía, vamos a crear la función estática ImportObject en nuestra clase JSProxy.

public static StringBuilder ImportObject(Type type, string executeServiceUrl)
{
    StringBuilder builder = new StringBuilder(400);
    builder.Append("$().ready(function(){");

    //crea el namespace.
    if (type.FullName.Contains('.'))
    {
        string ns = type.FullName.Substring(0, type.FullName.LastIndexOf('.'));
        builder.AppendFormat("Page.Ajax.Namespace(\"{0}\");", ns);
    }

    //declaración.
    builder.AppendFormat("{0}=function(){{", type.FullName);
    //variable interna con el nombre del tipo que representa.
    builder.AppendFormat("this.__type__=\"{0}\";", type.FullName);

    //inicialización de propiedades. Hay que crear una instancia de type
    //para determinar sus valores por defecto.
    object instance = type.Assembly.CreateInstance(type.FullName);

    FieldInfo[] fields = type.GetFields(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static);
    JavaScriptSerializer j = new JavaScriptSerializer();
    foreach (FieldInfo field in fields)
    {
        builder.AppendFormat("this.{0}=Page.Utils.JSON.parse(\"", field.Name);
        j.Serialize(field.GetValue(instance), builder);
        builder.Append("\");");
    }

    //si bien el tratamiento de las propiedades es igual al de las variables públicas (fields)
    //tal vez haya que hacer algo separado más adelante.
    PropertyInfo[] properties = type.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static);
    foreach (PropertyInfo property in properties)
    {
        builder.AppendFormat("this.{0}=Page.Utils.JSON.parse(\"", property.Name);
        j.Serialize(property.GetValue(instance, null), builder);
        builder.Append("\");");
    }

    //métodos.
    MethodInfo[] methods = type.GetMethods(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static);
    foreach (MethodInfo method in methods)
    {
        if (method.IsSpecialName)
            continue;

        //declaración
        builder.AppendFormat("this.{0}=function(", method.Name);
        //parámetros.
        ParameterInfo[] parameters = method.GetParameters();
        if (parameters.Length > 0)
        {
            foreach (ParameterInfo parameter in parameters)
                builder.AppendFormat("{0},", parameter.Name);
            builder.Remove(builder.Length - 1, 1);
        }
        builder.Append("){");
        //llamada interna a Page.Ajax.Invoke
        builder.AppendFormat("return Page.Ajax.Invoke(this.__type__, this, \"{0}\",", method.Name);
        //pasaje de parametros a Page.Ajax.Invoke
        if (parameters.Length > 0)
        {
            foreach (ParameterInfo parameter in parameters)
                builder.AppendFormat("{0},", parameter.Name);
        }
        builder.Remove(builder.Length - 1, 1);
        builder.Append(");");  //fin de la llamada a Page.Ajax.Invoke

        builder.Append("};");// fin del método.
    }

    builder.Append("};"); //fin del objeto.

    builder.Append("});"); //fin de $().ready( function(){
    return builder;
}

Lo único que hace es crear en un StringBuilder un código similar al que vimos más arriba, examinando con reflection el tipo que se le pasa como parámetro. El ajuste fino fue trabajoso… falta una coma, sobra un paréntesis, pero requiere más paciencia que inteligencia.

Vamos a probar. En el código C# de default.aspx incluimos la llamada a JSProxy.ImportObject que nos devuelve el javascript para el proxy de Calc, y lo incluye en el header de la página. El código debe verse así:

using System;

namespace AjaxConJQueryObjectProxy
{
    public partial class _Default : PageBase
    {
        protected override void OnPreLoad(EventArgs e)
        {
            base.OnPreLoad(e); 
            
            //AddHeaderScript es una función que en el ejemplo habíamos puesto como privada
            //de PageBase. Agrega un tag <script> en el header con el código que se le pasa.
            //Es muy útil, así que si la hacemos protected la podemos utilizar en todas las
            //páginas.
            string calcImport = JSProxy.ImportObject(typeof(Calc), "/InvokerService.asmx").ToString();
            base.AddHeaderScript(calcImport);           
        }
    }
}

Sin nada más que eso, ya podemos probar nuestro objeto Calc en javascript con una pequeña función de prueba:

function TestCalculadora()
{    
    var calc1 = new AjaxConJQueryObjectProxy.Calc();
    var calc2 = new AjaxConJQueryObjectProxy.Calc();
    
    calc1.Op1 = 3;
    calc1.Op2 = 7;
    
    calc2.Op1 = 1;
    calc2.Op2 = 4;
    
    alert(calc1.Sum()); //devuelve (operador1 = 3) + (operador2 = 7) = 10 => operador1
    alert(calc2.Sum()); //devuelve (operador1 = 1) + (operador2 = 4) = 5 => operador1
    alert(calc1.Sum()); //devuelve (operador1 = 10) + (operador2 = 7) = 17 => operador1
    alert(calc2.Sum()); //devuelve (operador1 = 5) + (operador2 = 4) = 9 => operador1                
};

¡Y –con suerte- funciona! Les recuerdo que el código completo está en Google Code.


Esta serie de posts son una especie de puesta al día de las ideas que se plasmaron en la arquitectura que utilizaba en mi trabajo en ElectroChance, la gran mayoría de ellas pergeñadas y/o recolectadas por Rick Hunter (por más que te cambies el nombre sabemos quién sos en realidad -sigan el link-), así que a él especialmente y a todos los que alguna vez conformaron ese dream-team, gracias.

No hay comentarios.: