lunes, 3 de agosto de 2009

Ajax: C# .Net 3.5 + jQuery (V) – Proxies de objetos para javascript, un servicio de ejecución.

Haciendo un recuento de las herramientas que estuvimos probando en las entradas anteriores de esta serie me imaginaba cómo sería a la hora de desarrollar una funcionalidad:

  • Creamos las funciones y procedimientos específicos para la comunicación entre cliente y servidor en el código C# de la (o las) página y los marcamos con WebMethod y ScriptMethod:
using System;
using System.Web.Script.Services;
using System.Web.Services;

namespace AjaxConjQueryJSProxy
{
    public partial class _Default : PageBase
    {
        [WebMethod]
        [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
        public static int Sum(int a, int b)
        {
            return a + b;
        }
    }
}
  • Sin hacer nada más que heredar la página de nuestra PageBase podemos utilizarlas desde javascript haciendo
var suma = Page.Sum(2,5);
  • Algo similar podemos hacer con funciones que consumimos desde un servicio Web. Tenemos que “registrarlo” en algún evento del ciclo de vida de la página, por ejemplo en el PreLoad:
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. */
            base.AddHeaderScript(JSProxy.ImportService(typeof(WebService1), "WebService1.asmx").ToString());           
        }
    }
}
  • y sin más lo podemos utilizar desde javascript haciendo:
Page.WebService1.HelloWorld()

Bastante bien, pero… soy un tipo vago, muy vago. Estaba pensando que podría, en vez de tener muchos servicios y métodos, hacer algo que me permita codificar una clase común y corriente en C# y utilizarla directamente en javascript.

Digamos que tengo la clase Calc, que se comporta como una calculadora: tiene dos operadores y al efectuar una operación devuelve el resultado al tiempo que lo coloca en el primer operador, que hace de acumulador. También puede tener un método-atajo al que le paso los dos operadores, establece las propiedades y efectúa la operación en un sólo paso.

No tiene sentido darle mucha funcionalidad. Mi calculadora de ejemplo sólo sabe sumar:

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();
        }
    }
}

Las propiedades Op1 y Op2 son los operadores, el método Sum los suma, devuelve el resultado y lo coloca en Op1. Por otro lado el “atajo” SumNumbers recibe los operadores como parámetros y luego de pasarlos a las propiedades correspondientes llama a Sum devolviendo el resultado.

Ahora, ¿por qué no puedo, en javascript y sin trabajo adicional (sin web services, web methods, ni nada por el estilo), hacer…

var calc1 = new AjaxConJQueryObjectProxy.Calc();

//prueba Sum
calc1.Op1 = 3;
calc1.Op2 = 7;
var res = calc1.Sum();
alert(res);

//prueba SumNumbers
res = calc1.SumNumbers(5,6);
alert(res);

…? ¿Eh? ¿Por qué?

Antes de empezar con todas estas pruebas hubiese dicho que es posible pero mucho más trabajoso que la comodidad que aporta, ya que al fin y al cabo llamar a un método web que haga lo mismo es muy fácil. Pero ahora estamos a un par de pasitos de conseguirlo con muy poco código:

  1. Creamos un objeto en javascript con las mismas propiedades que su equivalente en C#. Esto lo podemos hacer utilizando reflection en una forma similar a Automatizando la creación de proxies de javascript e Importando un proxy javascript.
  2. Al invocar un método en este objeto de javascript, internamente se invoca a un único web service que recibe el objeto serializado, el tipo equivalente en C# y la orden de ejecutar un método con los parámetros correspondientes, también serializados.
  3. El servidor crea una instancia del objeto equivalente en C# y ejecuta el método, devolviendo el resultado y el objeto serializado, ya que la ejecución pudo haber modificado propiedades.
  4. 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.

Hasta ahora, lo único que no tenemos es un servicio web “genérico” que pueda instanciar cualquier objeto y ejecutar cualquier método que se le solicite, así que es lo que vamos a hacer.

La firma del método sería:

[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public MethodExecuteResult Invoke(string typeName, string objectData, string methodName, string methodParams)

Invoke recibe los parámetros como string ya que no se sabe, a priori, a qué tipo corresponden: objectData (el objeto javascript serializado en JSON) debe convertirse en una instancia del tipo que indique typeName, y los elementos de methodParams (un array de parámetros serializado en JSON) deben convertirse al tipo de los parámetros del método. No podemos dejar esta conversión al framework ya que, por ejemplo, un 3 se convertiría a Int32 y tal vez el método requiere un Decimal.

El retorno es una clase sencilla que tiene como propiedades el objeto deserializado y el valor de retorno del método:

public class MethodExecuteResult
{
    public object TargetObject { get; set; }
    public object ReturnValue { get; set; }
}

Vamos primero por la conversión de objectData, para la que utilizaremos el método JavaScriptSerializer.Deserialize<T>. El problema aquí es que es un método genérico, y nosotros tenemos el tipo <T> como parámetro (¿por qué no hay una sobrecarga con el tipo como parámetro?). Hay dos formas de resolver este tipo de problemas: fácil y rápida de codificar o difícil y rápida al ejecutar. La primera es utilizar reflection para invocar al método genérico, y la segunda es tomar el Reflector y “robarse” el código necesario del framework, modificando el método luego… creo que a los efectos de esta prueba (y de casi toda la vida) la primera técnica será suficiente, y es bastante sencilla:

private static object JavascriptDeserialize(Type type, string objectData)
{
    MethodInfo methodInfo = typeof(JavaScriptSerializer).GetMethod("Deserialize");
    Type[] genericArguments = new Type[] { type };
    MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(genericArguments);
    return genericMethodInfo.Invoke((new JavaScriptSerializer()), new object[] { objectData });
}

Ahora que tenemos un método para deserializar JSON forzando el resultado a un tipo específico, todo es mucho más fácil. Puse los comentarios en el código:

[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public MethodExecuteResult Invoke(string typeName, string objectData, string methodName, string methodParams)
{
    //obtiene el tipo del objeto a invocar a partir de typeName
    Type type = Type.GetType(typeName);

    //obtiene la información del método a invocar a partir de methodName
    MethodInfo methodInfo = type.GetMethod(methodName);
    //obtiene la información de los parámetros del método a invocar.
    ParameterInfo[] methodParamsInfo = methodInfo.GetParameters();

    //si el método a invocar tiene parámetros obtiene los valores
    //a partir de methodParams, que es un array de strings con un parámetro serializado en json por posición.
    object[] paramValues;
    if (methodParamsInfo.Length > 0)
    {
        string[] paramValuesRaw = (string[])InvokerService.JavascriptDeserialize(typeof(string[]), methodParams);
        paramValues = new object[methodParamsInfo.Length];
        for (int paramIndex = 0; paramIndex < methodParamsInfo.Length; paramIndex++)
            paramValues[paramIndex] = InvokerService.JavascriptDeserialize(methodParamsInfo[paramIndex].ParameterType, paramValuesRaw[paramIndex]);
    }
    else
        paramValues = new object[] { };

    //obtiene una instancia del objeto a invocar a partir de objectData.
    object targetObject = InvokerService.JavascriptDeserialize(type, objectData);

    //invoca al método y obtiene el resultado.
    BindingFlags b = BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public;
    object methodReturnValue = type.InvokeMember(methodName, b, null, targetObject, paramValues);

    //crea el valor de retorno incluyendo el resultado de la invocación del método
    //y el objeto sobre el que se invocó serializado.
    return new MethodExecuteResult() { ReturnValue = methodReturnValue, TargetObject = targetObject };
}

Y listo, ya tenemos nuestro servicio de ejecución on the fly. Ahora sólo queda lo más fácil, hacer el proxy. Seguimos en la próxima.


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.: