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

martes, 2 de marzo de 2010

.Net MVC + jQuery: manejo de excepciones. IV: el lado del cliente.

Finalizamos el post anterior de esta serie con la infraestructura necesaria para atrapar en javascript los errores y excepciones que puedan producirse del lado del servidor y “canalizarlos” hacia tipos conocidos con una codificación similar a la que utilizamos en c#.

Recordemos el ejemplo final. Teníamos, en javascript, una excepción base:

ExceptionBase = function (type, message) {
    this.Type = type;
    this.Message = message;
};

De la que luego derivamos dos excepciones más específicas, una para excepciones de negocio (la que arrojaría nuestro código en el caso de que una operación supere cierto monto permitido, por ejemplo):

ProgramException = function (message, netException) {

    this.Reasons = [];
    this.NetException = netException;

    if (typeof message != "undefined" && message != null)
        this.Message = message;

};
ProgramException.prototype = new ExceptionBase("ProgramException", "Operation Error");

y la otra para errores “fatales”, es decir todos aquellos que indican que la aplicación ha arribado a un estado no contemplado y que por lo tanto no puede seguir utilizándose (que ha cascado, vamos):

FatalException = function (message, netException) {
    this.NetException = netException;
    if (typeof message != "undefined" && message != null)
        this.Message = message;
}

FatalException.prototype = new ExceptionBase("FatalException", "Unexpected Error");

Para ver cómo utilizamos estas clases veamos nuestro ejemplo de llamada $.ajax, donde se establece la diferencia entre errores de negocio y fatales, lanzándose una excepción u otra dependiendo el caso. Más concretamente la sección “success” (lo siguiente es sólo el fragmento correspondiente a “success” dentro de la llamada a $.ajax):

  //... (etc) ....

  success: function (response, status, xhr) {

   //Exception handling.
   var responseStatus = xhr.getResponseHeader("RESPONSE_STATUS");
   var ex = null;
   if (responseStatus == "ApplicationException") 
   {
      var netEx = JSON.parse(response);
      ex = new ProgramException(netEx.Message, netEx);
      ex.Reasons = netEx.Reasons;
   }
   else if (responseStatus == "UnexpectedException") 
   {
      var netEx = JSON.parse(response);
      ex = new FatalException(netEx.Message, netEx);
   }

   if(ex!=null)
   {
      if (async)
         ShowException(ex)
      else
         throw ex;     
   }

   //...(continúa)...

Esta infraestructura de excepción base y derivadas sería una complejidad decorativa y sin sentido si no tomamos, en algún lugar del código, una decisión en base a ese tipo que tanto esfuerzo nos lleva determinar.

La diferencia entre una excepción y otra, desde el punto de vista del front-end, radica en cómo se le presenta al usuario. En nuestro ejemplo, esa tarea le corresponde a la función ShowException:

ShowException = function (exception) {
    //non-fatal exceptions
    if (exception.Type == "ProgramException") {
        var $messageBox = GetMessageBox();
        $messageBox.find("#MessageBoxReasons").html(exception.Reasons.join( "<p/>" ));
        var buttons = {};
        $messageBox.dialog({
            autoOpen: true,
            modal: true,
            buttons: { "Ok": function () { $messageBox.dialog("destroy"); } },
            closeOnEscape: true,
            title: exception.Message,
            close: function () { $messageBox.dialog("destroy"); }
        });
    }
    //fatal exceptions.
    else if (exception.Type == "FatalException") {
        var displayHtml = "<h1>" + exception.Message + "</h1>";
        var doc = window.top.document;
        doc.open();
        doc.write(displayHtml);
        doc.close();
    }
}

Esta función simplemente evalúa el tipo de excepción que recibe y toma las decisiones necesarias. En este ejemplo, muy simple y esquemático, tenemos dos secciones: la primera parte del if crea una ventana utilizando $.dialog para mostrar prolijamente las excepciones de negocio, y la segunda parte (luego del else) “rompe” la pantalla, limpiando todo el html y dejando solamente el mensaje de error.

La función auxiliar “GetMessageBox” simplemente construye el html necesario para mostrar el cuadro de diálogo:

GetMessageBox = function () {
    var top$ = window.top.$;

    var $messageBox = top$("#MessageBoxContainer");
    if ($messageBox.length > 0)
        return $messageBox;

    var $messageBoxContainer = top$("<div />").appendTo(top$("body"));
    $messageBoxContainer.attr("title", ""); 
    $messageBoxContainer.css("display", "none");

    var $innerTable = top$("<table/>").appendTo($messageBoxContainer);
    $innerTable.css("width", "100%");

    var $innerTableFirstRow = top$("<tr/>").appendTo($messageBoxContainer);

    var $innerTableFirstRowFirstCell = top$("<td/>").appendTo($innerTableFirstRow);

    var $messageBoxImage = top$("<img/>").appendTo($innerTableFirstRowFirstCell);
    $messageBoxImage.attr("alt", "");
    $messageBoxImage.attr("src", ""); //TODO: set src.

    var $innerTableFirstRowSecondCell = top$("<td/>").appendTo($innerTableFirstRow);
    $innerTableFirstRowSecondCell.addClass("messageBoxMessage");

    var $messageBoxMessage = top$("<div/>").appendTo($innerTableFirstRowSecondCell);
    $messageBoxMessage.attr("id", "MessageBoxMessage");

    var $innerTableSecondRow = top$("<tr/>").appendTo($innerTable);

    var $innerTableSecondRowFirstCell = top$("<td/>").appendTo($innerTableSecondRow);
    $innerTableSecondRowFirstCell.attr("colspan", "2");
    $innerTableSecondRowFirstCell.addClass("messageBoxDescription");

    var $messageBoxDescription = top$("<div/>").appendTo($innerTableSecondRowFirstCell);
    $messageBoxDescription.attr("id", "MessageBoxDescription");

    var $innerTableThirdRow = top$("<tr/>").appendTo($innerTable);

    var $innerTableThirdRowFirstCell = top$("<td/>").appendTo($innerTableThirdRow);
    $innerTableThirdRowFirstCell.attr("colspan", "2");
    $innerTableThirdRowFirstCell.addClass("messageBoxReasons");

    var $messageBoxReasons = top$("<div/>").appendTo($innerTableThirdRowFirstCell);
    $messageBoxReasons.attr("id", "MessageBoxReasons");

    return $messageBoxContainer;
};

Notarán que en la sección en la que se muestra el error fatal no se utiliza jQuery. En estas situaciones tenemos que evitar cualquier referencia externa al código que se está ejecutando, ya que sólo sabemos que ha ocurrido un error inesperado y no conocemos el estado de los demás componentes de la aplicación (como por ejemplo el plugin de jQuery. El error podría deberse a que el cliente no pudo descargar el script de jQuery). Aquí tenemos que esforzarnos en codificar, siempre dentro de lo posible, código que funcione en circunstancias extremas… y la mejor manera es que sea extremadamente simple.

Hasta aquí hemos abarcado las situaciones más comunes, y sólo en el marco de llamadas $.ajax al servidor:

  • Excepciones de negocio generadas del lado del servidor.
  • Errores producidos en del lado del servidor.

Parece una estructura demasiado compleja para, finalmente, hacer un “if” y determinar si rompemos la pantalla o mostramos un cuadro de diálogo, cubriendo apenas los dos puntos de arriba. Es un buen momento para repasar sus ventajas:

  • El código y la metodología en javascript es asimilable a lo que estamos acostumbrados a hacer en c#. Esto hace que la implementación sea natural,  fácilmente “explicable” y “recordable”. Verán que el resto de los puntos aquí presentados se aplican tanto a nuestra solución de javascript como al manejo de excepciones en c#.
  • Modularidad/Desacoplamiento: su único punto de anclaje con respecto al código correspondiente a la funcionalidad de negocio del sistema son los bloques try…catch en las funciones de manejo de eventos.
  • Reutilización: cualquier excepción que requiera el mismo tratamiento que las ya implementadas puede representarse con alguna de las clases ya existentes.
  • Extensibilidad: si queremos que el sistema reaccione a un nuevo tipo de excepción sólo tendremos que codificar “en las puntas”: allí donde se lanza la excepción (el lugar del throw) y allí donde se maneja (ShowException).

Por otro lado hay mucho para explorar de aquí en más. Recordemos que ni siquiera cubrimos los casos mínimos para una aplicación “aceptable”. Veremos que al contemplar más situaciones (errores y excepciones de lado del cliente, en la comunicación, etc.) y al integrar más funcionalidad (por ejemplo mostrando un mensaje de error especial en ambientes de desarrollo o testing), en el manejo de “pequeños detalles” y casos especiales, será cuando este esquema muestre sus verdaderas ventajas.

lunes, 22 de febrero de 2010

.Net MVC + jQuery: manejo de excepciones. III: Atrapar errores del lado del servidor y comunicarlos controladamente al cliente.

[Continuación de .Net MVC + jQuery: manejo de excepciones. II: El problema.]


works-on-my-machine Espero que se haya entendido el planteo desarrollado en los posts anteriores de esta serie (preguntar es gratis, de cualquier manera)… lo que sin duda ha quedado claro es que el problema es retorcido.

Lo que sigue ahora es mi resolución del tema, más que perfectible por cierto (si alguien tiene sugerencias…), pero lo suficientemente funcional y probada como para ostentar orgullosamente el sello que he aplicado a esta entrada.


El primer paso es poder diferenciar, del lado del cliente, si el error o excepción se produjo durante la ejecución del código que procesa una respuesta o si la comunicación en sí fue fallida.

La importancia de lo anterior radica en que en el primer caso manejaremos la excepción del lado del servidor y podemos devolver al cliente información detallada y en un formato conveniente sobre cómo proceder:

  • si es un error de sistema o una excepción de negocio o un error de validación, si mostrar información detallada o no, si bloquear la aplicación y obligar al usuario a reiniciar (o a joderse), etc.
  • si es un error en la comunicación el cliente deberá tomar sus propias decisiones, de acuerdo con la información que posea en ese momento, pero ya contamos con un dato fundamental: la operación no llegó al servidor, por lo que podemos estar seguros de que nada ha sucedido “del otro lado”.

Recordemos que de forma predeterminada el servidor devolverá un error 500 y el código HTML de la pantalla de error asignada a ese código (por defecto aquella tan bonita de .Net con las letras en rojo catástrofe y el cuadro amarillo con detalles, más las inútiles indicaciones que todos conocemos). Esto no es precisamente un formato fácil de interpretar desde el código a la hora de tomar decisiones en el front-end.

Necesitamos interpretar desde el código la excepción porque tenemos que tener en cuenta que muchos tipos de errores “de aplicación” diferentes. Cada situación puede requerir diferentes propiedades en el objeto Exception para transmitir la información relevante del caso. Consideremos por ejemplo:

public class ShowCaseException : ApplicationException
{
   private List<string> reasons = new List<string>();

   public ShowCaseException()
      : base() { }

   public ShowCaseException(string message)
      : base(message) { }

   public ShowCaseException(string message, Exception innerException)
      : base(message, innerException) { }

   public List<string> Reasons
   {
      get { return reasons; }
   }
}

La excepción anterior contiene una propiedad extra (“Reasons”) con una lista de mensajes para presentar al usuario. Esto es algo que suelo utilizar bastante, ya que si durante una operación se producen varios problemas de validación es bueno presentárselos al usuario todos juntos y no solamente el primero y obligarlo a resolverlo antes de probar de vuelta y encontrarse con otro y así, multiplicando la cantidad de intentos.

Pero no podemos tratar con cada tipo de excepción en particular, viendo en cada caso qué información devolver y cómo. Una solución más práctica es devolver la excepción serializada en formato JSON, más un flag en el header de la respuesta que la diferencie de las respuestas “normales”:

internal static class ExceptionHandlingHelper
{
 public enum ResponseStatusEnum
 {
  Normal,
  ApplicationException,
  UnexpectedException
 } 

 internal static void ApplicationInstance_Error(object sender, EventArgs e)
 {
  Exception exception = HttpContext.Current.Server.GetLastError();
  if (exception is ApplicationException)
   ResponseHeadersHelper.CurrentHeaders.Add("RESPONSE_STATUS", ResponseStatusEnum.ApplicationException.ToString());
  else
  {
   Log.WriteException(exception);
   ResponseHeadersHelper.CurrentHeaders.Add("RESPONSE_STATUS", ResponseStatusEnum.UnexpectedException.ToString());
   if (![DEBUG_MODE])
    HttpContext.Current.Session.Abandon();
  }

  HttpContext.Current.Response.Clear();
  HttpContext.Current.Response.Write(ExceptionHandlingHelper.SerializeException(exception));
  HttpContext.Current.Server.ClearError();
 }
 //.... etc ....
}

Tenemos entonces tres estados posibles para una respuesta:

  • Error de negocio: ResponseStatusEnum.ApplicationException, son las excepciones lanzadas mediante un throw en nuestro código, y que deben derivar, por convención, de ApplicationException.
  • Error inesperado: ResponseStatusEnum.UnexpectedException, son todas las demás, originadas en throws por validaciones de consistencia en nuestro código o directamente desde el framework.
  • Normal: todo bien.

Así que lo primero que hacemos en nuestra rutina de manejo de excepciones es conseguir la excepción y determinar de qué tipo es, agregando un header en la respuesta (“RESPONSE_STATUS”) para que el cliente javascript pueda diferenciarlas fácilmente de las respuestas normales. Si es un error inesperado podemos, adicionalmente, registrarlo en el log y limpiar la sesión del usuario.

Luego se modifica la respuesta que por defecto enviaría el servidor, utilizando Response.Clear(), Server.ClearError() y escribiendo en el objeto Response la excepción serializada en JSON.

La serialización de la excepción es un poco molesta dado que el uso común de JavascriptSerializer arroja una excepción por culpa de la propiedad TargetSite, que se vuelve recursiva en cuanto al tipo. Pero, por otro lado, habíamos establecido que en un ambiente de producción no es conveniente enviar toda la información de la excepción ya que puede exponer datos internos a un ocasional atacante. Así que tenemos que implementar nuestra propia serialización para esta clase.

Una forma sencilla es recorrer el primer nivel de propiedades del objeto Exception y dejar que JavascriptSerializer se encargue del resto. El siguiente ejemplo es una prueba de concepto –no pretende ser una guía de lo que es o no seguro enviar al cliente-, solamente omite en la serialización las propiedades “InnerException”, “StackTrace” y “Source”. Tal vez habría que agregar más restricciones, probando siempre qué información estamos enviando en cada caso.

private static string SerializeException(Exception ex)
        {
            JavaScriptSerializer serializer = new JavaScriptSerializer();

            StringBuilder exSerialized = new StringBuilder("{");
            PropertyInfo[] exProperties = ex.GetType().GetProperties(BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance);
            foreach (PropertyInfo property in exProperties.Where(p => p.Name != "TargetSite"))
            {
                if (![DEBUG_MODE] &&                    
   (property.Name == "InnerException" || property.Name == "Source" || property.Name == "StackTrace"))
                    continue;

                exSerialized.AppendFormat("\"{0}\":", property.Name);
                serializer.Serialize(property.GetValue(ex, null), exSerialized);
                exSerialized.Append(", ");
            }

            if ([DEBUG_MODE]) 
            {
                exSerialized.AppendFormat("\"ToString\":");
                serializer.Serialize(ex.ToString(), exSerialized);
                exSerialized.Append(", ");
            }

            exSerialized.Remove(exSerialized.Length - 2, 2);
            exSerialized.Append("}");
            return exSerialized.ToString();
        }

La información así serializada es fácil de acceder desde el cliente ya que se convierte automáticamente en un objeto similar a Exception, pero en javascript.

Notarán que en el ejemplo incluí, para entornos de desarrollo, una propiedad extra que contiene la cadena devuelta por el método ToString de Exception, que es lo que se suele mostrar por pantalla en caso de errores. Tendremos, en resumen, disponibles en javascript casi todas las propiedades del objeto Exception más una propiedad “ToString” que contiene una cadena con toda esa información en un formato legible para el usuario/desarrollador.

Volvamos a javascript para ver cómo manejamos la respuesta ahora. Partimos del ejemplo del post anterior completando algunos de los “placeholders” que habíamos dejado, ya que ahora sabemos exactamente cómo llega la información desde el servidor:

function Save( data, callback )
{
 var async;
 if( typeof callback == "undefined")
  async=false;
 else
  async=true;
 
 var returnValue = null;
 
 $.ajax({
  url: "Comments/Save",
  data: data,
  async: async,
  success: function (response, status, xhr) {

   //Exception handling.
   var responseStatus = xhr.getResponseHeader("RESPONSE_STATUS");
   var ex = null;
   if (responseStatus == "ApplicationException") 
   {
      var netEx = JSON.parse(response);
      ex = new ProgramException(netEx.Message, netEx);
      ex.Reasons = netEx.Reasons;
   }
   else if (responseStatus == "UnexpectedException") 
   {
      var netEx = JSON.parse(response);
      ex = new FatalException(netEx.Message, netEx);
   }

   if(ex!=null)
   {
      if (async)
         ShowException(ex)
      else
         throw ex;     
   }

   //Normal response handling.
   if (async)
    callback(response)    
   else
    returnValue = response;
  }
  error: function(XMLHttpRequest, textStatus, errorThrown) {
   var ex = [GET_EXCEPTION_INFO];
   if(async)
    ShowException(ex)
   else
    throw ex;
  }
 });

 //async => returnValue == null;
 //sync  => returnValue == response;
 return returnValue;
}

La parte modificada es la de la sección “success” -previa al “Normal response handling”-. Verán que ahora, para determinar si hubo una excepción se consulta el encabezado de la respuesta que incluimos en el manejo de excepciones del lado del servidor -xhr.getResponseHeader("RESPONSE_STATUS")-. No importa qué excepción de negocio se haya producido, este header aparecerá siempre que ésta derive de ApplicationException. En la serialización JSON se devolverán todas las propiedades del tipo específico –en un entorno de desarrollo, recordémoslo siempre- ya que utilizamos JsonSerializer.

Por ejemplo, si la excepción lanzada es aquella ShowCaseException de la que hablamos al principio, sabremos por el header que deriva de ApplicationException y al mismo tiempo conservaremos la propiedad “Reasons”, específica del tipo, al serializar. Si prueban verán que aparece en el objeto netEx cuando se ejecuta var netEx = JSON.parse(response);

Cuando deserializamos la excepción la encapsulamos en un objeto conocido para el cliente. Esto es para reflejar el hecho de que para el cliente de javascript sólo existen dos objetos derivados de Exception relevantes: los de negocio (excepciones “de programa” y los de sistema (“fatales”). Para ello creamos sus clases correspondientes, inspiradas un poco en las de .Net (una excepción base y dos derivadas), pero en estilo javascript. Esto no es necesario, pero me resultó muy cómodo (veremos luego):

//Exception base
ExceptionBase = function (type, message) {
    this.Type = type;
    this.Message = message;
};

//Program Exception
ProgramException = function (message, netException) {

    this.Reasons = [];
    this.NetException = netException;

    if (typeof message != "undefined" && message != null)
        this.Message = message;

};
ProgramException.prototype = new ExceptionBase("ProgramException", "Operation Error");

//Fatal Exception
FatalException = function (message, netException) {

    this.NetException = netException;

    if (typeof message != "undefined" && message != null)
        this.Message = message;

}

FatalException.prototype = new ExceptionBase("FatalException", "Unexpected Error");

Son estas dos clases las que nos permiten codificar luego este tipo de instrucciones:

var netEx = JSON.parse(response);
ex = new ProgramException(netEx.Message, netEx);
ex.Reasons = netEx.Reasons;

Hasta aquí, un paso más en la batalla. Nos resta:

  • Determinar cómo tratar los errores en la comunicación.
  • Determinar cómo mostrar toda esta información (hasta ahora no hicimos nada con el método “ShowException”).

Hasta la próxima, nos leemos. (Actualización: sigue en .Net MVC + jQuery: manejo de excepciones. IV: el lado del cliente.)

miércoles, 17 de febrero de 2010

.Net MVC + jQuery: manejo de excepciones. II: El problema.

[Continuación de .Net MVC + jQuery: manejo de excepciones. I: Requerimientos.]


Tengamos presente nuestro ejemplo de manejo de errores y excepciones en $javascript (jQuery):

$("#Save").click(function () {
 try {
  $("#CommentForm").valid();
  Save({
   User: $("#User").val(),
   Date: $("#Date").val(),
   CommentText: $("#CommentText").val()
  });
 }
 catch (ex) {
  ShowException(ex);
 }
});

Por el lado de las excepciones, tenemos en principio aquellas generadas por nuestro código. En el ejemplo, serían aquellas que arroje la función valid() luego de verificar los datos ingresados por el usuario.

Luego tenemos aquellas que se generen durante la llamada al servidor. Asumiendo que Save() hace una llamada $.ajax para enviar los datos al servidor, tenemos en realidad dos escenarios posibles: sincrónico y asincrónico.

Si la llamada al servidor es sincrónica Save() debe hacer un throw en algún punto del código con la información necesaria para que sea atrapado por el catch del código de arriba y ShowException muestre un mensaje al usuario.

Si la llamada es asincrónica deberíamos pasarle a Save() un parámetro callback, una función a ser invocada cuando el servidor responda. Por ejemplo (muy esquemáticamente):

Save({
 User: $("#User").val(),
 Date: $("#Date").val(),
 CommentText: $("#CommentText").val(),
 function(response){
  alert(response);
 }
});

En este segundo caso es el método Save() el que debe internamente contemplar la posibilidad de excepciones y decidir, de acuerdo con la respuesta, si invoca al callback o a ShowException.

Creo que en algunas situaciones es necesario el uso de llamadas asincrónicas, pero que en general es más fácil manejar la lógica sincrónica, por algo es por lo que se empieza al aprender a programar. Así que personalmente prefiero que cada función javascript que se comunica con el servidor dé la opción de pasar o no una función de callback.

Si juntamos estas dos opciones y hacemos un boceto de Save() quedaría algo por el estilo de:

function Save( data, callback )
{
 var async;
 if( typeof callback == "undefined")
  async=false;
 else
  async=true;
 
 var returnValue = null;
 
 $.ajax({
  url: "Comments/Save",
  data: data,
  async: async,
  success: function (response, status, xhr) {

   //Exception handling.
   if( [RESPONSE_IS_EXCEPTION] )
   {
    var ex = [GET_EXCEPTION_INFO];
    if (async)
     ShowException(ex)
    else
     throw ex;     
   }

   //Normal response handling.
   if (async)
    callback(response)    
   else
    returnValue = response;
  }
  error: function(XMLHttpRequest, textStatus, errorThrown) {
   var ex = [GET_EXCEPTION_INFO];
   if(async)
    ShowException(ex)
   else
    throw ex;
  }
 });

 //async => returnValue == null;
 //sync  => returnValue == response;
 return returnValue;
}

En el ejemplo se asume que si se pasa un parámetro de callback la llamada debe ser asincrónica. En este caso se ejecuta $.ajax, el flujo sigue en la línea siguiente y returnValue será null cuando Save() termine. En algún momento posterior, cuando el servidor responda, jQuery invocará al código que se pasa en el parámetro success de $.ajax. De ser una llamada sincrónica se ejecutará $.ajax e inmediatamente el código del parámetro success.

Si se produce un error o una excepción no controlada del lado del servidor $.ajax invocará a la función especificada en el parámetro “error”. Lo mismo sucederá ante errores en la comunicación en sí.

Tanto en error como en success debemos determinar, de alguna manera (que también veremos luego, en el ejemplo estos agujeros blancos están entre corchetes), si hubo una excepción o error. Si la hubo armamos un objeto “ex” con la información que necesite ShowException. Si no, devolvemos el valor.

La diferencia entre sincrónico y asincrónico en este punto es que en el caso de ser una llamada sincrónica hacemos un throw (que atrapará el catch de Save) o llamamos a la función callback o devolvemos el valor a través del retorno de la función, mientras que en el caso de la llamada asincrónica debemos manejar aquí mismo la excepción o llamamos a la función callback. Si hiciéramos un throw en este punto, durante una llamada asincrónica, la excepción sería atrapada por el navegador y el usuario vería un error de javascript (en el mejor de los casos. En el peor, no vería nada y pensaría que sus datos fueron grabados correctamente).

Hasta aquí las excepciones. Vamos ahora a contemplar la posibilidad de errores. Si no tienen en claro la diferencia les recomiendo –otra vez- hacer un paréntesis y leer este post.

A modo de resumen de ese post, recordemos que si al momento de grabar los datos “La fecha del comentario ingresado corresponde a un informe ya aprobado”, eso es una excepción (una situación contemplada en el código en forma de validación, un throw en nuestro código del lado del servidor. Es un mensaje que tiene utilidad para el usuario, que debe indicar que él ha cometido un error y no el sistema). Pero si al momento de grabar el sistema intenta insertar un valor nulo en donde no se puede, eso es un error (una excepción para el framework o la base de datos, pero una situación no contemplada en nuestro código).

¿Dónde deberíamos esperar errores? Fácil, en todos lados. Lo inesperado puede por definición aparecer en cualquier momento. Pensemos qué situaciones pueden darse y qué hacer en cada caso:

  • Un error en el código de negocio del lado del servidor: en este caso podríamos enviar una respuesta que le indique al código javascript (en success) que se ha producido un error de sistema y a ShowExceptions que debería “romper” la pantalla impidiendo que el usuario continúe utilizando la aplicación o mostrar información detallada, dependiendo de si estamos en un ambiente de desarrollo o testing.

    Un ejemplo sería el intento de utilizar una variable que está en nulo, pero también que la base de datos no esté disponible o muy ocupada, o un timeout de base de datos. Para este caso tendríamos una excepción del lado del servidor, pero que no deriva de ApplicationException.

  • Un error en la infraestructura del lado del servidor: en este caso el problema no está en nuestro código y no es atrapado por él. Es el que se daría si el servidor web está muy ocupado. Puede ser antes o después de la operación, pero en todo caso no es controlado por nosotros.

    Aquí no tendríamos una excepción de .Net del lado del servidor, sino que éste, sin que podamos evitarlo, devolvería un error HTTP (típicamente un error 500, aunque podría ser otro). En este caso es jQuery el que atraparía el error e invocaría a la función especificada en el parámetro “error” de la llamada $.ajax.

  • Un error en la llamada, del lado del cliente: en este caso la llamada $.ajax jamás llega al servidor (por ejemplo si la url es incorrecta o está mal formada).

    Aquí hay un punto de debate: dado que el error se produce enteramente del lado del cliente no tenemos posibilidad de registrarlo, sólo nos enteraremos de él a través de una comunicación del usuario (por mail, teléfono, o como sea). La única información de la que dispondremos será aquella que le mostremos por pantalla… pero no es una buena práctica mostrar detalles internos al usuario final… Pero esto es último es discutible, ya que si vamos a suponer un error provocado por un usuario malicioso es de todas maneras información a la que él puede acceder, ya sea que se la hagamos más fácil o más difícil.

    Lo que es seguro, volviendo al tema, es que ShowExceptions tiene que manejar también esta situación y, por lo menos, asimilarla a la indicada en los puntos anteriores y darle el mismo tratamiento.

  • Un error en el código javascript, del lado del cliente. Es decir, un error de javascript ya sea en la función Save() o en el manejador del evento click. Este tipo de errores sería atrapado por el catch más externo de todos, el de la función que asociamos al evento click.

    En este caso el objeto “ex” que ShowExceptions recibirá como parámetro no estaría armado por nosotros sino por el motor de javascript (y lo que es peor, será ligeramente diferente entre navegadores, e incluso entre distintas versiones del mismo navegador). Así que tenemos que contemplar también la posibilidad de que a ShowExceptions le llegue este otro objeto “más o menos desconocido” proporcionado por el motor de javascript.

  • Un error en el código de manejo de errores. Es decir, un error de javascript adentro de ShowExceptions. Créanme que el andamiaje necesario para todo este control es lo suficientemente complejo como para que tenga sus propios errores, y éste es el peor escenario: el error en ShowExceptions nos estaría ocultando aquél que le dio origen, y deberemos resolverlo antes de enfrentarlo. Dado que ShowExceptions es la última fortaleza del funcionamiento del sistema deberá ser a prueba de balas: ya no importa cómo ni en qué situación, su misión es mostrarnos todos los errores que se hayan producido (propios y ajenos) sin perder información y asegurarse de que la pantalla del lado del cliente quede bloqueada. Tarea ingrata si las hay, pero alguien tiene que hacerlo.

Resumamos, como para ir terminando este post ya demasiado extenso, las situaciones que debemos contemplar:

  • Excepciones generadas desde nuestro código en javascript (las que se generen en la función valid() de validación de datos ingresados por el usuario).
  • Llamadas sincrónicas y asincrónicas. La diferencia está en hacer un throw o llamar a ShowExceptions en la función que se comunica con el servidor.
  • Errores o excepciones generados del lado del servidor. En este caso la comunicación ha sido exitosa.
  • Errores en la comunicación en sí, provocados por la conexión (“se cayó la red”) o por el código (“url mal formada).
  • Errores en el código javascript que implementa una funcionalidad específica.
  • Errores en el código javascript de control de errores.

La vida no es fácil… luego seguimos. (Actualización: sigue en .Net MVC + jQuery: manejo de excepciones. III: Atrapar errores del lado del servidor y comunicarlos controladamente al cliente.)

lunes, 15 de febrero de 2010

.Net MVC + jQuery: manejo de excepciones. I: Requerimientos.

Me pasé las últimas semanas trabajando sobre la beta de Visual Studio 2010 (acaba de salir el Visual Studio 2010 RC) armando un esquema de trabajo (un mini-framework) para proyectos MVC, en un intento de materializar la experiencia acumulada en otros proyectos en la forma de herramientas encapsuladas, probadas y listas para reutilizar.

Uno de los puntos que más trabajo me ha llevado es el manejo de excepciones. Un tema traicionero, simple a primera vista pero complicado y retorcido a medida que se avanza. Siempre lo tuve cubierto (es decir, sin graves problemas) con un poco de alambre, pegamento y parches aquí y allá, y quise aprovechar este nuevo comienzo en limpio para pensar un esquema más consistente y por lo tanto más sólido y fácil de mantener.

Comencemos, como corresponde, por los requerimientos. ¿Qué pretendo de a la infraestructura de un proyecto en cuanto a manejo de errores y excepciones (teoricé sobre la diferencia hace un tiempo, en Errores y excepciones)?

En principio, un uso estándar y transparente. Por ejemplo, en el botón “Save” de una vista, el programador debería codificar, en javascript (+jQuery, por supuesto):

$("#Save").click(function () {
 try {
  $("#CommentForm").valid();
  Save({
   User: $("#User").val(),
   Date: $("#Date").val(),
   CommentText: $("#CommentText").val()
  });
 }
 catch (ex) {
  ShowException(ex);
 }
});

En el ejemplo anterior, el bloque try…catch captura los errores de validación disparados por la instrucción $("#CommentForm").valid(); (es el plugin jQuery Validation) y la función ShowException los muestra en una forma amigable al usuario. Este no es el comportamiento “normal” de la función valid() de jQuery Validation, hay que codificarlo.

Pero no sólo eso. La función Save del ejemplo debe enviar los datos al servidor y el servidor debe pasarlos al modelo de negocio y esperar excepciones. Excepciones del tipo “La fecha del comentario ingresado corresponde a un informe ya aprobado” y cosas por el estilo, tan molestas para nosotros como necesarias para nuestros clientes. Es decir que se comunicará con una acción en un controlador, supongamos:

namespace ShowCase.Web.Controllers
{
    public class CommentsController : Controller
    {
  public bool SaveComment(CommentsModel model)
  {
   model.Save();
   return true;
  }
    }
}

Vemos que la acción devuelve siempre true. Esto es porque espera que los problemas se comuniquen a través de las excepciones. Un ejemplo esquemático para el método Save del modelo sería:

public class CommentsModel
{
 public void Save()
 {
  FinancialReport financialReport = FinancialReports.GetByDate(this.Date);
  if(financialReport.Status == ReportStatus.Approved)
   throw new ApplicationException("The comment's date points to an approved report.");
  
  FinancialReports.SaveComment(financialReport, this);
 }
}

El manejo de este tipo de excepciones generadas del lado del servidor debería ser, para el programador de la funcionalidad, idéntico al de las generadas del lado del cliente. Así que tenemos que llegar, de alguna manera, desde el throw en el método Save de CommentsModel, al catch en nuestra función de javascript.

Para complicar las cosas, agreguemos los errores. Estamos hablando ahora de verdaderos errores del sistema: errores de codificación, de conexión con la base de datos, de conexión entre el cliente y el servidor web, de javascript, etc. Errores del tipo que solemos llamar catastróficos, pero que yo prefiero denominar inesperados, como para restarle dramatismo al asunto (un cliente que no llamaría a las 3 de la mañana por un error “inesperado” tal vez sí lo haga por uno “catastrófico”).

Es el tipo de errores, en definitiva, que deberían “romper” o “cerrar” la aplicación, puesto que se ha arribado a un estado no contemplado, y cualquier acción posterior podría derivar en datos corruptos, inconsistentes, o –por lo menos, y más probablemente- en nuevos, más molestos y más extraños errores.

La función ShowException del ejemplo debería diferenciar entre las excepciones de negocio y los errores. Para los primeros mostrará un mensaje al usuario, y para los segundos deberíamos grabar un archivo de log, y podríamos establecer dos comportamientos posteriores:

  • uno para desarrollo o pruebas, mostrar por pantalla toda la información disponible acerca del error, y dejar que la aplicación siga abierta (el desarrollador o tester decidirá por sí mismo si puede continuar o no sin problemas),
  • y otro para producción, limpiar completamente la pantalla y mostrar un mensaje de error escueto invitando al usuario a reiniciar la aplicación o a llamar al administrador del sistema (siempre y cuando no seamos nosotros).

Es importante que, en entornos de producción, no sólo la función ShowException no muestre más de lo que debe, sino que el servidor mismo no envíe más información que la estrictamente requerida.

Todos estos requerimientos deben implementarse, dentro de lo posible, en una forma transparente al programador de la funcionalidad. Personalmente considero esta transparencia como algo muy importante, y la resigno (obligando al programador a derivar de ciertas clases, a implementar o a invocar ciertas funciones especiales) sólo cuando la alternativa es imposible o extremadamente más complicada. Cuando programamos una funcionalidad tenemos la cabeza puesta en facturas, aprobaciones, balances, fechas de cierre… en ese contexto, dónde fue generada la excepción o qué tipos de excepción hay y cómo deben transmitirse desde su origen hacia el cliente, son sutilezas que de requerir tratamiento especial serán seguramente pasadas por alto.

Pero, en definitiva, qué tan importante es esta transparencia o cuándo una alternativa es “imposible” o “extremadamente complicada” dependerá de cada uno. El hecho es que gracias a .Net MVC y jQuery podemos lograr un nivel muy aceptable de transparencia a través de diferentes puntos de intercepción y extensión convenientemente ubicados en la infraestructura del proyecto.

Suficiente para una primera entrega. En la próxima –en breve- analizaremos más en detalle los requerimientos en busca de las situaciones más comunes y los problemas que deberemos enfrentar al momento de picar el código.

Continúa en .Net MVC + jQuery: manejo de excepciones. II: El problema.

sábado, 13 de febrero de 2010

Frases: jQuery.

Javascript ha muerto, ha nacido $(javascript).

Una de mi cosecha para la posteridad.

jueves, 28 de enero de 2010

Frases: $.noop()

“$.noop returns the sound of one hand clapping.”

$.noop devuelve el sonido de una sola mano aplaudiendo.”

- Karl Swedberg comentando la documentación de $.noop() de jQuery.

Para los despistados: $.noop() es una función de jQuery que -como su nombre indica- no hace nada.

jueves, 22 de octubre de 2009

Plugimanía, plugiadicción, plugidependencia, pluginitis.

jQuery está buenísimo. Lo uso todos los días, lo dije una y otra vez, y no me cansaré de repetirlo.

Su mayor fortaleza, opino, no radica en su reducidísimo (como corresponde) core sino en la amplísima comunidad de desarrolladores que aportan, oficial o extraoficialmente, nuevas funcionalidades encapsuladas en forma plugins.

Yo imagino que, como en toda comunidad, se sigue la regla del 90-10: 10% aporta activamente y 90% utiliza las funcionalidades provistas “as is” o a lo sumo con un poco de manoseo. Entonces, si derivamos la cantidad de desarrolladores-usuarios a partir de la cantidad de desarrolladores-aportadores obtendremos… no sé, un montón de gente.

Los usuarios-desarrolladores son aquellos que encuentran en jQuery la forma de implementar en cinco minutos aquella funcionalidad que estaban buscando. Es decir que llegan a jQuery a partir de una necesidad concreta que usualmente satisfacen rápidamente para proseguir con su camino, habiendo sumado una herramienta más a su caja de experiencias y dispuestos a volver a utilizarla allí donde consideren que sea más apropiada que otras.

Luego tenemos a los plugimaníacos, aquellos quienes ya han utilizado intensivamente la herramienta y por ello buscan en jQuery el plugin mágico que resuelva algún nuevo problema. Son los que lo crean si es que no lo encuentran o mejoran uno ya existente si es que no cubre totalmente sus necesidades… o por simple y sana diversión. Es aquel 10% del que hablábamos.

Una porción menor de desarrolladores siente que tiene que hacer eso tan difícil en jQuery, que necesita hacerlo en jQuery. Desarrolladores que prefieren implementar un nuevo plugin aún ante la existencia de herramientas o soluciones más apropiadas para el requerimiento concreto en el que están trabajando.

Ahí donde una pequeña animación en flash o un applet de java o silverlight parezcan soluciones más razonables o naturales veremos a los adictos decir “eso puede hacerse con jQuery” y poner manos a la obra, prestos a demostrar sus (en general extraordinarias) habilidades y a defender el orgullo de su herramienta (¡hablamos de jQuery, malpensados!) ante las demás. Son, del 10% de aportantes activos, el 1% que lleva jQuery al límite y hacia nuevos horizontes.

Pero, dentro de estos últimos, otra minoría, los plugiadictos, no sólo se bandean para el lado de lo extremadamente complejo, sino que también lo hacen hacia lo extremadamente simple. Si son de buscar en el mundo jQuery se habrán encontrado con más de uno que ha creado algún plugin que encapsula la más trivial y estándar de las tareas o que ha utilizado una ferrari (del tipo jqGrid) para la más aburrida de las tablitas no paginadas.

Hasta aquí toda gente normal, simpática, o a lo sumo un tanto extravagante. Pero cuando avanzamos en la progresión exacerbando un tanto más estas conductas pasamos de castaño a oscuro y entramos en el terreno de las patologías. Citaré dos de ellas: la plugidependencia y la pluginitis. Es posible, como se verá, que un caso extremo de la primera desemboque en la segunda.

El plugidependiente no es (ni puede ser, se verá) un aportador al repositorio de jQuery. El plugidependiente, debido al uso y abuso de la herramienta (ya sea motivado por él mismo o por su entorno) ha olvidado (o nunca aprendido) que existen cosas llamadas HTML, Javascript o DOM.

Lo reconocemos por un clásico afán de probar que “eso no puede hacerse” mediante la búsqueda infructuosa de un plugin de jQuery que lo haga. En su mundo aquello que jQuery no pueda hacer no puede hacerse, y punto.

Otras características remarcables, en casos extremos, son el desconocimiento de HTML más allá de los tags más básicos (aquellos que sirven de soporte a plugins de jQuery, como DIV y TABLE) y recurrentes intentos de utilizar sintaxis de jQuery en otros contextos (Javascript, o más extremo aún: C#, Visual Basic o cualquier otro lenguaje). Intentos que suelen culminar con un asombrado “¿ah, eso es de jQuery?”.

En el extremo absoluto de esta patología el desarrollador desconoce jQuery como extensión y cree que $(“DIV”).each( function(){…} ) es la sintaxis propia de Javascript o parte de HTML.

Como sucede con todas las herramientas y cosas en general, el abuso vuelve nocivo aquello que en el uso es beneficioso. Llegamos a la pluginitis.

El primer síntoma es una página web que tiene un título y una tablita y tarda unos 5-10 segundos en descargar. ¿Qué sucede? Pues nada, que tiene referencias a jQuery, jQuery UI, jqModal (pero… ¿no hay algo parecido en jQuery UI?) y unas 10 o 20 referencias más a archivos .js externos sin minimizar que no se utilizan absolutamente para nada… tal vez la página ni siquiera tenga un script, sino que simplemente… bueno, pluginitis.

Otros síntomas son: un browser (que no es el IE) que se arrastra, puro contenido generado dinámicamente del lado del cliente, imposibilidad de seguir la generación de ese código o de averiguar siquiera cual de las 3 o 4 extensiones parecidas de las incluidas en la página es la que está dibujando ese contenido.

¿Cómo salir? El primer paso es reconocer el problema. El segundo buscar ayuda y el tercero es una dura combinación de sacrificio, paciencia y constancia.

De mi parte, espero que el pequeño esfuerzo de escribir este post se justifique por haber ayudado, aunque sea a un sólo plugidependiente o pluginoso, a abrirse a una nueva vida. Si ése es tu caso, agradeceré tu comentario.

jueves, 10 de septiembre de 2009

Estar en la pomada.

No tiene nada que ver con lo que iba a escribir, simplemente me dio curiosidad el origen de la expresión, y aquí va:

Cuenta la leyenda que hace siglos, en Bretaña los soldados partían a la batalla numerados. Cada guerrero exhibía un pendón con un determinado número de manzanas […]

Llegados tiempos de escasez, entre otras muchas cosas empezó a faltar el ungüento con que se curaban las heridas de los guerreros a su vuelta del combate, de modo que se decidió usar la clasificación de la pomada para ver quién tenía derecho a que sus heridas fueran curadas con el escaso ungüento […]

De este modo, se generalizó primero la expresión estar en el ungüento, que, posteriormente, cuando el grupo de pendonados con manzana pasó a ser conocido como la pomada, quedó como estar en la pomada, como sinónimo de estar en el grupo de los primeros clasificados.

(extracto del blog infulas)

Aclarado el punto (mucho más interesante que lo que sigue, por cierto) voy a la pequeña reflexión que lo motivó: desdesarrollo está en su máximo de visitas, pegando un interesante salto respecto del mes anterior.

Tan interesante ha sido el susodicho que me he puesto a revolver el analytics con un poco más de obsesividad que de costumbre en busca de su origen.

Quería corroborar que, encantados con mi prosa, inteligencia y capacidad analítica (e ironía), los lectores caían en masa.

Finalmente no me ha quedado otra que enfrentar la verdad: la causa ha sido la aparición de ciertas palabras en los títulos, palabras que han atraído especialmente la atención de google. La marea no es de lectores sino de buscadores, y de ellos una ínfima parte pasa más de 5 segundos en el blog.

Somos, en definitiva, más o menos los mismos de siempre. La única diferencia es que en vez de estar charlando en una esquina de pueblo lo estamos haciendo en medio de una concurrida estación de trenes.

¿Qué es lo que ha transformado aquella esquina solitaria en este concurrido lugar de paso? Las palabras han sido, no muy soprendentemente: “jQuery” y “AJAX” (de la serie Ajax: C# .Net 3.5 + jQuery).

Qué mejor indicador de cuáles son las tecnologías que hay que dominar para estár “en la pomada” hoy en día que el  constante crecimiento de las búsquedas que se realizan alrededor de jQuery:

Cada vez me convenzo más de que el futuro de la web (y de la mayoría de las interfaces de usuario) será escrito en javascript.

viernes, 4 de septiembre de 2009

Microsoft .Net MVC Framework 1.0 … en serio.

video-421 Silencio de radio estos días, ¿lo notaron? Tal vez sí –eso espero-, ya que corté un período de extrema verborragia que abarcó las últimas semanas, aunque –reconozco- con más relleno que ideas.

El relativo silencio tiene su origen en una sucesión de días –y noches-febriles en los que se juntaron el inicio de las clases -con su carga de revisiones, correcciones e improvisado planeamiento (tal cosa existe)- y el inicio de un nuevo proyecto en el trabajo, que es lo que quería comentar en este post, y –por fin- voy al grano.

Este proyecto es mi primera aplicación desarrollada con el ASP.Net MVC de Microsoft y es también el primero que se desarrolla con esta tecnología en la empresa para la que trabajo.

Creo que puede ser interesante ir comentando a medida que avanzo. Muchos de ustedes son programadores y seguramente han trabajado, jugado o leído algo sobre el MVC de Microsoft, y saben muy bien que el partido se juega muy diferente al entrenamiento con ejemplos y tutoriales: la funcionalidad requerida no es negociable (salvo en pequeños detalles), hay que terminar en tiempo y forma, y la presión de no saber (no saber estimar, no saber cómo se hacen tareas comunes, no saber si el framework cubre o no determinada necesidad, no saber si algo es fácil o difícil…) y tener que cumplir, pesa.

El Proyecto.

El proyecto es pequeño, dos programadores con una agenda de aproximadamente cuatro semanas de trabajo. Es una típica aplicación de gestión del estilo pantalla-contra-base-de-datos, con dos o tres complicadas y el resto ABM’s. Pero cargamos también con cierta obligación de entregar como subproducto la experiencia plasmada en herramientas puedan reutilizarse y métodos y prácticas estándar que puedan transmitirse… y errores que no vuelvan a repetirse.

Experiencia.

Somos dos programadores senior, y me toca aportar la experiencia previa de haber trabajado con el patrón MVC los últimos 3 años, si bien implementado sobre un framework desarrollado ad-hoc y mantenido, corregido y mejorado constantemente por el mismo equipo durante ese largo período de tiempo.

Herramientas: primeras decisiones.

Ya dije que trabajamos sobre C#, sobre el .Net 3.5 con el MVC 1.0, resta aclarar que la aplicación trabaja sobre Oracle. Demasiadas innovaciones para un sólo proyecto (en mi caso), así que decidí implementar entidades y acceso a datos con plantillas de CodeSmith, una herramienta de la cual no estoy enamorado pero manejo, y que para mí representa un problema menos.

De lado del cliente utilizamos el framework de javascript jQuery (algo sin lo cual ya no vale la pena vivir), con jQuery UI y el plugin jqGrid (una súper grilla que hace de todo).

Para ser sincero tengo que aclarar que robé la estética (hojas de estilo y demás) de otra aplicación ya desarrollada para el mismo cliente, así que no puedo opinar demasiado en cuanto a las facilidades para el diseño de estas herramientas, salvo que son muy fáciles de reproducir copiando y pegando archivos.

Curva de aprendizaje de MVC.

Estoy comentando aquí la curva de aprendizaje del patrón MVC en sí mismo: cuánto se tarda en aprender los conceptos, los términos, la nomenclatura y darse un par de palos contra la pared hasta llegar a ese punto en el que “vemos en MVC”, es decir cuando logramos encajar intuitivamente las funcionalidades requeridas dentro del patrón.

Así que no vale mi experiencia, sino la de mi compañero de proyecto –programador senior con buena predisposición y mucha experiencia con .Net-, que creo que luego de tres días completos de idas y vueltas (más o menos) ya le está tomando la mano (¡que comente!).

Para recomendar: comenzamos con estos videos cortos de Microsoft, un muy buen punto de partida.

Impresiones del .Net MVC 1.0.

Mi primera impresión fue “esto es buenísimo, es más o menos la misma arquitectura que venía implementando a mano, con los detalles escabrosos ya resueltos y muy bien integrada con el Visual Studio”.

Pero apenas completados los primeros tutoriales (de esos en los que armamos una aplicación -tan funcional como inútil- de punta a punta en 5 minutos) y comenzado el trabajo “en serio”, aparecieron las falencias. Lo que está está razonablemente bien implementado pero hay algunas cositas bastante desagradables, y mi impresión actualizada es que “le faltan cosas”, y que probablemente la próxima versión (2.0) represente una mejora muy significativa.

Puntos fuertes: la sobria estructura de la plantilla inicial nos ayuda a mantenernos organizados, una muy bien implementada relación entre vista y controlador, sencilla y flexible a la vez, la extensibilidad del framework en general (siempre hay dónde meter los dedos para adaptarlo a nuestra forma de trabajo). Por otro lado hay una comunidad grande y muy activa, con muchos y muy buenos recursos.

Por lo menos tengo que reconocer que el resultado final, el html, es mucho… muchísimo más razonable y manejable que el que produce como salida el .Net “sin MVC”: no más viewstate, no más update panel, no más ver una cosa del lado del servidor y otra (compleja, pesada y que encima no funciona) del lado del cliente.

Puntos débiles: el lado javascript, concretamente en lo más fundamental: carece de un método de conexión sencillo que soporte AJAX entre el cliente y el servidor. Las librerías de Microsoft… miré un poco la documentación y bien gracias, el código ni lo abrí. Como siempre, una copia berreta y muy complicada de algo que es muy fácil de implementar con herramientas ya existentes (en breve la adaptación de la serie al MVC). jQuery está ahí, viene con el paquete casi como una sugerencia, pero no vi ningún tipo de integración real (si alguien puede desasnarme éste es el momento).

Otra que no me gustó es cómo quedan –por defecto- las vistas cuando hacemos pantallas “reales” (y no formularios tontos de ejemplo)… esos “helpers” me recuerdan mucho a ASP 3.0 y la verdad que la legibilidad final, si implementamos las cosas tal como dicen los tutoriales… y… es una mezcla horrorosa de javascript, html y tags del lado del servidor. Pero, veremos en breve, eso es salvable.

La curva de aprendizaje del .Net MVC.

Así que, en resumen: si ya conocemos el patrón MVC y también el framework .Net, veremos que ésta implementación es un grandioso punto de partida que nos resuelve lo básico, un poco más, y nos da la base para armar un esquema de desarrollo realmente ágil.

Pero no está regalado, hay que hacerlo. Los detalles tras bambalinas (vistas tipadas, implementación de validaciones del lado javascript, helpers, bindeo) se vuelven un tanto complejos cuando se los quiere forzar por fuera del muy limitado uso estándar para el que están pensados pero, como dije antes, todavía no tuve un problema al que no le haya encontrado solución con un poco de ayuda de google.

Yo voy… pongámosle una semana a tiempo completo, y me considero en forma para esta pequeña primera aplicación con intenciones de sentar una base para algo mejor.

Promesas.

Mucha teoría, mucho discursito en el aire, ya lo sé. Voy a ir armando algunos posts más complejos con ejemplos y algunas soluciones implementadas. No hay mejor manera de validar la razonabilidad de un esquema que mostrarlo y prestar oído a los comentarios.

martes, 4 de agosto de 2009

Ajax: C# .Net 3.5 + jQuery (VI) – Proxies de objetos para javascript, el lado del cliente.

Con esta entrada finalizamos esta serie en la que hemos prototipado diferentes proxies en javascript: para llamar a métodos web, a servicios, y para utilizar objetos de C# “como si fueran de javascript”.

Para el final de la entrada anterior teníamos resuelto un servicio (InvokerService.asmx en nuestro código de ejemplo) que nos permitía recibir un objeto de cualquier tipo en JSON, deserializarlo, ejecutar un método indicado por un parámetro y devolver tanto el resultado del método como una representación del estado final del objeto. Quedó entonces pendiente para esta entrada la parte del cliente, es decir, un objeto javascript que “simule” ser ese objeto de C#.

Creo que lo más fácil es primero hacer un proxy para un tipo específico a mano y luego utilizar ese código como modelo para codificar un procedimiento que genere un proxy similar, pero ya tomando el tipo como parámetro.

Recordemos lo que este proxy debe hacer:

  • Al invocar un método en este objeto de javascript, internamente  pasa la llamada a un único web service (InvokerService.asmx) que recibe el objeto serializado, el tipo equivalente en C# y la orden de ejecutar un método con los parámetros correspondientes, que también recibe serializados.
  • El servidor devuelve el resultado del método y el estado final del objeto serializado, ya que esa ejecución pudo haber modificado alguna de sus propiedades.
  • De vuelta en el cliente copiamos las propiedades del objeto que recibimos a sus correspondientes en el de javascript y devolvemos el resultado del método.

Empecemos entonces con el código. Buen momento para recordarles que la solución de VS2008 completa está subida a Google Code.

Para que la creación del proxy por código sea más sencilla tenemos que trasladar toda la funcionalidad que podamos fuera del proxy, a una función estática que podamos incluir en nuestro archivo JSProxy.js.

Así, vamos a crear primero una función auxiliar, Page.Ajax.Invoke, que es la que hará la mayor parte del trabajo:

Page.Ajax.Invoke = function(type,obj,method)
{
    var params = null;
    
    if(arguments.length > 3)
    {
        var params = new Array();
        for(var i=3;i<arguments.length;i++)
            params.push(JSON.stringify(arguments[i]));
    }
    
    //función para pasar a JSON.stringify que excluye __type__ de la serialización del 
    //objeto ya que el tipo se pasa en type.
    var excludeType = function(key,value){
        if(key!="__type__")
            return value;                                
    };
    
    var jsonObj = Page.Utils.JSON.stringify(obj, excludeType);
    var jsonParams=Page.Utils.JSON.stringify(params);

    var ret = Page.InvokerService.Invoke(type, jsonObj, method, jsonParams);

    for(var i in ret.TargetObject)
        obj[i] = ret.TargetObject[i];
    
    return ret.ReturnValue;                            
}

La declaración de esta función es un poco rara, porque la complica el hecho de que no sabemos cuántos parámetros requiere el método a invocar. Podríamos pasar un array, pero es más fácil para la codificación si los pasamos como argumentos adicionales, accediéndolos a través del array especial de javascript arguments.

Así, sólo están especificados los tres primeros parámetros, que son siempre los mismos: type (el nombre del tipo en C# a invocar), obj (una referencia al proxy javascript que inicia la invocación) y method (el método a invocar). A partir de allí, debemos pasarle los parámetros para el método a invocar, tantos como sean necesarios. ¿Enrevesado? Es más fácil codificarlo que decirlo, créanme.

Tomemos como modelo la clase Calc, el ejemplo de nuestro post anterior:

namespace AjaxConJQueryObjectProxy
{
    public class Calc
    {
        public Calc() { }

        public decimal Op1 { get; set; }
        
        public decimal Op2 { get; set; }

        public decimal Sum()
        {
            this.Op1 = this.Op1 + this.Op2;
            return this.Op1;
        }

        public decimal SumNumbers(decimal op1, decimal op2)
        {
            this.Op1 = op1;
            this.Op2 = op2;

            return this.Sum();
        }
    }
}

El proxy javascript queda como sigue:

Page.Ajax.Namespace("AjaxConJQueryObjectProxy");

AjaxConJQueryObjectProxy.Calc = function(){
   this.__type__ = "AjaxConJQueryObjectProxy.Calc";        

   this.Op1 = 0;
   this.Op2 = 0;

   this.Sum = function()
   {
       return Page.Ajax.Invoke(this.__type__, this, "Sum");
   }

   this.SumNumbers = function(op1, op2)
   {
       return Page.Ajax.Invoke(this.__type__, this, "SumNumbers", op1, op2);
   }
}

Como verán, quedó bastante simple. Es apenas un pasamanos estético entre el programador final y Page.Ajax.Invoke. Con eso como guía, vamos a crear la función estática ImportObject en nuestra clase JSProxy.

public static StringBuilder ImportObject(Type type, string executeServiceUrl)
{
    StringBuilder builder = new StringBuilder(400);
    builder.Append("$().ready(function(){");

    //crea el namespace.
    if (type.FullName.Contains('.'))
    {
        string ns = type.FullName.Substring(0, type.FullName.LastIndexOf('.'));
        builder.AppendFormat("Page.Ajax.Namespace(\"{0}\");", ns);
    }

    //declaración.
    builder.AppendFormat("{0}=function(){{", type.FullName);
    //variable interna con el nombre del tipo que representa.
    builder.AppendFormat("this.__type__=\"{0}\";", type.FullName);

    //inicialización de propiedades. Hay que crear una instancia de type
    //para determinar sus valores por defecto.
    object instance = type.Assembly.CreateInstance(type.FullName);

    FieldInfo[] fields = type.GetFields(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static);
    JavaScriptSerializer j = new JavaScriptSerializer();
    foreach (FieldInfo field in fields)
    {
        builder.AppendFormat("this.{0}=Page.Utils.JSON.parse(\"", field.Name);
        j.Serialize(field.GetValue(instance), builder);
        builder.Append("\");");
    }

    //si bien el tratamiento de las propiedades es igual al de las variables públicas (fields)
    //tal vez haya que hacer algo separado más adelante.
    PropertyInfo[] properties = type.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static);
    foreach (PropertyInfo property in properties)
    {
        builder.AppendFormat("this.{0}=Page.Utils.JSON.parse(\"", property.Name);
        j.Serialize(property.GetValue(instance, null), builder);
        builder.Append("\");");
    }

    //métodos.
    MethodInfo[] methods = type.GetMethods(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static);
    foreach (MethodInfo method in methods)
    {
        if (method.IsSpecialName)
            continue;

        //declaración
        builder.AppendFormat("this.{0}=function(", method.Name);
        //parámetros.
        ParameterInfo[] parameters = method.GetParameters();
        if (parameters.Length > 0)
        {
            foreach (ParameterInfo parameter in parameters)
                builder.AppendFormat("{0},", parameter.Name);
            builder.Remove(builder.Length - 1, 1);
        }
        builder.Append("){");
        //llamada interna a Page.Ajax.Invoke
        builder.AppendFormat("return Page.Ajax.Invoke(this.__type__, this, \"{0}\",", method.Name);
        //pasaje de parametros a Page.Ajax.Invoke
        if (parameters.Length > 0)
        {
            foreach (ParameterInfo parameter in parameters)
                builder.AppendFormat("{0},", parameter.Name);
        }
        builder.Remove(builder.Length - 1, 1);
        builder.Append(");");  //fin de la llamada a Page.Ajax.Invoke

        builder.Append("};");// fin del método.
    }

    builder.Append("};"); //fin del objeto.

    builder.Append("});"); //fin de $().ready( function(){
    return builder;
}

Lo único que hace es crear en un StringBuilder un código similar al que vimos más arriba, examinando con reflection el tipo que se le pasa como parámetro. El ajuste fino fue trabajoso… falta una coma, sobra un paréntesis, pero requiere más paciencia que inteligencia.

Vamos a probar. En el código C# de default.aspx incluimos la llamada a JSProxy.ImportObject que nos devuelve el javascript para el proxy de Calc, y lo incluye en el header de la página. El código debe verse así:

using System;

namespace AjaxConJQueryObjectProxy
{
    public partial class _Default : PageBase
    {
        protected override void OnPreLoad(EventArgs e)
        {
            base.OnPreLoad(e); 
            
            //AddHeaderScript es una función que en el ejemplo habíamos puesto como privada
            //de PageBase. Agrega un tag <script> en el header con el código que se le pasa.
            //Es muy útil, así que si la hacemos protected la podemos utilizar en todas las
            //páginas.
            string calcImport = JSProxy.ImportObject(typeof(Calc), "/InvokerService.asmx").ToString();
            base.AddHeaderScript(calcImport);           
        }
    }
}

Sin nada más que eso, ya podemos probar nuestro objeto Calc en javascript con una pequeña función de prueba:

function TestCalculadora()
{    
    var calc1 = new AjaxConJQueryObjectProxy.Calc();
    var calc2 = new AjaxConJQueryObjectProxy.Calc();
    
    calc1.Op1 = 3;
    calc1.Op2 = 7;
    
    calc2.Op1 = 1;
    calc2.Op2 = 4;
    
    alert(calc1.Sum()); //devuelve (operador1 = 3) + (operador2 = 7) = 10 => operador1
    alert(calc2.Sum()); //devuelve (operador1 = 1) + (operador2 = 4) = 5 => operador1
    alert(calc1.Sum()); //devuelve (operador1 = 10) + (operador2 = 7) = 17 => operador1
    alert(calc2.Sum()); //devuelve (operador1 = 5) + (operador2 = 4) = 9 => operador1                
};

¡Y –con suerte- funciona! Les recuerdo que el código completo está en Google Code.


Esta serie de posts son una especie de puesta al día de las ideas que se plasmaron en la arquitectura que utilizaba en mi trabajo en ElectroChance, la gran mayoría de ellas pergeñadas y/o recolectadas por Rick Hunter (por más que te cambies el nombre sabemos quién sos en realidad -sigan el link-), así que a él especialmente y a todos los que alguna vez conformaron ese dream-team, gracias.

lunes, 3 de agosto de 2009

Ajax: C# .Net 3.5 + jQuery (V) – Proxies de objetos para javascript, un servicio de ejecución.

Haciendo un recuento de las herramientas que estuvimos probando en las entradas anteriores de esta serie me imaginaba cómo sería a la hora de desarrollar una funcionalidad:

  • Creamos las funciones y procedimientos específicos para la comunicación entre cliente y servidor en el código C# de la (o las) página y los marcamos con WebMethod y ScriptMethod:
using System;
using System.Web.Script.Services;
using System.Web.Services;

namespace AjaxConjQueryJSProxy
{
    public partial class _Default : PageBase
    {
        [WebMethod]
        [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
        public static int Sum(int a, int b)
        {
            return a + b;
        }
    }
}
  • Sin hacer nada más que heredar la página de nuestra PageBase podemos utilizarlas desde javascript haciendo
var suma = Page.Sum(2,5);
  • Algo similar podemos hacer con funciones que consumimos desde un servicio Web. Tenemos que “registrarlo” en algún evento del ciclo de vida de la página, por ejemplo en el PreLoad:
using System;

namespace AjaxConJQueryObjectProxy
{
    public partial class _Default : PageBase
    {
        protected override void OnPreLoad(EventArgs e)
        {
            base.OnPreLoad(e); 
            /* AddHeaderScript es una función que en el ejemplo habíamos puesto como privada
            de PageBase. Agrega un tag <script> en el header con el código que se le pasa.
            Es muy útil, así que si la hacemos protected la podemos utilizar en todas las
            páginas. */
            base.AddHeaderScript(JSProxy.ImportService(typeof(WebService1), "WebService1.asmx").ToString());           
        }
    }
}
  • y sin más lo podemos utilizar desde javascript haciendo:
Page.WebService1.HelloWorld()

Bastante bien, pero… soy un tipo vago, muy vago. Estaba pensando que podría, en vez de tener muchos servicios y métodos, hacer algo que me permita codificar una clase común y corriente en C# y utilizarla directamente en javascript.

Digamos que tengo la clase Calc, que se comporta como una calculadora: tiene dos operadores y al efectuar una operación devuelve el resultado al tiempo que lo coloca en el primer operador, que hace de acumulador. También puede tener un método-atajo al que le paso los dos operadores, establece las propiedades y efectúa la operación en un sólo paso.

No tiene sentido darle mucha funcionalidad. Mi calculadora de ejemplo sólo sabe sumar:

namespace AjaxConJQueryObjectProxy
{
    public class Calc
    {
        public Calc() { }

        public decimal Op1 { get; set; }
        
        public decimal Op2 { get; set; }

        public decimal Sum()
        {
            this.Op1 = this.Op1 + this.Op2;
            return this.Op1;
        }

        public decimal SumNumbers(decimal op1, decimal op2)
        {
            this.Op1 = op1;
            this.Op2 = op2;

            return this.Sum();
        }
    }
}

Las propiedades Op1 y Op2 son los operadores, el método Sum los suma, devuelve el resultado y lo coloca en Op1. Por otro lado el “atajo” SumNumbers recibe los operadores como parámetros y luego de pasarlos a las propiedades correspondientes llama a Sum devolviendo el resultado.

Ahora, ¿por qué no puedo, en javascript y sin trabajo adicional (sin web services, web methods, ni nada por el estilo), hacer…

var calc1 = new AjaxConJQueryObjectProxy.Calc();

//prueba Sum
calc1.Op1 = 3;
calc1.Op2 = 7;
var res = calc1.Sum();
alert(res);

//prueba SumNumbers
res = calc1.SumNumbers(5,6);
alert(res);

…? ¿Eh? ¿Por qué?

Antes de empezar con todas estas pruebas hubiese dicho que es posible pero mucho más trabajoso que la comodidad que aporta, ya que al fin y al cabo llamar a un método web que haga lo mismo es muy fácil. Pero ahora estamos a un par de pasitos de conseguirlo con muy poco código:

  1. Creamos un objeto en javascript con las mismas propiedades que su equivalente en C#. Esto lo podemos hacer utilizando reflection en una forma similar a Automatizando la creación de proxies de javascript e Importando un proxy javascript.
  2. Al invocar un método en este objeto de javascript, internamente se invoca a un único web service que recibe el objeto serializado, el tipo equivalente en C# y la orden de ejecutar un método con los parámetros correspondientes, también serializados.
  3. El servidor crea una instancia del objeto equivalente en C# y ejecuta el método, devolviendo el resultado y el objeto serializado, ya que la ejecución pudo haber modificado propiedades.
  4. De vuelta en el cliente copiamos las propiedades del objeto que recibimos a sus correspondientes en el de javascript y devolvemos el resultado del método.

Hasta ahora, lo único que no tenemos es un servicio web “genérico” que pueda instanciar cualquier objeto y ejecutar cualquier método que se le solicite, así que es lo que vamos a hacer.

La firma del método sería:

[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public MethodExecuteResult Invoke(string typeName, string objectData, string methodName, string methodParams)

Invoke recibe los parámetros como string ya que no se sabe, a priori, a qué tipo corresponden: objectData (el objeto javascript serializado en JSON) debe convertirse en una instancia del tipo que indique typeName, y los elementos de methodParams (un array de parámetros serializado en JSON) deben convertirse al tipo de los parámetros del método. No podemos dejar esta conversión al framework ya que, por ejemplo, un 3 se convertiría a Int32 y tal vez el método requiere un Decimal.

El retorno es una clase sencilla que tiene como propiedades el objeto deserializado y el valor de retorno del método:

public class MethodExecuteResult
{
    public object TargetObject { get; set; }
    public object ReturnValue { get; set; }
}

Vamos primero por la conversión de objectData, para la que utilizaremos el método JavaScriptSerializer.Deserialize<T>. El problema aquí es que es un método genérico, y nosotros tenemos el tipo <T> como parámetro (¿por qué no hay una sobrecarga con el tipo como parámetro?). Hay dos formas de resolver este tipo de problemas: fácil y rápida de codificar o difícil y rápida al ejecutar. La primera es utilizar reflection para invocar al método genérico, y la segunda es tomar el Reflector y “robarse” el código necesario del framework, modificando el método luego… creo que a los efectos de esta prueba (y de casi toda la vida) la primera técnica será suficiente, y es bastante sencilla:

private static object JavascriptDeserialize(Type type, string objectData)
{
    MethodInfo methodInfo = typeof(JavaScriptSerializer).GetMethod("Deserialize");
    Type[] genericArguments = new Type[] { type };
    MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(genericArguments);
    return genericMethodInfo.Invoke((new JavaScriptSerializer()), new object[] { objectData });
}

Ahora que tenemos un método para deserializar JSON forzando el resultado a un tipo específico, todo es mucho más fácil. Puse los comentarios en el código:

[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public MethodExecuteResult Invoke(string typeName, string objectData, string methodName, string methodParams)
{
    //obtiene el tipo del objeto a invocar a partir de typeName
    Type type = Type.GetType(typeName);

    //obtiene la información del método a invocar a partir de methodName
    MethodInfo methodInfo = type.GetMethod(methodName);
    //obtiene la información de los parámetros del método a invocar.
    ParameterInfo[] methodParamsInfo = methodInfo.GetParameters();

    //si el método a invocar tiene parámetros obtiene los valores
    //a partir de methodParams, que es un array de strings con un parámetro serializado en json por posición.
    object[] paramValues;
    if (methodParamsInfo.Length > 0)
    {
        string[] paramValuesRaw = (string[])InvokerService.JavascriptDeserialize(typeof(string[]), methodParams);
        paramValues = new object[methodParamsInfo.Length];
        for (int paramIndex = 0; paramIndex < methodParamsInfo.Length; paramIndex++)
            paramValues[paramIndex] = InvokerService.JavascriptDeserialize(methodParamsInfo[paramIndex].ParameterType, paramValuesRaw[paramIndex]);
    }
    else
        paramValues = new object[] { };

    //obtiene una instancia del objeto a invocar a partir de objectData.
    object targetObject = InvokerService.JavascriptDeserialize(type, objectData);

    //invoca al método y obtiene el resultado.
    BindingFlags b = BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public;
    object methodReturnValue = type.InvokeMember(methodName, b, null, targetObject, paramValues);

    //crea el valor de retorno incluyendo el resultado de la invocación del método
    //y el objeto sobre el que se invocó serializado.
    return new MethodExecuteResult() { ReturnValue = methodReturnValue, TargetObject = targetObject };
}

Y listo, ya tenemos nuestro servicio de ejecución on the fly. Ahora sólo queda lo más fácil, hacer el proxy. Seguimos en la próxima.


Esta serie de posts son una especie de puesta al día de las ideas que se plasmaron en la arquitectura que utilizaba en mi trabajo en ElectroChance, la gran mayoría de ellas pergeñadas y/o recolectadas por Rick Hunter (por más que te cambies el nombre sabemos quién sos en realidad -sigan el link-), así que a él especialmente y a todos los que alguna vez conformaron ese dream-team, gracias.

viernes, 31 de julio de 2009

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

El método toma ahora cinco parámetros:

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

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

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

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

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

La funcionalidad queda codificada así:

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

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

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

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

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

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

        ParameterInfo[] parameters = publicStaticMethod.GetParameters();

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

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

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

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

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

        builder.Append("};");
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    if (ns.length < 2)
        return;

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

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

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

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

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

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

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

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

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

    base.OnPreLoad(e);
}

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

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

La solución completa está en Google Code.

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


Esta serie de posts son una especie de puesta al día de las ideas que se plasmaron en la arquitectura que utilizaba en mi trabajo en ElectroChance, la gran mayoría de ellas pergeñadas y/o recolectadas por Rick Hunter (por más que te cambies el nombre sabemos quién sos en realidad -sigan el link-), así que a él especialmente y a todos los que alguna vez conformaron ese dream-team, gracias.

jueves, 30 de julio de 2009

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

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

Recapitulando, habíamos codificado la función

Page.Ajax.Call(methodName, data);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

public static StringBuilder Create(Page page)

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

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

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

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

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

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

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

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

        ParameterInfo[] parameters = publicStaticMethod.GetParameters();

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

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

        builder.Append("){");

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

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

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

        builder.Append("};");
    }

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

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

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

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

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

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

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



    }
}

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

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

public partial class _Default : PageBase

Agregamos un método web:

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

y ahora vamos al javascript y lo invocamos:

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

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

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

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

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


Esta serie de posts son una especie de puesta al día de las ideas que se plasmaron en la arquitectura que utilizaba en mi trabajo en ElectroChance, la gran mayoría de ellas pergeñadas y/o recolectadas por Rick Hunter (por más que te cambies el nombre sabemos quién sos en realidad -sigan el link-), así que a él especialmente y a todos los que alguna vez conformaron ese dream-team, gracias.