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.

2 comentarios:

Cerebrado dijo...

Me gustó. Ahora, de dónde sacas incentivo para hacer éstas cosas cuando no es en un proyecto requerido?

AcP dijo...

La verdad que no sé. Yo empecé a hacerlo para un proyecto en particular (y a medida que el proyecto lo iba requiriendo) y luego lo "pasé" al framework, ajustándolo un poco más, y corrigiendo algún que otro detalle suelto.

Para mí, hacerlo "in abstracto", sin pensar en un uso concreto es -aparte de muy poco motivar- casi imposible.