Mostrando las entradas con la etiqueta WebMethods. Mostrar todas las entradas
Mostrando las entradas con la etiqueta WebMethods. Mostrar todas las entradas

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.

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.

viernes, 31 de julio de 2009

Ajax: C# .Net 3.5 + jQuery (IV) – Importando un proxy javascript.

Ayer ya teníamos armada una infraestructura que nos permite invocar los web methods de una página .aspx desde el código javascript de esa misma página fácilmente y sin necesidad de código adicional o mantenimiento.

Esto es útil en el caso –más común- de los web methods que sirven a las funcionalidades de una página en particular. Hay, sin embargo, otros web methods que implementan funcionalidades compartidas por varias páginas y que por ello no se ubican en una u otra sino en forma separada, en web services (.asmx).

Una pequeña digresión: se habrán dado cuenta de que una página web y un servicio son muy parecidos. Esto es porque básicamente son la misma cosa: HttpHandlers, clases que responden a una petición HTTP. Sólo tienen sutiles diferencias en la implementación. Sigamos.

El código que ubicamos en la página base (en mi ejemplo la clase PageBase) incluye en el html el proxy javascript (a través de nuestra clase JSProxy) para las llamadas a los web methods de cada página:

using System;
using System.Text;
using System.Web.UI;
using System.Web.UI.HtmlControls;

namespace AjaxConjQueryJSProxy
{
    public abstract class PageBase : Page
    {
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);

            //crea el proxy para ESTA página.
            this.AddHeaderScriptInclude("jquery-1.3.2.js");
            this.AddHeaderScriptInclude("json2.js");
            this.AddHeaderScriptInclude("JSProxy.js");

            StringBuilder javascriptProxyCode = JSProxy.Create(this);
            this.AddHeaderScript(javascriptProxyCode.ToString());
        }
(etcétera)

Por ello la función JSProxy.Create tomaba como argumento una página en particular representada por una instancia de la clase Page.

Pensemos un poco. ¿Cual sería la diferencia entre crear el proxy para una página y un servicio?

  • Para empezar, en una página los web methods son métodos estáticos, mientras que en un servicio web no.
  • Del lado de javascript, la función en no debería estar ubicada en el espacio de nombres “Page” sino en otro. Es decir que en vez de codificar “Page.Sum(3,4);” deberíamos hacer (en realidad es cuestión de gustos, pero debería ser en uno distinto de Page) “Page.[nombre del servicio].Sum(3,4);”. Esto tiene dos razones: la primera y mas obvia es que podría darse el caso de que el método del servicio a importar coincidiera con el nombre de alguno de la página, y la segunda es un leve TOC que sufrimos casi todos los programadores.

¿Y cuál es la diferencia entre el crear el proxy para métodos ubicados de la misma página y métodos ubicados en otra?

  • Sólo el espacio de nombres en el que ubicamos las funciones proxy de javascript. Si tenemos que llamar a un método de la misma página hacemos Page.Sum(3,4), si está ubicado en otra deberíamos hacer Page.MiOtraPagina.Sum(3,4).

Así que necesitamos dos variaciones adicionales que modifican ligeramente el método JSProxy.Create. Así que vamos a hacer lo siguiente. Renombremos al método “JSProxy.Create” como “JSProxy.CreateInternal” y convirtámoslo en privado de JSProxy. Luego crearemos los métodos públicos necesarios para que “desde afuera” todo siga igual. JSProxy.Create quedará así:

private static StringBuilder CreateInternal(string targetNamespace, Type type, string url, string ajaxNamespace, SearchMembers searchMembers)

El método toma ahora cinco parámetros:

targetNamespace: es el espacio de nombres donde se ubicarán las funciones creadas en javascript. Si le pasamos “Page” las funciones estarán definidas como “Page.Sum”, “Page.HelloWorld”, etc. Si le pasamos “Page.Servicio1” las funciones estarán definidas “Page.Servicio1.Sum”, “Page.Servicio1.HelloWorld”, etc.

type: es un objeto Type que representa la clase para la cual queremos crear un proxy.

url: es la url (la dirección de la página o del servicio) en donde se encuentran los métodos. ¿Recuerdan que la url de un método es “/Default1.aspx/Sum” o “/WebService1.asmx/HelloWorld”? La primera parte “/Default1.aspx” o “/WebService1.asmx” no puede deducirse del Type. Esto es porque el Type describe qué es el objeto, y no dónde o a través de qué dirección responde a las llamadas.

ajaxNamespace: es el espacio de nombres en donde se encuentra la función compartida de javascript “Call”. En el ejemplo que estamos siguiendo es siempre “Page.Ajax” (porque hacemos siempre “Page.Ajax.Call(…);” ), pero decidí que ya que tenía que modificar la función iba a introducir este cambio principalmente porque no me gustó mucho cómo quedaron armados los espacios de nombres y pienso modificarlos en algún momento.

searchMembers: indicará si hay que crear proxies para los web methods estáticos (en una página), de instancia (los de un servicio) o de todos (un handler o cualquier otra cosa), y si bien no es estrictamente necesario (podríamos simplemente generar proxies para todos los métodos marcados con WebMethodAttribute) sirve para acelerar un poco la recorrida.

La funcionalidad queda codificada así:

private static StringBuilder CreateInternal(string targetNamespace, Type type, string url, string ajaxNamespace, SearchMembers searchMembers)
{
    BindingFlags bindingFlags = BindingFlags.FlattenHierarchy | BindingFlags.Public;
    switch (searchMembers)
    {
        case SearchMembers.Instance:
            bindingFlags = bindingFlags | BindingFlags.Instance;
            break;

        case SearchMembers.Static:
            bindingFlags = bindingFlags | BindingFlags.Static;
            break;
    }
    MethodInfo[] methods = type.GetMethods(bindingFlags);

    //50 caracteres para cada método es una estimación, claro.
    StringBuilder builder = new StringBuilder(50 * methods.Length);
    builder.Append("$().ready( function() {");

    builder.AppendFormat("{0}.Namespace(\"{1}\");", ajaxNamespace, targetNamespace);

    foreach (MethodInfo publicStaticMethod in methods)
    {
        if (publicStaticMethod.GetCustomAttributes(typeof(WebMethodAttribute), true).Length == 0)
            continue;

        builder.AppendFormat("{0}.{1} = function(", targetNamespace, publicStaticMethod.Name);

        ParameterInfo[] parameters = publicStaticMethod.GetParameters();

        if (parameters.Length > 0)
        {
            foreach (ParameterInfo parameter in parameters)
                builder.AppendFormat("{0},", parameter.Name);

            builder.Remove(builder.Length - 1, 1);
        }

        builder.Append("){");
        builder.AppendFormat("return {0}.Call(\"{1}/{2}\"", ajaxNamespace, url, publicStaticMethod.Name);

        if (parameters.Length > 0)
        {
            builder.Append(",{");
            foreach (ParameterInfo parameter in parameters)
                builder.AppendFormat("{0}:{0},", parameter.Name);

            builder.Remove(builder.Length - 1, 1);
            builder.Append("});");
        }
        else
            builder.AppendFormat(");");

        builder.Append("};");
    }

    builder.Append("});");
    return builder;
}

Y ahora creamos el método público Create para que todo siga igual “hacia afuera”… y lo renombramos a CreateForPage (me gusta más,  sólo eso):

public static StringBuilder CreateForPage(Page page)
{
    return JSProxy.CreateInternal("Page", page.GetType(), page.ResolveUrl(page.AppRelativeVirtualPath), "Page.Ajax", SearchMembers.Static);
}

Nota: no estoy muy seguro de ese “page.ResolveUrl(page.AppRelativeVirtualPath)”, están avisados.

Una función para importar métodos web de un servicio:

public static StringBuilder ImportService(Type type, string url)
{
    if (!type.IsSubclassOf(typeof(WebService)))
        throw new ArgumentException("El tipo a importar debe representar un servicio web (debe heredar de WebService).", "type");

    string targetNamespace = string.Concat("Page.", type.Name);
    return JSProxy.CreateInternal(targetNamespace, type, url, "Page.Ajax", SearchMembers.Instance);
}

Y otra para importar métodos web de otra página:

public static StringBuilder ImportPage(Type type, string url)
{
    if (!type.IsSubclassOf(typeof(Page)))
        throw new ArgumentException("El tipo a importar debe representar una página (debe heredar de Page).", "type");

    string targetNamespace = string.Concat("Page.", type.Name);
    return JSProxy.CreateInternal(targetNamespace, type, url, "Page.Ajax", SearchMembers.Static);
}

Resta un pequeño detalle… esos “Namespaces” en javascript (“Page.Webservice1”, “Page.Ajax”) no se van a crear solos mágicamente. Si revisan un poco el código de arriba verán que en la línea 20 de CreateInternal estoy generando código javascript que invoca a una función llamada “Namespace” que debe estar ubicada junto a “Call”. Este lugar para nosotros es Page.Ajax, así que falta el código de Page.Ajax.Namespace( fullns ), que podemos ubicar en el ejemplo en el archivo JSProxy.js.

Lo que hace esta función es justamente crear el espacio de nombres que se le pasa. Si queremos que esté disponible el espacio “Page.Webservice1” la llamada será

Page.Ajax.Namespace(“Page.Webservice1”);

En javascript no existen los espacios de nombres como en c#. Lo que sea hace es… una metida de dedos, un truquito. Por ejemplo, para crear “Page.Webservice1” hacemos

var Page = new Object();
Page.Webservice1 = new Object();

así que la función Namespace queda codificada así:

Page.Ajax.Namespace = function(fullns) {
    var ns = fullns.split(".");
    eval("var x=" + ns[0]+";");
    if (typeof (x) == "undefined")
        eval(ns[0] + " = new Object(); ");

    if (ns.length < 2)
        return;

    var nsAcum = ns[0];
    for (var i = 1; i < ns.length; i++) {
        eval("var x=" + nsAcum + "." + ns[i] + ";");
        if (typeof (x) == "undefined")
            eval(nsAcum + "." + ns[i] + " = new Object(); ");

        nsAcum += "." + ns[i]
    }
}

Nota: estoy seguro de que hay formas más elegantes de hacer eso. No soy un codificador muy “ingenioso”, ésta funciona y punto.

Bueno, ya podemos probar. Agreguemos un servicio web a nuestro proyecto y agreguemos un método “HelloWorld” que devuelva “hola mundo” (¡no olviden descomentar el atributo ScriptService en la clase y de agregar el atributo ScriptMethod a HelloWorld! Acabo de perder media hora dando vueltas con eso).

using System.ComponentModel;
using System.Web.Script.Services;
using System.Web.Services;

namespace AjaxConjQueryJSProxy
{
    /// 
    /// Summary description for WebService1
    /// 
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ToolboxItem(false)]
    // To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. 
    [System.Web.Script.Services.ScriptService]
    public class WebService1 : System.Web.Services.WebService
    {
        [WebMethod]
        [ScriptMethod(ResponseFormat= ResponseFormat.Json)]
        public string HelloWorld()
        {
            return "Hello World";
        }
    }
}

Ahora vamos a hacer que se pueda llamar a ese servicio desde el javascript de una página. Pongamos por caso “Default.aspx”. Como esto es específico de cada página, es razonable que el código para incluir el proxy esté en cada página que lo requiera. En este caso debemos agregar:

protected override void OnPreLoad(EventArgs e)
{
    string proxyWebService1 = JSProxy.ImportService(typeof(WebService1), "/WebService1.asmx").ToString();

    HtmlGenericControl script = new HtmlGenericControl("script");
    script.Attributes.Add("type", "text/javascript");
    script.InnerHtml = proxyWebService1;
    this.Header.Controls.Add(script);

    base.OnPreLoad(e);
}

Y sin hacer nada más tenemos disponible la llamada a HelloWorld de WebService1 en el javascript de Default.aspx haciendo:

$().ready(function() {
    alert(Page.WebService1.HelloWorld());
});

La solución completa está en Google Code.

Actualización: en la próxima entrada llevamos las cosas al extremo prototipando proxies en javascript de objetos c#.


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.

jueves, 30 de julio de 2009

Ajax: C# .Net 3.5 + jQuery (III) – Automatizando la creación de proxies de javascript.

En la entrada anterior terminamos creando proxies en javascript para invocar en una forma sencilla y natural desde el cliente a los métodos del lado del servidor (aquellos marcados con el atributo WebMethod en el código de una página .aspx).

Recapitulando, habíamos codificado la función

Page.Ajax.Call(methodName, data);

que recibe el nombre del método y un objeto con los parámetros correspondientes y luego, para cada WebMethod, creábamos un pequeño proxy en javascript con la forma

Page.Suma = function(a, b)
{
    return Page.Ajax.Call( "Sum", {a:a,b:b} );
}

que nos permite, a la hora de codificar una funcionalidad específica, hacer la llamada en una sola línea y con una sintaxis más natural. Siguiendo con el ejemplo:

var s = Page.Suma(3,5);

¿Todo ese código para hacer una llamada a un WebMethod? Está bien, es más bonito, pero en la vida real hay varios métodos para cada página -a veces realmente muchos- y es una molestia tener que estar codificando y manteniendo estas funciones triviales. En general, cuando uno va avanzando sobre algo nuevo va cambiando constantemente las declaraciones de los métodos a medida que se aproxima a la solución, probando, corrigiendo… y para cada cambio, por menor que sea, hay que acordarse de modificar esa función.

Es, por otro lado, una estructura más difícil de captar para quien se acerca por primera vez al código de un sistema. Si las personas cambian o si el trabajo sobre un proyecto en particular es esporádico, estas “convenciones” suelen diluirse, tomar diferentes variaciones dependiendo de quién las codifique, modificándose un poco, llenándose de errores y siendo más molestas que útiles…

…a menos que creemos una infraestructura que las actualice automáticamente. La idea es crear una clase abstracta que descienda de Page y que sea base de todas las páginas de nuestro sitio, e implementar en esta clase los métodos necesarios para generar el proxy automáticamente.

Este método debe utilizar Reflection para recorrer los métodos estáticos de la clase en busca de aquellos marcados con WebMethod y generar los proxies examinando su estructura.

Antes que nada vamos a hacer una pequeña modificación a Page.Ajax.Call que nos permita codificarla en un archivo .js aparte y que sea el mismo para todas las páginas, para que haya que generar menos código automáticamente. Es cuestión de agregar un parámetro con la url del método en vez de codificarla en el cuerpo. Queda:

Page.Ajax.Call = function(url, data) {
    var returnValue = null;

    if (typeof (data) == "undefined" || data == null)
        data = "{}";

    else
        data = Page.Utils.JSON.stringify(data);

    $.ajax({
        type: "POST",
        data: data,
        url: url,
        contentType: "application/json; charset=utf-8",
        dataType: "text",
        async: false,
        success: function(response) { returnValue = response; },
        error: Page.Ajax.HandleError
    });

    return Page.Utils.JSON.parse(returnValue).d;
}

y ya la podemos mover, junto con la demás funciones de soporte, a un archivo aparte (en mi solución es el archivo JSProxy.js).

Vamos al centro del problema. La clase System.Type describe una clase (tipo o type en inglés). Para cualquier objeto podemos obtener el Type que describe la clase de la que es instancia mediante el método GetType. Por otro lado, el namespace System.Reflection contiene clases y métodos que ayudan a trabajar esa metadata.

Agregamos al proyecto la clase estática de C# JSProxy, que tendrá un sólo método definido como:

public static StringBuilder Create(Page page)

Es decir, recibe una página y devuelve en un StringBuilder el código javascript que crea las funciones proxy que mencionábamos arriba (la función Page.Suma, por ejemplo).

El código es relativamente sencillo una vez que nos familiarizamos con las clases y métodos de System.Reflection:

  • Utilizamos page.GetType() para obtener la descripción de la clase correspondiente a una página en particular.
  • El método GetMethods -por ejemplo page.GetType().GetMethods(…) – nos devuelve un array de objetos MethodInfo que describen los métodos estáticos.
  • Para cada método obtenemos los atributos con los que está decorado utilizando GetCustomAttributes para determinar si está presente el atributo WebMethod.
  • Y con el método GetParameters() obtenemos un array de objetos ParameterInfo que describe los parámetros de ese método.

Esa es toda la información que necesitamos para ir construyendo el javascript. El método completo es algo así:

public static StringBuilder Create(Page page)
{
    BindingFlags bindingFlags = BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Static;
    MethodInfo[] methods = page.GetType().GetMethods(bindingFlags);

    //50 caracteres para cada método es una estimación, claro.
    StringBuilder builder = new StringBuilder(50 * methods.Length);
    builder.Append("$().ready( function() {");

    foreach (MethodInfo publicStaticMethod in methods)
    {
        if (publicStaticMethod.GetCustomAttributes(typeof(WebMethodAttribute), true).Length == 0)
            continue;

        builder.AppendFormat("Page.{0} = function(", publicStaticMethod.Name);

        ParameterInfo[] parameters = publicStaticMethod.GetParameters();

        if (parameters.Length > 0)
        {
            foreach (ParameterInfo parameter in parameters)
                builder.AppendFormat("{0},", parameter.Name);

            builder.Remove(builder.Length - 1, 1);
        }

        builder.Append("){");

        string url = page.ResolveUrl(page.AppRelativeVirtualPath);
        builder.AppendFormat("return Ajax.Call(\"{0}/{1}\"", url, publicStaticMethod.Name);

        if (parameters.Length > 0)
        {
            builder.Append(",{");
            foreach (ParameterInfo parameter in parameters)
                builder.AppendFormat("{0}:{0},", parameter.Name);

            builder.Remove(builder.Length - 1, 1);
            builder.Append("});");
        }
        else
            builder.AppendFormat(");");

        builder.Append("};");
    }

    builder.Append("});");
    return builder;
}

Bien, ya tenemos todo lo necesario para nuestra página base:

namespace AjaxConjQueryJSProxy
{
    public abstract class PageBase : Page
    {
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);

            //crea el proxy para ESTA página.
            this.AddHeaderScriptInclude("jquery-1.3.2.js");
            this.AddHeaderScriptInclude("json2.js");
            this.AddHeaderScriptInclude("JSProxy.js");

            StringBuilder javascriptProxyCode = JSProxy.Create(this);
            this.AddHeaderScript(javascriptProxyCode.ToString()); 
        }

        private void AddHeaderScriptInclude(string src)
        {
            HtmlGenericControl script = new HtmlGenericControl("script");
            script.Attributes.Add("type", "text/javascript");
            script.Attributes.Add("src", src);
            this.Header.Controls.Add(script);
        }

        private void AddHeaderScript(string code)
        {
            HtmlGenericControl script = new HtmlGenericControl("script");
            script.Attributes.Add("type", "text/javascript");
            script.InnerHtml = code;
            this.Header.Controls.Add(script);
        }



    }
}

Noten que no solamente incluí el código para generar el proxy sino que también el que incluye los javascripts necesarios (los de jQuery, JSON y el que contiene Page.Ajax.Call).

Ahora vamos a probarlo. Supongamos que tenemos que implementar una nueva pantalla. Creamos la nueva página y cambiamos su declaración para que herede de PageBase:

public partial class _Default : PageBase

Agregamos un método web:

[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public static int Sum(int a, int b)
{
    return a + b;
}

y ahora vamos al javascript y lo invocamos:

<script type="text/javascript">      
function TestSum()
{
        alert("2+5="+Page.Sum(2,5));
}
    
$().ready(function() {
	TestSum();
});                           
</script>

Si lo modificamos tenemos que… ¡no hay que hacer nada! Cualquier modificación en los métodos de la página es automáticamente pasada a javascript gracias a la infraestructura que hemos creado.

Ya no hay excusas para hacer un postback, mucho menos para utilizar UpdatePanels, ViewState y ese tipo de cosas… bueno, no exageremos, sigue faltando mucho camino para recorrer.

El código de este walkthrough está en Google Code aunque no es exactamente igual, tiene algunas modificaciones que planeo comentar en los próximos posts (la idea es la misma).

Actualización: en la cuarta entrega vemos una pequeña variación para crear proxies a servicios e importar el proxy de una página en otra: Ajax: C# .Net 3.5 + jQuery (IV) – Importando un proxy javascript.


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.

miércoles, 29 de julio de 2009

Ajax: C# .Net 3.5 + jQuery (II) – Resolviendo el problema de la serialización de fechas en JSON.

En la entrada anterior experimentaba la comunicación entre cliente (jQuery) y servidor (ASP .Net 3.5) mediante JSON. Más específicamente, entre una página web (html plano) y una clase que hereda de WebService, utilizando dos serializadores: System.Runtime.Serialization.Json.DataContractJsonSerializer y System.Web.Script.Serialization.JavaScriptSerializer.

Los web services son buenos para implementar funcionalidad consumida desde diferentes páginas (.aspx). Pero en una aplicación de gestión la gran mayoría de los métodos son específicos de una sola página, ya que están fuertemente vinculados con la interacción que esa página propone.

Estaba a punto de crear una enorme infraestructura para vincular servicios web (elementos .asmx) con páginas  (.aspx) en una relación “uno a uno” (repasen el post anterior) cuando fui amablemente desasnado por Martín (gracias), más avezado en .Net 3.5.

Cuestión que me entero de que podemos incluir un método web en cualquier página. Vamos a hacer una prueba (todo el código de este ejemplo está subido a Google Code).

  • Vamos al VS 2008, creamos un nuevo proyecto web y en el código c# de default.aspx incluimos:
using System;
using System.Web.Script.Services;
using System.Web.Services;
using System.Web.UI;

namespace AjaxConjQueryJSProxy
{
    public partial class _Default : Page
    {
        [WebMethod]
        [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
        public static string HelloWorld()
        {
            return "HelloWorld";
        }

        [WebMethod]
        [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
        public static int Sum(int a, int b)
        {
            return a + b;
        }

        [WebMethod]
        [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
        public static DateTime AddMonths(DateTime date, int months)
        {
            return date.AddMonths(months);
        }

    }
}

¡Tan simple como hacer los métodos estáticos y marcarlos con el atributo System.Web.Services.WebMethodAttribute!

  • Ahora agreguemos el javascript de jQuery al proyecto y la respectiva referencia en el <head> de default.aspx:
<head runat="server">
    <title>Untitled Page</title>
    <script type="text/javascript" src="jquery-1.3.2.js"></script>
</head>
  • Una llamada al método “Sum” se vería, desde javascript, así:
function TestSum()
{
    $.ajax({
        type: "POST",
        data: '{"a":"3","b":"5"}',
        url: "/Default.aspx/Sum",
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        async: false,
        success: function (response){ alert(response.d); },
        error: function(){alert("error");}
    });          
}

Si la invocamos veremos un alert con un “8” que es, casualmente, la suma de 3+5. Hay dos novedades, si es que vienen del primer post de esta serie: la primera es la url, que tiene la forma [Nombre de la página]/[Nombre del método] (“/Default.aspx/Sum”) y la segunda es que en este caso pasamos dos parámetros. Si ven bien la opción “data” verán que se los pasamos como una cadena con la representación en notación JSON de un objeto (NO es lo mismo que un objeto, es sólo su representación en JSON) con una propiedad correspondiente a cada parámetro del método.

Como pasamos los parámetros como cadenas en JSON necesitamos una función que tome los objetos reales y cree esa cadena. Como en todo algoritmo que maneje datos ingresados por el usuario, lo mejor (creo yo) es confiar en código ampliamente distribuido antes de hacerlo uno mismo. Créanme que se van a ahorrar más de un dolor de cabeza. Estas funciones tienen que lidiar con caracteres de escape y código malicioso ingresado por algún vivillio. Recuerden quién está al tope de los 25 errores de programación más peligrosos.

  • Así que vamos a agregar el código javascript mantenido por JSON.org de dos funciones: JSON.parse (converte una cadena JSON en un objeto) y JSON.stringify (convierte un objeto en una cadena JSON). Lo bajamos, lo agregamos al proyecto, y ahora el <head> de default.aspx debe verse así:
<head runat="server">
    <title>Untitled Page</title>
    <script type="text/javascript" src="jquery-1.3.2.js"></script>
    <script type="text/javascript" src="json2.js"></script>
</head>
  • Bien, vamos a probar los otros métodos, pero antes… soy un tipo vago, muy vago. Y eso es bueno, porque me da tanta fiaca escribir código que tiendo a la reutilización. Ya que todos los métodos de la página van a ser sincrónicos y todos apuntan a la misma url, podríamos…
var Page = new Object();                   
Page.Ajax = new Object();        
Page.Ajax.HandleError = function(xhr, textStatus, errorThrown)
{
    var error = JSON.parse(xhr.responseText);
    throw error;
}
Page.Ajax.Call = function(methodName, data)
{
    var returnValue = null;
    
    if(typeof(data) == "undefined" || data == null )
        data = "{}";
    
    else 
        data = JSON.stringify(data);
    
    $.ajax({
        type: "POST",
        data: data,
        url: "/Default.aspx/" + methodName,
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        async: false,
        success: function (response){ returnValue = response.d; },
        error: Page.Ajax.HandleError
    });  
    
    return returnValue;
}

El código anterior corresponde a la función Page.Ajax.Call(methodName, data) (me gustan los namespaces en javascript) a la que le pasamos el nombre del método y los parámetros si es que los tiene. Podemos pasarlos como cadenas o como objetos. En este segundo caso la función se encarga de pasarlos a cadenas usando JSON.stringify.

Por otro lado esta función encapsula el manejo de errores que como verán es muy rudimentario, pero que podremos ir mejorando luego. La idea es que si hay una excepción del lado del servidor ésta se transforme en una excepción de javascript en forma transparente, creándose una cadena de excepciones continua desde el servidor hacia el cliente hasta llegar a la primera función que desencadena toda la pila de llamadas (veremos cómo queda al final).

  • Un paso más… podríamos hacer un “proxy” en javascript para cada método:
Page.HelloWorld = function()
{           
    return Page.Ajax.Call("HelloWorld");
}

Page.Suma = function(a, b)
{
    return Page.Ajax.Call( "Sum", {a:a,b:b} );
}

Page.AddMonths = function(date, months)
{
    return Page.Ajax.Call( "AddMonths", {date:date,months:months} );
}
  • Esto nos proporciona un acceso mucho más natural a los métodos del lado del servidor. Hagamos la prueba:
function TestHelloWorld()
{                
    try
    {
        alert(Page.HelloWorld());
    }
    catch(ex)
    {
        alert(ex);
        alert("Test error: " + ex.ExceptionType + "\n" + ex.Message);
    } 
}  

function TestSum()
{
    try
    {
        alert("2+5="+Page.Suma(2,5));
        alert("3-2="+Page.Suma(3,-2));
        alert("Next call should raise an exception: Sum(2.4,5)");
        alert("2.4+5="+Page.Suma(2.4,5));
    }
    catch(ex)
    {
        alert("Test error: " + ex.ExceptionType + "\n" + ex.Message);
    }             
}

function AddMonths()
{
    try
    {
        alert("3 Months from now="+Page.AddMonths( new Date(), 3).toString() );
        alert("2 Months ago="+Page.AddMonths( new Date(),-2).toString() );
    }
    catch(ex)
    {
        alert("Test error: " + ex.ExceptionType + "\n" + ex.Message);
    }                       
}

$().ready( function(){ 
    TestHelloWorld();
    TestSum(); 
    AddMonths();
});

Como verán, a la hora de programar la funcionalidad real, pongamos por ejemplo la suma, sólo tengo que hacer:

var s = Page.Suma(3,5);

…y del resto puedo olvidarme. Si hay errores en la infraestructura su corrección no afectará a la funcionalidad y viceversa. Por ejemplo… notarán un “pequeño” problema con las fechas. En realidad no es un “pequeño” sino un “enorme” problema con las fechas.

El problema es que JSON no define un formato estándar para fechas. Así que se suelen presentar diferencias de implementación. Sin ir más lejos, ASP.Net, cuando devuelve la respuesta del servidor, serializa las fechas en JSON utilizando la forma "/Date(1198908717056)/". Cuando jQuery intenta transformar de vuelta la cadena en objeto simplemente no entiende esa fecha y la interpreta como un string.

Problemas de delegar el control de una interfaz (entre cliente y servidor, ¿se acuerdan que lo había mencionado?) a una herramienta que no controlamos. Para no ser duros con ASP.Net y jQuery por el malentendido, digamos desde el vamos que es solucionable. Pero (por lo menos para mí) fue muy, MUY difícil.

Lo que me gustaría enfatizar, de todas maneras, es que es importante arreglarlo donde corresponde: adentro de Page.Ajax.Call, y no mirar para otro lado y dejar que cada programador se enfrente al problema de convertir las cadenas en fechas por sí mismo. Lo que sucedería si hiciésemos eso (y no por culpa de cada programador en particular) es que cada uno “haría la suya” y muchas de esas conversiones tendrían pequeños y sutiles problemas listos para explotar en el peor momento.

Mi “workaround” comenzó por interiorizarme un poco del problema, aquí. La solución, entonces, es interceptar la respuesta del servidor y transformar las cadenas "/Date(1198908717056)/" en algo que jQuery interprete como fecha.

Luego de pelearme un buen rato desistí de encontrar cómo es que jQuery necesita las fechas para entenderlas como tales. Por suerte las funciones JSON.parse y JSON.stringify permiten pasar una función para realizar conversiones especiales en uno y otro sentido que salva la vida en estos casos.

  • Así que primero encapsulamos la librería de JSON dentro de Page e implementamos nuestra propia deserialización de fechas:
Page.Utils = new Object();
Page.Utils.JSON = new Object();    
Page.Utils.JSON.stringify = function(obj)
{
    return JSON.stringify(obj);
}
Page.Utils.JSON.parse = function(text)
{
    return JSON.parse(text, function (key, value)
    {
        if (typeof value === 'string')
        {
            var found = /^\/Date\((\d+)\)\/$/.exec(value);
            if(found != null)            
                return new Date(parseInt(found[1],10));                
        }        

        return value;                
    });    
}
  • Y ahora le decimos a jQuery que queremos la respuesta del servidor como texto plano, y la parseamos utilizando Page.Utils.JSON.parse. Esto lo podemos hacer en Page.Ajax.Call, cambiando dataType de “json” a “text” y parseando al final. Queda:
Page.Ajax.Call = function(methodName, data)
{
    var returnValue = null;
    
    if(typeof(data) == "undefined" || data == null )
        data = "{}";
    
    else 
        data = Page.Utils.JSON.stringify(data);
    
    $.ajax({
        type: "POST",
        data: data,
        url: "/Default.aspx/" + methodName,
        contentType: "application/json; charset=utf-8",
        dataType: "text",
        async: false,
        success: function (response){ returnValue = response; },
        error: Page.Ajax.HandleError
    });  
    
    return Page.Utils.JSON.parse(returnValue).d;
}

There I fixed It! Un poco de alambre, pero todo queda tras bambalinas. Hay tres claves para el éxito en el desarrollo de software: encapsulamiento, encapsulamiento y encapsulamiento. No importa qué tan grave sea un problema o qué tan mal hayamos implementado una función, un procedimiento o una tecnología, el encapsulamiento hace que las explosiones queden contenidas y que podamos reemplazar o mejorar las partes por separado.

Sin ir más lejos es probable que éstos, mis primeros pasos en jQuery y en el .Net 3.5 estén viciados de errores y omisiones. Pero estoy seguro de que a medida que surjan los problemas se podrán ir arreglando sin que eso afecte a la funcionalidad codificada sobre esta estructura.

Les recuerdo que la solución de VS 2008 completa de este walkthrough está en google code.

Falta mucho camino todavía…

Un paso más en la batalla: Ajax: C# .Net 3.5 + jQuery (III) – Automatizando la creación de proxies de javascript.


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.

martes, 28 de julio de 2009

Ajax: C# .Net 3.5 + jQuery (I?)

Como mencionaba tangencialmente el otro día, estoy haciendo mis primeras armas en el mundo real con jQuery y .Net 3.5. El mundo real impone sus tiempos y necesidades, así que mis primeros pasos van en una dirección muy concreta: armar una infraestructura para el manejo de la comunicación entre cliente y servidor con llamadas Ajax.

¿Por qué? Para más detalle los remito a La amansadora .Net, aunque resumo: hacer aplicaciones complejas utilizando el modelo propuesto por Microsoft (hasta ahora, que han reaccionado y están cambiando el rumbo), postbacks, viewstate, eventos del lado del servidor… es un dolor de cabeza, sobre todo en el mantenimiento. Simplemente por eso.

La infraestructura, en principio, es más o menos como indica el título: Vistual Studio 2008, C# y .Net 3.5 del lado del servidor y jQuery del lado del cliente.

La idea es que sea lo más simple y transparente posible de utilizar. Hay un mundo de posibilidades con el que uno no quiere lidiar cuando está implementando una funcionalidad concreta y tiene otros problemas en la cabeza.

“Lo más simple posible” puede llegar a ser muy complicado. Comencemos por los requerimientos: hacer una llamada desde javascript a un método de una clase y obtener un resultado. Lo que me imaginé (acepto opiniones, no tengo nada en firme todavía) es algo así:

Supongamos que estoy programando una página web en la que el usuario está buscando un cliente. Para simplificar, digamos que pone el código y la página le muestra Apellido, Nombre y Teléfono.

El código HTML que debería escribir el que implementa esta funcionalidad debería ser algo así:

<form id="form1" runat="server">
<div>
<p>
Código del cliente:
<input type="text" id="txtClienteCodigo" />
<input type="text" id="btnBuscar" onclick="BuscarCliente()" type="button" />
</p>
<p>
<label>
    Nombre:</label><input type="text" id="txtClienteNombre" />
</p>
<p>
<label>
    Apellido:</label><input type="text" id="txtClienteApellido" /></p>
<p>
<label>
    Teléfono:</label><input type="text" id="txtClienteTelefono" /></p>
</div>
</form>

y el código javascript algo así:

function BuscarCliente()
{    
    try
    {                  
        var codigo = $("#txtClienteCodigo").val();                                 
        Page.ClienteBuscar(codigo, MostrarCliente);
    }
    catch(ex)
    {
        Page.MostrarError(ex);
    }                    
}

function MostrarCliente(cliente)
{
    if(clienteDatos == null)
    {
        $("#txtClienteNombre").val("");
        $("#txtClienteApellido").val("");
        $("#txtClienteTelefono").val("");            
        alert("No se ha encontrado el cliente solicitado.");
    }
    else
    {
        $("#txtClienteNombre").val(clienteDatos.Nombre);
        $("#txtClienteApellido").val(clienteDatos.Apellido);
        $("#txtClienteTelefono").val(clienteDatos.Telefono);
    };                              
}

…bueno, más o menos. Es un boceto.

Pero creo que la idea se entiende: funcionalidad simple, código simple. Pido un valor, llamo a una función, me devuelve el resultado, lo muestro. El resto tiene que estar manejado por la infraestructura del sistema.

En la línea 5 del javascript se obtiene el código del cliente desde un input type=text con jQuery. Noten que el id del control es conocido, no quiero lidiar con cosas del tipo <%=txtClienteNombre.ClientID%> metidas en medio del código (por ejemplo…).

En la línea 6 está la llamada Ajax: Page.ClienteBuscar es un método del lado del servidor. Al método se le pasan los parámetros definidos de acuerdo a su declaración en C#, más uno adicional (MostrarCliente) que es la función que continuará la ejecución (definida a partir de la línea 14) ya que es una llamada asincrónica.

Si hay un error en la infraestructura previo a la llamada al servidor se arroja una excepción con información suficiente para mostrar al usuario (la variable ex del catch en la línea 8) a través de un método (Page.MostrarError, línea 10) que también es parte de la infraestructura.

Si no hay problemas en la llamada los datos son devueltos a MostrarDatosCliente (línea 14) que simplemente los muestra. Noten que aquí no hay manejo de errores: como la llamada es asincrónica, cualquier excepción tiene que ser manejada (mostrada) por la infraestructura.

Bien, ésa es la utopía. Como toda utopía empieza con un “Hello World”.

Comencé buscando algo de información sobre comunicación entre cliente y servidor en .Net utilizando JSON y encontré Prepare a JSON Web Service and access it with JQuery, un buen artículo de Sohel Rana en CodeProject.

Mi “Hello World” es muy parecido, apenas un resumen de aquél, que les recomiendo leer entero.

La idea es hacer una llamada a un Web Service mediante el método $.ajax de jQuery y recibir como respuesta una cadena que es la serialización en JSON de un objeto creado del lado del servidor. El artículo presenta dos métodos de serialización: utilizando System.Runtime.Serialization.Json.DataContractJsonSerializer y System.Web.Script.Serialization.JavaScriptSerializer, y vamos a probar los dos.

Subí la solución resultante de este walkthrough a un repositorio en Google Code, así que la pueden descargar completa por aquí.

Vamos paso a paso:

  • En el VS 2008, abrimos creamos un proyecto de tipo “ASP .Net Web Application”. Yo lo llamé “AjaxConjQuery”.
  • Agreguemos una nueva clase llamada "Test1”. En el primer “Hello World” devolveremos una instancia serializada de esta clase con System.Runtime.Serialization.Json.DataContractJsonSerializer. El código será:
using System.Runtime.Serialization;

[DataContract]
public class Test1
{
    [DataMember]
    public string Text {get; set;}

    public Test1()
    {
        this.Text = "Hello World";
    }
}

Noten que la clase está decorada con el atributo “DataContract” y la propiedad Text con “DataMember”, y que es de lectura/escritura. DataContractJsonSerializer sólo trabajará con las clases y propiedades que estén marcadas con estos atributos y que sean de lectura/escritura.

  • Agreguemos una nueva clase llamada “Test”. En el segundo “Hello World” la devolveremos serializada con JavaScriptSerializer.
public class Test2
{
    public string Text
    {
        get { return "Hello World"; }
    }
}

JavaScriptSerializer serializa todas las propiedades públicas, por lo que no tenemos que marcar nada ni preocuparnos por el método "set".

  • Bien, ahora agregamos un nuevo elemento de tipo “Web Service”:
using System.ComponentModel;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Web.Script.Serialization;
using System.Web.Script.Services;
using System.Web.Services;

namespace AjaxConjQuery
{
    /// 
    /// Summary description for WebService1
    /// 
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ToolboxItem(false)]
    [ScriptService]
    public class WebService1 : WebService
    {
        [WebMethod]
        [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
        public string HelloWorld1()
        {
            DataContractJsonSerializer serializer = new
                DataContractJsonSerializer(typeof(Test1));

            using (MemoryStream ms = new MemoryStream())
            {
                serializer.WriteObject(ms, new Test1());
                string jsonString = Encoding.Default.GetString(ms.ToArray());
                return jsonString;
            }
        }

        [WebMethod]
        [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
        public string HelloWorld2()
        {
            JavaScriptSerializer j = new JavaScriptSerializer();
            return j.Serialize(new Test2());
        }

    }
}

Lo importante aquí es que la clase debe estar marcada con el atributo System.Web.Script.Services.ScriptService (viene comentado por defecto) y cada método con el atributo System.Web.Script.Services.ScriptMethod(ResponseFormat = ResponseFormat.Json) con el que indicamos que la respuesta será en formato JSON.

Pueden ver las diferencias en el uso de uno y otro serializador comparando HelloWorld1 y HelloWorld2.

  • Ahora es cuestión de agregar jQuery al proyecto (el archivo está aquí) y referenciarlo en la sección <head> del html de default.aspx incluyendo
<script type="text/javascript" src="jquery-1.3.2.js"></script>
  • Necesitamos dos botones, que podemos tirar en cualquier lado:
    <input type="button" id="btnHelloWorld" value="Hello World" onclick="btnHelloWorld_OnClick()" />
    <input type="button" id="btnHelloWorld2" value="Hello World2" onclick="btnHelloWorld2_OnClick()" />    
  • Y las funciones javascript correspondientes:
function btnHelloWorld_OnClick()
{       
    $.ajax({
        type: "POST",
        data: "{}",
        url: "/WebService1.asmx/HelloWorld1",
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        success: function(response){
         var d = eval('('+response.d+')'); 
         alert( d.Text );
        },
        error: function(XMLHttpRequest, textStatus, errorThrown){ 
            alert("ResponseText: " + XMLHttpRequest.responseText + "\ntextStatus:" + textStatus + "\nerrorThrown:" + errorThrown);
        }
    });
}
        
function btnHelloWorld2_OnClick()
{       
    $.ajax({
        type: "POST",
        data: "{}",
        url: "/WebService1.asmx/HelloWorld2",
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        success: function(response){ 
            var d = eval('('+response.d+')');
            alert( d.Text ); 
            },
        error: function(XMLHttpRequest, textStatus, errorThrown){ 
            alert("ResponseText: " + XMLHttpRequest.responseText + "\ntextStatus:" + textStatus + "\nerrorThrown:" + errorThrown);
        }
    });
}
  • ¡Listo! Con suerte F5, compila y funciona.

Aquí lo importante es ver la llamada a $.ajax. Los parámetros relevantes son:

  • data, en el que le pasamos los parámetros al servicio web (en este caso no tiene),
  • url, en el que se indica la dirección del servicio con la forma “[nombre de la página]/[nombre del método]”, y
  • las propiedades success y error en la que se proporcionan funciones de retorno en caso de éxito y error (ojo que aquí el documento de CodeProject indica mal el nombre de la propiedad “error”, que aparece como “failure”)

Como verán, luego de hacer un eval() de la cadena recibida (response.d) podemos acceder a la propiedad “Text” del objeto que se crea, que no es otra cosa que el equivalente en javascript a una instancia la clase Test1 o Test2 en nuestro código c#.

De aquí a mis objetivos es apenas un pasito… como de aquí a la Luna. Hay que trabajar, no queda otra.

Actualización: continúa en Ajax: C# .Net 3.5 + jQuery (II) – Resolviendo el problema de la serialización de fechas en JSON.


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.