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.

No hay comentarios.: