viernes, 31 de julio de 2009

Unámonos… mañana.

 proscastinator

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.

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.

Anuncios del futuro.

Visto en flapa.

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.

lunes, 27 de julio de 2009

Datos, información y velocidad.

velocidad1 velocidad2

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.

domingo, 26 de julio de 2009

Los más rápidos.

Un video con algunos de los records “más rápidos” que incluye, entre otros, alguna gente con habilidades tan sorprendentes como inútiles:

  • La apiladora de vasos más rápida (es impresionante).
  • El “aplaudidor” más rápido.
  • El que se desviste más rápido (ésta habilidad puede llegar a ser útil en algunas ocasiones… me pregunto si en esas ocasiones hace el resto igual de rápido).
  • La selladora más rápida (debería trabajar en AFIP).

Visto en ALT1040.

viernes, 24 de julio de 2009

Jueguitos de Viernes: Metro Siberia.

Informática práctica me ha hecho acordar de Metro.Siberia. Es un juego muy, pero muy adictivo que me ha hecho perder largas horas en su momento. Es tan simple y complicado como atravesar un entorno futurista en una nave a la que sólo controlamos encendiendo la propulsión en pequeñas dosis con la barra espaciadora.

Siberia

Si los sistemas operativos fuesen comida…

cocina-rapida Windows sería comida rápida:
  • Es fácil de conseguir.
  • Sirve para saciar el apetito, pero tiene carencias nutricionales.
  • El usuario tiene muy poco control sobre la personalización de la comida (con queso, sin queso, con pepinillos, sin pepinillos y poco más).

cocina-gourmet Mac comida gourmet:

  • Es cara.
  • La presentación es casi más importante que el producto en sí.
  • El usuario no puede aderezar libremente la comida. Tiene que comerla como se la sirven y si cuestiona el sabor, generalmente se le tachará de tener mal gusto.
comida-casera y Linux comida casera:
  • Siempre estuvo ahí, no se inventó por negocio, sino por necesidad y por placer.
  • Se ajusta a todos los bolsillos, puede ser barata, cara o incluso gratuita.
  • Se cuida la presentación, pero lo importante es la comida.

El resto en un viejo post de jEsuSda rescatado del olvido por una larga (realmente larga) cadena de “vía” a la que me sumo:

Vía Punto Geek.

jueves, 23 de julio de 2009

Avances en almacenamiento.

2.2GB of Floppy Disks

El que sacó esa foto (sí, son discos de 3 y 1/2) nos cuenta que

There are 11 stacks of 140 disks here, making 1540 disks. Assuming most of them are 1.44MB disks then that means there is about 2.2GB of storage in all these disks.

o sea que es más o menos la cuarta parte de

supertalent-smallestusb

este USB Super Talent de 8 Gb cuyas medidas son 31.3mm x 12.4mm x 3.4mm.

miércoles, 22 de julio de 2009

Ese es… ¿Ballmer?

¡Sí! Presentando Windows 1.0. No ha cambiado mucho con los años.

Visto en Abadía Digital, donde han recopilado un top 5 de las locuras del ¿carismático? CEO de MS.

martes, 21 de julio de 2009

Depurar código es el mejor y más intensivo método de aprendizaje.

chica-microsoft-pantalla-de-error-azul En esta última semana estuve librando mis primeras verdaderas batallas con jQuery, un framework de javascript que, al igual que mooTools, me parece sencillamente fabuloso.  La verdad es que me encantaría utilizar los dos al mismo tiempo, tan difícil resulta decidirme por uno u otro.

Me sorprendo ingenuamente de su efectividad porque vengo de la época en que javascript era apenas una herramienta para hacer validaciones del lado del cliente y ahorrarle un poco de tiempo y transiciones al usuario (con 56K apretar “submit” y esperar 20 segundos –con suerte- para recibir un par de mensajes de error era toda una frustración). Una época en la que cada uno armaba su propio “framework” y se conformaba con que fuera funcional para IE -y tal vez para Netscape-, le permitiera cargar dinámicamente una lista, armar una grilla básica y –para los más intrépidos- hacer unas ventanas de diálogo más elegantes que el básico alert() de javascript.

Con mucho sudor y lágrimas esos frameworks fueron creciendo y haciéndose más complejos… para luego descubrir (que frase tan típica de viejo la experiencia) que “estos mocosos de ahora la tienen regalada”, y que logran en 10 minutos lo que a uno le ha costado años.

En fin, todo eso fue ñañería y digresión. La cuestión es por primera vez en un largo rato estoy aplicando profesionalmente, a gran escala y en un sistema complejo, una tecnología totalmente nueva para mí.

¿Y cuál es la mejor manera de aprender? A esto iba: haciéndose desde abajo, revolviéndose en el fango, peleando entre la mugre… depurando código.

No me voy a hacer el humilde, lo hago porque no tengo alternativa, lo hago porque soy el nuevo y el más inútil de todos, porque me toca ocuparme de lo que me tiren y que, lógicamente, es aquello en lo que nadie quiere meterse. Porque no puedo esquivar el bulto, para resumir. Hay que pagar el derecho de piso, y el camino es resolver problemas. Simples, complejos, triviales… todos… y tratar de hacerlo bien (y ni hablar si se da el caso extremo que comentaba en Guía para perplejos: primera vista al código en un nuevo trabajo y... ¡ups!).

Sí, lo hago refunfuñando por lo bajo… hubo varias ocasiones en las que por suerte no tenía un objeto contundente cerca mío, pero tengo que reconocer que he aprendido más de jQuery en dos días que con todas las pruebas y pequeños proyectos inútiles que hice anteriormente.

Esto es porque lo que hay que hacer se aprende con los tutoriales, con los manuales, con los proyectos de prueba. Pero lo que no hay que hacer sólo se aprende corrigiendo errores o cometiéndolos.

¿Y para qué transitar el tortuoso camino de cometer todos los errores posibles para hacerlo un poco mejor la próxima vez? Es mucho mejor depurar, sacarle las pulgas a un sistema ya casi terminado y aprender no sólo de los errores que encontramos sino también de los muchos aciertos que los rodean. Es un curso intensivo, demanda el doble de paciencia y esfuerzo pero sólo una cuarta parte del tiempo que implicaría “aprender por la positiva”.

Dice el adagio que “el que se quema con leche ve una vaca y llora”. Pasarse un par de horas fracasando estrepitosamente en la búsqueda de algún lugar para validar si la sesión ha caducado para luego resignarse a implementar una solución particular en cada lugar donde se presente el problema (¡muchos lugares!) nos fijará en la memoria las bondades del encapsulamiento y la separación en capas a través de interfaces que podamos controlar (por cierto, esto último es lo que suele olvidarse).

Así que la próxima vez que se enfrenten a un mar de código al que –en principio- no le encuentren ningún sentido… no piensen que están depurando sino que están aprendiendo, aprendiendo tan rápido que en breve llegará ese “click” en donde nuestro cerebro, ya acostumbrado a la sintaxis propia del nuevo lenguaje o herramienta, deje de mirar y empiece a ver.

Ah, por cierto… todo muy lindo, he aprendido mucho en dos días y descubierto varios errores, pero todavía no he logrado corregir elegantemente ni uno de ellos. Ya lo dije, paciencia.

lunes, 20 de julio de 2009

Panoramas polares

Una increíble recopilación de 50 ejemplos de panorámicas polares en 50 Dazzling Examples of Polar Panorama Photography. Algunos de mis favoritos:

1 2
3 4
6 7

Visto en Punto Geek.

domingo, 19 de julio de 2009

Video Games

Es el título de esta animación obra de MUSCLEBEAVER que tiene por protagonistas a los personajes de varios clásicos de los videojuegos.

Según el autor, la animación fue pensada como prólogo de un documental alemán acerca de la vida cotidiana de algunos adictos a MMORPG’s.

Los personajes están muy bien caracterizados (fueron redibujados), las transiciones entre las diferentes pantallas son buenísimas y el final… er… un poco extravagante, pero vale por lo inesperado.

Basta de bla. Acá está el video.

Algunos de esos juegos están disponibles en flash para jugar online: Pong, Pacman, Space Invaders, Mario Bros, Monkey Island (er… bueno, no realmente, sólo un video gracioso que encontré de casualidad), Doom, Street Fighter.

Visto en Punto Geek.

viernes, 17 de julio de 2009

Jueguitos de Viernes: Tennis Stars Cup

La gente de Three Melons se vanagloria de su experticia en el desarrollo de juegos sobre Unity… y tienen con qué hacerlo.

En su página principal presentan, al día de hoy, cinco juegos. Tennis Stars Cup es el que está abajo a la derecha… es espectacular.

tennisstarscup

Más allá de los muy cuidados detalles gráficos, el juego es buenísimo. Controlamos la acción a través de gestos con el mouse de una manera muy intuitiva, lo que permite aprender y utilizar todos los efectos durante el partido. Créanme que para el tercero del campeonato ya se vuelve necesario poner en práctica todos los trucos posibles.

jueves, 16 de julio de 2009

¿Qué puedo ser…? (meme).

viral  ¡Oh! ¡Oh! (Entusiasmo) ¡Me han pasado un meme! Ha sido Iboisset, a él se lo ha pasado TicTac… ¿y esto con qué se come?

Es uno difícil… “¿Qué puedo ser?” Uno puede ser tantas cosas… para complicar más la reflexión hay que tener en cuenta que lo que uno puede ser suele confundirse dolorosamente con lo que uno quiere ser.

Creo que puedo ser útil, servir para algo, hacer una diferencia. Usualmente no una gran diferencia, pero siempre alguna. Quiero ser un buen compañero de equipo y creo que puedo serlo… pero bajo mis términos, lo que implica que a veces puedo, desde algún punto de vista, ser un poco… desalmado, diría.

Puedo ser alegre, divertido, despreocupado, triste, hosco, irascible. A veces quiero ser en blanco y negro (o a lo sumo en CGA), pero -como todo el mundo- puedo ser a colores o en escala de grises.

Me interesa saber qué dirían Improbable, Alfonso o Juan, así que a ellos les paso esta molestia.

Crónica de -y consejos para- una búsqueda de trabajo.

 diario La idea de este post es comentar mi experiencia, no porque crea que sea importante o representativa, sino –como todo en este blog- simplemente porque me es divertido hacerlo y tal vez invite a la discusión o el comentario.

Me presenté como desarrollador senior en .Net (con acento en “plataformas web”) o analista funcional (soy un hermafrodita informático), y estoy aquí, en Buenos Aires, Argentina. Aclaro porque creo que ese mercado -si bien golpeado por la crisis- sigue estando muy activo, lo que desgraciadamente no es la norma.

Cuando empecé a coquetear con la idea de cambiar de trabajo comencé por actualizar mi CV en cuanta página encontré por allí. Éste es un consejo muy escuchado: circula tu CV por todos lados. La realidad es que aquí en Argentina el 90% (porcentaje al voleo) de las ofertas para el rubro de sistemas se publica en uno de los tres grandes sitios (UniversoBit, Computrabajo, Bumeran), y la mayoría en todos y ellos y en los diarios al mismo tiempo.

Hay ciertas consideraciones que hacer respecto de lo anterior:

  • Si me preguntan, yo diría que ni comprar el diario ni matarse cargando el CV en otros sitios vale la pena, basta con los tres mencionados (que podrían tirar una moneda ¿eh? Este chivo fue gratis).
  • Cuidado porque uno puede estar postulando para las consultoras A, B y C, que sacan sus anuncios por separado (con redacción diferente) pero que son en definitiva para el mismo puesto en la empresa Z. También puede suceder que se estén buscando varios puestos (típico de software factory o consultora grande) y que uno se postule, sin saberlo, a varios en la misma empresa o incluso en el mismo proyecto.
  • De lo anterior surge que el seguimiento es importante para no perder el tiempo (transitar dos procesos de selección por el mismo puesto real). Yo no lo hice y se me complicó, y fui a entrevistas para puestos para los que ya me habían entrevistado. Hice sociales en las consultoras, eso sí, lo que es algo muy rescatable.
  • Y –opuesto a casi toda opinión que se lee por allí- que también es importante tener un sólo CV, no varias versiones. ¿Por? Simple: porque puede que dos versiones diferentes lleguen a la misma instancia de selección por dos caminos independientes… y a mi me parece que queda feo que en uno diga “semi-senior” y en el otro “gurú”, por más que sean para puestos diferentes. En todo caso se puede complementar el CV con cartas ad-hoc para cada postulación, pero siempre, siempre, hay que tener en cuenta que diferentes versiones podrían pasar por los mismo ojos.

¿LinkedIn, redes sociales? Bueno, sí… aparecen un par de ofertas (yo tengo unos escasos 50 contactos), pero son las mismas que en los sitios mencionados. De todas maneras hay que estar, no cuesta nada y es cómodo tener el CV cargado allí. Por otro lado es seguro que alguien, en algún momento hará una búsqueda en google (es indispensable hacerla uno mismo antes, para evitar que te agarren desprevenido), así que si van a aparecer las fotos de la borrachera de la semana pasada entre los resultados por lo menos que no sea lo único, y si aparece bien abajo y no arriba de todo, un poco mejor.

Nota de esperanza para los que desesperan ante la aparición de algún material inconveniente en una búsqueda por nombre y apellido: la segunda página de resultados no la lee nadie. Mi consejo: te pones un blog y te haces algunas entradas (¡serias!) como para desplazar el material NSFW fuera de la primera página. ¿Quien sabe? Tal vez te pique el entusiasmo y te conviertas en blogger.

Nota de desesperanza: si te llamas Juan Perez o Pedro Fernandez disfrutas del verdadero y único anonimato en internet: puedes hacer lo que quieras, total nadie va a distinguirte entre miles de personas con el mismo nombre. Pero si quieres capitalizar tu “2.0” búscate un nick bien original (y que no sea del tipo “LaMasLargaYLaMasAncha”), y no creo que quede mal sugerir explícitamente esa búsqueda en el CV.

telefono Sigamos… yo tenía (como todo el mundo, pero digamos que las hice explícitas), en principio, muy altas expectativas monetarias. Y eso se notó porque no llamaba nadie. Creo que la mejor técnica -si es que se puede practicar la paciencia- es empezar alto e ir bajando de a poco hasta que empiecen a darse los contactos. El nivel en el que empiecen a llamar (y no el que nosotros pretendamos) será nuestro valor de mercado. ¿Conforme? Pues adelante. ¿Inconforme? Pues no pierdas el tiempo y concéntrate en mejorar tu posición en la empresa en la que estás, que la realidad no cambia por postularse a 200 avisos por semana y la oportunidad que se da como la lotería, una-sola-vez-en-la-vida… no existe (bueno, sí existe… como también existe el ganador de la lotería).

El primer contacto es telefónico o por mail, usualmente en los dos o tres primeros días y aparece de vuelta –¿para qué quieren el CV?- el tema de la remuneración pretendida, el de los requisitos, y las expectativas. Hay que reconfirmar, sobre todo la remuneración.

No es mi intención calificar a los selectores profesionales, sólo apunto que es un dato real de mi experiencia que si uno es programador en C# y pide $10 al menos un selector llamará por un puesto de $2 y pretenderá que cuando conozcamos la empresa, el ambiente, el proyecto y las fantásticas oportunidades estaremos felices comiendo eso con cebolla cruda todos los días.

La parte de la tecnología lo entiendo, no tienen por qué ser expertos y me parece razonable para un ajeno a la programación el intuir –nosotros sí que sabemos poner nombres confusos a las cosas- que tal vez “C++” y “C#” no son tan diferentes uno del otro… también me parece razonable ofrecer +/- 10% de lo pretendido, pero ¿la mitad, el doble, el triple o la tercera parte…?

La otra posibilidad es que el selector declare que “la remuneración todavía no está del todo determinada” o alguna ambigüedad por el estilo. Seré prejuicioso pero eso me suena a proyecto raro, en pañales, a empresa poco seria o a “hacer base de datos” para aprovechar espacios de avisos ociosos y ya pactados (práctica que en un momento era común, ahora no sé).

¿Vale la pena ir a una entrevista en esas condiciones? Que cada uno decida.

Y luego el primer contacto cara a cara. De lo más inútil para uno como candidato. No esperen formarse una idea de la empresa, de la oferta, de los requerimientos técnicos, nada de eso. Es el primer e indispensable filtro grueso para que el selector corrobore que somos “por lo menos presentables” y se asegure de que la consultora o el departamento de RH no quede mal molestando al cliente o al jefe pactando un encuentro que resulta inconducente a los dos primeros minutos.

Las preguntas de siempre: ¿por qué estás buscando? ¿qué esperás de un nuevo empleo? Contáme algo de vos (esa la odio, es como que el selector no tiene ni ganas de pensar una pregunta decente) y, pasado cierto punto, las respuestas de siempre. Con la práctica ya tenía puesto el cassette (qué viejo que soy) y repetía lo mismo casi palabra por palabra. Acá valen los consejos sobre vestirse bien (por lo menos bañarse), dar un fuerte apretón de manos (o no, es un tema controversial), mirar a los ojos (o no, gran debate)…

Un detalle curioso que me sorprendió es la nueva moda (lo hacen casi todos) de comenzar o terminar la entrevista con un muy marketinero “te cuento algo sobre nosotros: Los Superselectores es una consultora que se enfoca en…” El problema es que el discursito es usualmente vomitado en una forma tan, pero tan McDonald’s que el efecto es de despersonalización total, una especie de molesto recordatorio de que “ya vi a mil y un postulantes para mil y un puestos y dije esto mil y una veces y dentro de 10 minutos no voy a poder reconocerte aunque nos crucemos en un pasillo”.

Lo que hay que entender, en definitiva, es que podemos ser verdaderos genios con los algoritmos… pero que la persona que tenemos en frente no tiene idea de lo que es un algoritmo y no podría distinguir a un ingeniero de un programador junior si no fuera por lo que dice el CV… y la imagen que da esa persona. Que es difícil para un junior simular ser ingeniero estamos todos de acuerdo, pero no es tan difícil para un ingeniero quedar fuera de lugar como un pelo**** soltando un discurso desubicado, poco serio o demasiado técnico cuando no corresponde. Así que a cuidar las formas, que aquí es lo que vale.

Esta entrevista termina, inevitablemente, con el “en estos días te avisamos por sí o por no”. Las formas siempre muy cuidadas… pero salvo contadísimas excepciones (sólo recuerdo uno o dos mails) olvídense de que alguien les va a avisar de que no superaron esta instancia. No importa qué tan seri@ o simpátic@ resulte el selector@, sólo llamará al momento de pactar una segunda entrevista.

El tan recomendado seguimiento de llamar uno para “ver cómo va”, por otro lado, me parece completamente inútil: más fácil y realista es no esperar una respuesta, seguir con la búsqueda, no hacerse la cabeza, no dejar de postularse a otras ofertas, y si llaman bien… y si no también. Esto vale, en realidad, para cualquier instancia. Ahora, si lo vemos al revés, es un lindo detalle que no cuesta nada avisar a todos los “pendientes” que nos “bajamos” de la búsqueda ya que aceptamos una de las ofertas. Puede que ese mail, rescatado dentro de un par de años, deje una buena imagen.

entrevista3 Ya la segunda instancia es algo a lo que uno debería ir más seriamente preparado. Aquí quién es ingeniero o licenciado se nota. Y se nota quién es de verdad y quién tiene sólo el título, no importa el CV de cada uno.

Es una instancia de evaluación mutua, así que hay que pensar un poco qué preguntar y estar atento a los detalles. Probablemente sea en la cocina, en el lugar de trabajo real y no en una sala de reuniones estándar y despersonalizada. Es la oportunidad (tal vez la única) de percibir a la gente, su actitud… “el ambiente”, en definitiva. Puede que conozcamos a nuestro probable superior, y en este caso yo diría que es importante abrirse y ser natural y sincero. Si algo en su forma de ser o actuar (o en la nuestra) “choca” lo soportaremos todos los santos días, y seguramente no por mucho tiempo.

Y que no sorprenda, sobre todo en empresas grandes, entrevistas intermedias exactamente iguales a (un verdadero calco de) la primera. En tal caso, agua(ntarse) y ajo(derse) y esperar pacientemente la próxima, si es que se da.

En la mayoría de los procesos en los que participé (y fueron por lo menos 15) la segunda entrevista fue la más jugosa, en la que se habló de trabajo real y cotidiano con una o varias personas que realmente estaban en el día a día. En algunos casos (tal el que finalmente se concretó) hubieron más entrevistas, pero me parecieron un poco más… “políticas”, con gerentes o jefes de más alto nivel que simplemente quieren (muy razonablemente, para mí es un punto a favor) conocer y ser conocidos por todos los que de él dependen aunque no sea directamente, o con consultores u otros que desempeñan la misma tarea que nos correspondería y que evalúan cuestiones puramente técnicas (otro punto a favor, por lo menos sabremos qué nivel tendrán como base nuestros compañeros de equipo).

desierto Y luego, si tenemos suerte, una oferta real y concreta. Lo único que se me ocurre recalcar de esta instancia es que me parece de mal gusto ponerse a negociar, por aquello de lo que me quejaba más arriba: ninguna de las dos partes puede mover el salario fuera de ciertos márgenes conocidos desde un principio, por lo que es un ahorro mutuo de tiempo y una nota de seriedad dejar eso en claro desde el vamos y no volver a regatear luego de haber comprometido el tiempo de varias personas durante un par de semanas. De todas maneras reconozco que hay personas que lo hacen con una sutileza digna de un cirujano y nunca quedan mal. Yo en eso soy un elefante en un bazar así que mejor ni lo intento.

Eso es todo. Si tengo que resumirlo en una palabra, ésta sería perseverancia. A comentar, que se acaba el mundo.

miércoles, 15 de julio de 2009

Novedades personales (fiesta del autobombo).

Ratas Para todos aquellos que no siguen mi blog de personales, sobras, retazos y clase B en general, no prestan atención a mis interesantísimos twits, no son parte de mi red en LinkedIn, apenas si leen de vez en cuando el feed y sólo entran al blog cuando hay algún video o imagen que no carga en el reader -y ni hablar de leer mi perfil aquí en la columna de la derecha-… bueno, para todos excepto una ínfima –pero valiosa- minoría (snif), aclaro: acabo de cambiar de trabajo (¡tada!).

Efectivamente, hoy fue mi segundo día como programador en una “proveedora de soluciones basadas en nuevas tecnologías bla bla bla”, Iceberg Solutions. Lo que llamaríamos coloquialmente una consultora o software factory, tirando a mediana diría yo, pero en todo caso visiten su página web.

Si vienen leyendo este blog desde hace un tiempo entenderán que sí, he sido tentado por el lado oscuro (no creo tener hijos por ahí, pero andaré con cuidado por la dudas). Lo digo –antes de que alguien me lo eche en cara- por cosas como ésta y ésta, que son las que preceden conclusiones del tipo “el pez por la boca muere” o “eres amo de tus silencios y esclavo de tus palabras” (tal vez ya he escrito demasiadas)… aunque para hacer honor a la verdad –o para complicarla tanto que apenas se entienda- también hay que decir que, en mi afán por contemporizar y brindar una lectura un poco irónica y mordaz pero nunca extremista (que al fin y al cabo aquí está mi nombre y apellido) he escrito esto y esto otro.

Como ya he dicho, es muy pronto para hablar de lo nuevo –salvo decir que me estoy llevando, por suerte y por ahora, una muy buena primera impresión-. Se me ocurrió que cuando termine esta suerte de orgía autorreferente hija de un egocentrismo fuera de control (v.g.: este post) sería bueno comentar un poco este proceso de selección, como para escapar a tanta teoría que se ha posteado sobre el tema en este blog (como aquí, aquí, aquí, y también por acá y acá).

Pero esas son, por ahora, promesas. La realidad es que bastante vida le he robado al fiel lector estirando este post que bien podría haber cumplido su cometido con una frase o –exagerando- un pequeño párrafo. Seinfeld estaría orgulloso.