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.

4 comentarios:

Cerebrado dijo...

Salvando las diferencias (por ejemplo, sin usar JQuery en su momento) es exactamente lo que yo habia hecho en una aplicacion anterior... pero te voy adelantando algunos problemas, para que vayas pensando las modificaciones:
1) Es posible que desde una pagina quieras llamar al metodo de otra (por una agrupacion natural de los webmethods en namespaces y/o funcionalidades) Yo lo solucione creando por reflection unos "indices de objetos webmethos" (objetos que contenian la url, el metodo y los datos necesario) que se escribian en archivos javascript agrupados por dichas funcionalidades. Una pagina para usarlos, simplemente debia hacer referencia al archivo javascript adecuado, y "usar" el objeto correspondiente.

2) Tenes que verificar de invalidar las caches para los webmethods.

3) Trata de que las clases que contienen los webmethods sean singleton. Es un trabajito del lado del server, pero te va a ser muy util.

4) Ojo con las llamadas cross domain (en su momento habia que hacer un workaround con IE para que te deje)

5) Apenas llegue de nuevo a Argentina (y como prometi en mi blog, que ya deberia llamarse el blog de promesas incumplidas...), voy a publicar mi codigo para generar json para objetos anidados o filtrar propiedades.

AcP dijo...

AAAAAAhhhhh.... sí falta mucho, y esos detalles (chache, cross domain) ni los había pensado.

Sí tenía resuelto el tema de llamar de una página a los métodos de otra (el código en google code ya tiene eso), porque en la aplicación de acá se necesita...

Por otro lado me están comentando que estoy reinventando ASP.NET MVC, que funciona más o menos así (aunque no lo vi en detalle), y ya está hecho (gran ventaja). Por suerte todo esto tiene una utilidad concreta en la aplicación sobre la que estoy trabajando ahora.

Anónimo dijo...

Por favor Andrés sigue,sigue que estoy descubriendo como debería estar haciendo las cosas....

AcP dijo...

@Ibioisset: gracias, pero ¡ojo! no te confíes, que yo también.