Lo encontró @Cerebrado hace un montón, pero recién me lo mandó la semana pasada… me colgué un rato y finalmente junté las fuerzas necesarias para postearlo… agendado para mañana.
Lo encontró @Cerebrado hace un montón, pero recién me lo mandó la semana pasada… me colgué un rato y finalmente junté las fuerzas necesarias para postearlo… agendado para mañana.
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?
¿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?
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.
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:
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.
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).
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!
<head runat="server">
<title>Untitled Page</title>
<script type="text/javascript" src="jquery-1.3.2.js"></script>
</head>
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.
<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>
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).
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} );
}
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.
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;
});
}
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.
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:
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.
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".
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.
<script type="text/javascript" src="jquery-1.3.2.js"></script>
<input type="button" id="btnHelloWorld" value="Hello World" onclick="btnHelloWorld_OnClick()" />
<input type="button" id="btnHelloWorld2" value="Hello World2" onclick="btnHelloWorld2_OnClick()" />
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);
}
});
}
Aquí lo importante es ver la llamada a $.ajax. Los parámetros relevantes son:
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.
El que los sistemas de transmisión de datos se han vuelto cada vez más veloces es un hecho obvio e indiscutible. Baste con recordar que hace escasos 15 años estábamos todos muy contentos con nuestros módems de 56K.
Esta velocidad de transmisión ha permitido enriquecer los mensajes llevándolos desde el texto puro a la multimedia y haciéndolos cada vez más disponibles. Cuando yo era chico (y no soy tan viejo) tener teléfono en casa era toda una novedad. Hoy prácticamente todos tenemos celulares.
Internet, si bien no está presente en todos los hogares y bolsillos es por lo menos de fácil acceso a través de distintos puntos de acceso a un precio razonable (locutorios, por ejemplo). Por ahora los abonos de acceso a través de la telefonía celular con tarifa plana (y razonable) son una utopía (hay que leer la letra chica), pero no faltará mucho para que se conviertan en parte de la vida cotidiana.
Una pequeña reflexión motivada por el comentario de Senior Manager en Los más rápidos. La entrada es apenas un video gracioso con algunos de los records “más rápidos”, lo importante es el comentario:
Ser el más rápido en "algo" seguro que algún mérito tiene. No obstante, y como contraste, he sentido una tendencia en la blogosfera que se inclina hacia el "slow motion" y que ya no la motiva la velocidad de la información... No sé si se deba al agotamiento digital u a otras razones, lo que si sé es que cada vez es más evidente. ¿lo has sentido tú?
SM
Tenemos cada vez más velocidad y disponibilidad de transmisión de datos. ¿Y de información?
Si estoy a punto de salir de casa y recibo un mensaje que dice que “está lloviendo en Buenos Aires” eso es información, ya que me mueve a tomar el paraguas antes de salir para no mojarme. Si ya estoy en la calle (mojándome) es apenas un dato que no aporta ningún conocimiento. Lo mismo si no estoy en Buenos Aires o si no planeo salir.
En pocas palabras, la información es un conjunto de datos relevante para quien lo posee. Un mensaje aporta una cantidad de conocimiento (dato) que puede ser medida objetivamente en un lugar y tiempo determinado: llueve, no llueve, la bolsa sube, la bolsa baja, salió tal producto al mercado, se descubrió tal cosa o se utilizó tal tecnología. Si aporta o no información dependerá de quién lo tenga entre manos, de sus motivaciones, objetivos y posibilidades en ese momento.
Una de las definiciones que recuerdo (y no recuerdo de quién) formalizaba diciendo (más o menos) que “la información es un conjunto de datos relevante para la toma de decisiones”. Es decir que un mensaje, un dato, es información si (y sólo) si motiva un cambio en el comportamiento de quien lo recibe.
Llevándolo a la vida cotidiana: uno se levanta a la mañana, lee el diario y encuentra allí un montón de mensajes, de datos. Si uno no lee el diario todos los días tal vez mira el noticiero del mediodía o de la tarde, y es más o menos lo mismo: una porción de estos mensajes será información para nosotros (es decir, inclinará la balanza en alguna decisión o motivará algún cambio en nuestra conducta): el estado del tiempo, determinadas noticias sobre economía, política, tecnología, algunos anuncios comerciales.
Agreguemos algunos diarios online, tal vez de otros países, algunos blogs generalistas y otros especialistas. Agregadores sociales, Menéame, Bitácoras. Agreguemos las actualizaciones de Facebook, LinkedIn, FriendFeed…
Una vuelta más de tuerca: convirtamos todo eso en un flujo continuo (streaming) que nos llegue al celular, la notebook o la netbook a través del e-mail, de Twitter o de un widget sobre el escritorio.
Sumémosle a esta marea una cuota de incertidumbre: no todos los mensajes son exactos, algunos son contradictorios, no todas las fuentes tienen la misma confiabilidad.
Imaginemos ahora que, a pesar del ruido, buena parte de todo eso es información, que es relevante para nuestras decisiones, que motiva o que debería motivar ciertos cambios en nuestra conducta, acciones, respuestas, llamados, más mensajes.
Es claro cómo podemos llegar rápidamente a la sobrecarga, a la saturación. La imposibilidad de procesar (filtrar, analizar, y actuar en consecuencia) lleva a la ansiedad, al estrés.
¿Dónde está el punto de inflexión? El modelo de “leo diarios y blogs y veo las noticias para para estar enterado de lo que sucede y ver cómo afecta a mi vida” tiene un límite, el límite del tiempo y atención que podemos dedicarle a filtrar y analizar un conjunto de mensajes en busca de aquellos que tengan cierta relevancia para nosotros.
Es una tentación interpretar automáticamente ese flujo continuo de datos como información ante la que uno debería reaccionar, en vez de buscar información para tomar decisiones de acuerdo a objetivos y motivaciones preexistentes… Una tentación de alguna manera inducida por los productores de esos mensajes, que compiten por nuestra atención e intentan desesperadamente que creamos que son relevantes (al fin y al cabo si creemos que son relevantes entonces son relevantes, puesto que todo este tema de la relevancia es, recordémoslo, subjetivo).
Cuando hay abundancia o saturación de mensajes -es el caso- el modelo colapsa, nosotros colapsamos.
Es necesario cambiar el enfoque: en vez de partir del conjunto de datos disponible (casi infinito) podríamos, por ejemplo, partir de nuestras necesidades, obligaciones o motivaciones. En vez de leer megabytes de mensajes por día, tendríamos que pensar “¿qué quiero/tengo que hacer? ¿qué necesito saber?” y buscar mensajes de acuerdo a ello. En vez de filtrar debemos buscar.
A mí me ha sucedido -y no creo que sea excepcional- en mis primeros contactos con la red y sobre todo con la “blogósfera”, que descubría un blog, luego otro y otro, que me llevaba a otro y luego a otro, a los agregadores, a Twitter… pretendía leer todo como si fuese un diario o un libro, pretendía filtrar todo aquello. Finalmente mis costumbres fueron cambiando y aquella marea de bytes se redujo a un par de blogs que sigo y leo cotidianamente, y que son separados de otro conjunto, bastante más grande, al que accedo cuando quiero leer o buscar sobre algún tema en particular.
Cerrando con el tema del “slow motion” y la motivación de la velocidad: sí, la tendencia (a ir más despacio, a leer menos, a estar menos conectado, a bajar el ritmo) es evidente. Creo que tiene que ver con el reflujo de una gran cantidad de personas que ha ingresado a “la red 2.0” con el modelo mental del diario y el noticiero y que, habiendo llegado a la saturación, sienten la necesidad de ir más despacio o, mejor dicho, de volver a controlar el tiempo y de no someterse al ritmo impuesto por una tecnología en continua aceleración.