Caching de bundles en MVC 4 (o MVC 3, o Webforms…)

Hace unos meses ya estuvimos comentando el interesante paquete System.Web.Optimizations que se distribuía con la developer preview de MVC 4, aunque también decíamos que este paquete era igualmente descargable a través de Nuget, y esto hacía posible su uso con MVC 3 o incluso con WebForms.

Como vimos en su momento, su uso era bastante sencillo. En resumidas cuentas, si no queríamos complicarnos demasiado la vida, era suficiente con introducir el siguiente código en la inicialización de la página (en el Application_Start del Global.asax):

    BundleTable.Bundles.RegisterTemplateBundles();

(Bueno, en la preview el método a llamar era EnableDefaultBundles(), pero el resultado era el mismo)

Simplemente con ello, ya podíamos acceder a dos recursos que eran generados automáticamente por el sistema:

  • /scripts/js, que contiene un único archivo .js con todos los scripts existentes en la carpeta /scripts del proyecto, convenientemente minimizados.
  • /content/css, donde encontramos en un único archivo todos los .css presentes en /content, también minimizados para aligerar su descarga.
  • /content/themes/base/css, nuevo en la última versión del componente, que incluye los estilos relativos al tema básico de jQuery UI, utilizado en la plantilla del proyectos de MVC 4.

Además de las ventajas que ofrece en tiempo de ejecución, este mecanismo es muy cómodo a la hora de mantener los scripts actualizados con Nuget. Es decir, dado que descargamos todos los scripts en una única llamada y bajo una única denominación, ya no será necesario andar actualizando las referencias a las bibliotecas en el _Layout.cshtml, algo que resulta molestillo en estos momentos cada vez que actualizamos.

Con la beta de MVC 4 recientemente liberada, este componente ha incluido también un interesante sistema de cacheo que hace aún más eficiente y automático su uso. Si observamos el layout de la plantilla de proyectos ASP.NET MVC 4 veremos que incluye las siguientes líneas:

<link href="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Content/css")" 
      rel="stylesheet" type="text/css" />
<link href="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Content/themes/base/css")" 
      rel="stylesheet" type="text/css" />
<script src="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Scripts/js")">
</script>

Observad que las referencias a los distintos recursos se generan a través del método ResolveBundleUrl(). Pues bien, el código generado por las líneas anteriores es, más o menos así de sorprendente:

<link href="/Content/css?v=oI5uNwN5NWmYrn8EXEybCIbINNBbTM_DnIdXDUL5RwE1" 
      rel="stylesheet" type="text/css" />
<link href="/Content/themes/base/css?v=UM624qf1uFt8dYtiIV9PCmYhsyeewBIwY4Ob0i8OdW81" 
      rel="stylesheet" type="text/css" />
<script src="/Scripts/js?v=GP89PKpk2iEmdQxZTRyBnKWSLjO7XdNG4QC1rv6LPxw1"></script>

En las referencias se está añadiendo un parámetro que contiene un valor hash correspondiente al contenido de la carpeta empaquetada en el momento de generación de la vista. La primera petición que se realice enviará de vuelta los recursos empaquetados y minimizados con una validez a nivel de caché de un año; las siguientes peticiones, por tanto, se resolverán muy rápidamente mediante un HTTP 304 (No modificado):

Respuestas HTTP 304 en peticiones

A partir de ahí, cualquier cambio que se realice en alguno de los archivos o directorios incluidos en los bundles provocará un cambio de hash, por lo que la petición será diferente y, por tanto, el resultado no se tomará desde la caché.

Publicado en: Variable not found.

Registro centralizado de scripts en MVC 4 y Webpages 2

ASP.NET MVCUna novedad que descubro en los tutoriales preliminares de la segunda versión de WebPages, y que por tanto tendremos disponible en las futuras versiones de WebMatrix y ASP.NET MVC 4, es la posibilidad de registrar los scripts y estilos que necesitan nuestros componentes visuales (sean layouts, vistas completas, parciales o helpers), centralizando su carga y evitando duplicidades.

Por ejemplo, imaginad que tenemos una vista parcial o helper que requiere la inclusión de una biblioteca de scripts concreta. Si introducimos los correspondientes tags <script>, y por cualquier causa necesitamos utilizar este componente más de una vez sobre la misma página, estaremos realizando una doble carga del archivo externo, lo que además de lento puede generar errores. Otros escenarios igualmente incómodos son cuando si hay varias parciales distintas que usan una misma biblioteca común, o cuando queremos generar los scripts en un lugar determinado del HTML (por ejemplo, al final de la página) también desde una vista parcial o un helper. Y por supuesto, lo mismo ocurre con los estilos CSS.

El Asset Manager es un componente introducido en WebPages 2, también disponible para MVC 4, que va a poner un poco de orden ahí, usando un mecanismo bastante parecido al disponible en Webforms desde hace bastante tiempo.

Así, el objeto estático Asset (definido en System.Web.WebPages) permite registrar bloques de script inline, referencias a archivos de script externos, bloques de estilos, y referencias a archivos de hojas de estilo usando métodos tan simples como los siguientes:

    Assets.AddScriptBlock("alert('hi!');", true); // Include <script> tag
    Assets.AddScript("~/scripts/jquery.1.6.2.js");
    Assets.AddScript("~/scripts/jquery.1.6.2.js");
    
    Assets.AddStyleBlock("p { font-size: 4em}", true); // Include <style> tag
    Assets.AddStyle("~/content/site.css");

Este registro se puede implementar en cualquier parte dentro del ciclo de ejecución de la vista: en la página de contenido, parciales, helpers, de forma que se irán registrando todos los elementos que puedan hacer falta. Más adelante, para generar los bloques almacenados basta, con hacer lo siguiente por ejemplo en el Layout:

            [...]
            </div>
        </div>
    </footer>
    @Assets.GetScripts()
</body>
</html>

El resultado en ejecución sería:

            [...]
            </div>
        </div>
    </footer>
    <script type="text/javascript">alert('hi!');</script>
    <script src="/scripts/jquery.1.6.2.js" type="text/javascript"></script>
</body>
</html>

Y de la misma forma, tenemos disponible un método @Assets.GetStyles() para obtener todos los bloques relativos a estilos de la página que hemos ido registrando, y que podríamos generar en el interior de la etiqueta <head>, por ejemplo.

Es interesante saber que, al generar el código, Asset detectará si existen dos referencias hacia el mismo archivo de script o estilos, en cuyo caso la referencia se generará sobre la página una única vez, aunque por razones obvias este control no se realizará sobre los bloques de scripts o estilos inline.

Publicado en: Variable not found.

SignalR (IV): Hubs

Como vengo comentando desde hace un tiempo, SignalR es un framework realmente impresionante y aporta unas posibilidades enormes en prácticamente cualquier tipo de aplicación. Ya hemos visto qué es y las bases en las que se sustenta, y también hemos visto algunos ejemplos de uso utilizando conexiones persistentes (aquí y aquí), que es el enfoque de menor nivel disponible a la hora de desarrollar servicios basados en esta plataforma.

En este post ascenderemos a un nivel de abstracción mucho mayor que el proporcionado por las conexiones persistentes y veremos cómo utilizar los Hubs, otro mecanismo proporcionado por SignalR que nos permitirá lograr una integración increíble entre el código cliente y de servidor, y hará aún más sencilla la implementación de este tipo de servicios.

En este momento ya deberíais tener claro qué cosas se pueden hacer con las conexiones persistentes de SignalR, e incluso cómo implementarlas:

  • En el cliente:
    • Conectar con un endpoint o servicio SignalR, manteniendo la conexión virtualmente abierta.
    • Recibir asíncronamente (a través de un callback) los mensajes enviados desde el servidor mediante broadcasts o mensajes directos.
    • Enviar mensajes al servicio usando el método send().
  • En el servidor:
    • Detectar la conexión de nuevos clientes.
    • Detectar la desconexión de clientes.
    • Recibir los mensajes enviados por los clientes.
    • Enviar objetos a todos los clientes conectados, a grupos de ellos, o a un cliente concreto.
Podemos hacer algunas cosillas más, como capturar los errores tanto en cliente como en servidor; aunque no lo hemos visto en los anteriores posts, son aspectos bastante triviales.

Pues bien, con Hubs vamos a poder hacer exactamente las mismas cosas, pero usaremos una sintaxis mucho más fluida y directa tanto en cliente como en servidor, gracias a la mágica flexibilidad que aportan tecnologías como javascript y los tipos dinámicos de .NET.

Y para demostrarlo, implementaremos una sencilla hoja de cálculo multiusuario en tiempo real. Todos los usuarios conectados a la misma podrán editar celdas directamente, y las actualizaciones se irán propagando al resto de clientes de forma automática. Podéis descargar el proyecto para Visual Studio 2010 en el enlace que encontraréis al final del post, aunque en el siguiente vídeo se muestra la locura de resultado en ejecución con algunos clientes conectados editando celdas de forma aleatoria:

Hoja de cálculo multiusuario en ejecución
Veremos que teniendo las herramientas apropiadas es algo bastante sencillo. Conceptualmente, sólo tenemos que hacer lo siguiente:
  • en el lado cliente, cuando un usuario modifique el valor de una celda, enviar al servidor el nuevo valor V que ha introducido en la celda X, Y.
  • en el lado servidor, ante la recepción del mensaje anterior, notificar a todos los clientes conectados que el usuario U ha establecido el nuevo valor V en la celda X, Y.
  • de nuevo en el lado cliente, y ante la recepción del mensaje anterior, modificar la celda X, Y para que aparezca el nuevo valor V que introdujo el usuario U.
Obviamente, esto irá acompañado por algún script más para conseguir que se puedan editar las celdas de forma similar a una hoja de cálculo, o para mantener actualizados los sumatorios de la última fila, pero no nos centraremos en ello. En cualquier caso, siempre podéis verlo descargando el proyecto de prueba.

1. El lado servidor: creación de un Hub

En SignalR, un Hub es la clase donde se implementa el servicio, es decir, el punto desde el cual se gestionarán las peticiones enviadas por todos los clientes conectados al mismo, y desde donde se emitirán los mensajes destinados a actualizar su estado.

En la práctica, se trata simplemente de una clase que hereda de la clase Hub, definida en SignalR.Hubs, en cuyo interior encontraremos los métodos que van a ser invocados directamente desde el cliente. En tiempo de ejecución, SignalR creará un proxy o representante del hub en el lado cliente, con métodos idénticos a los definidos en la clase, en cuyo cuerpo introducirá las llamadas Ajax necesarias para que se produzca la invocación de forma automática. Por tanto, si nuestra clase Hub tiene un método llamado “hacerAlgo()” y el proxy que hemos creado en cliente se llama “hub”, podemos invocarlo desde script haciendo un simple hub.hacerAlgo().

En nuestro ejemplo en el servidor lo único que tenemos que hacer es recibir las notificaciones de cambios de celda y enviárselas al resto de usuarios, lo que podemos conseguir con estas líneas:

public class MultiuserExcel: Hub
{
    public void Update(int y, int x, string value)
    {
        this.Clients.updateCell(this.Caller.userName, y, x, value);
    }
}
Voy a destacar varios aspectos de este código, comenzando por su extrema simplicidad. Casi sin haber visto antes nada de SignalR se puede intuir lo que hace. Observad también que hemos llegado a un punto de abstracción tal que no vemos nada relativo a conexiones ni desconexiones, simplemente implementamos lógica de nuestro servicio.

Es interesante también la propiedad Clients de la clase Hub. Ésta representa al representa al conjunto de clientes conectados, y nos permite llamar directamente a funciones que creemos en el lado cliente. O sea, que si desde el servidor hacemos una llamada a Clients.hacerAlgo(), se ejecutará la función de script hacerAlgo() en todos y cada uno de los clientes conectados. Ya ahí cada uno podrá procesar el mensaje y parámetros que enviemos.

Por último llamar la atención también sobre la propiedad Caller. Ésta, también heredada de la clase base Hub, nos permite acceder a propiedades definidas a nivel de script en el cliente que ha realizado la llamada al método de servidor Update(). En nuestro caso, será una propiedad en la que almacenaremos el nombre del usuario conectado, como veremos algo más adelante.

Fijaos que las clásicas fronteras entre cliente y servidor parecen haberse disuelto, es como si ambas capas se ejecutaran en el mismo proceso, aunque obviamente no es así: es SignalR el que se está encargando de mapear las llamadas entre ellas de forma transparente. Y aunque pueda parecer pura brujería, se trata simplemente de un uso ingeniosísimo de los tipos dinámicos de .NET 4 y de la flexibilidad de javascript.

2. El lado cliente

Como comentaba anteriormente, voy a saltarme el código destinado a hacer que funcionen los mecanismos básicos de edición de la hoja de cálculo, y nos centraremos en lo que nos interesa en este momento, la implementación de la comunicación con el servidor.

Lo primero que debemos hacer en el lado cliente es referenciar desde la vista o página dos archivos de script. El primero de ellos es el mismo que utilizábamos con las conexiones persistentes, /scripts/jquery.signalR.js, mientras que el segundo es un script dinámico generado por SignalR en la dirección /signalr/hubs. Para generarlo, SignalR localizará todas las clases descendientes de Hub disponibles en el proyecto e incluirá en el script un proxy para cada una de ellas; en nuestro ejemplo, generará un objeto llamado $.connection.multiUserExcel (el nombre del hub), que es el que podremos utilizar como proxy, aunque normalmente lo asignaremos a una variable para hacer más cómodo su uso.

Por tanto, un primer acercamiento al código de la vista específico para conectarse a un servicio SignalR podría ser algo así:

<script src="@Url.Content("~/scripts/jquery.signalR.min.js")" type="text/javascript"></script>
<script src="signalr/hubs" type="text/javascript"></script>
<script type="text/javascript">
    $(function() {
        var hub = $.connection.multiuserExcel; // en “hub” tenemos el proxy
        // .. resto del código
    });
</script>
Una consecuencia derivada de la inclusión del script dinámico generado por SignalR es que, a diferencia de lo que ocurría con las conexiones persistentes, no será necesario modificar la tabla de rutas de nuestra aplicación, puesto que los proxies contienen toda la información necesaria para que los servicios puedan ser utilizados de forma directa.

2.1. Envío de mensajes al servidor

Si analizamos los objetos generados de forma dinámica por SignalR para representar al hub en el lado cliente (proxies) podremos ver que éstos incluyen métodos exactamente con el mismo nombre y parámetros que los que hemos implementado en el lado servidor. Es decir, si en nuestra clase Hub tenemos métodos X() e Y(), el proxy en cliente dispondrá de estos dos mismos métodos, aunque en su implementación únicamente se realizarán las llamadas vía Ajax a sus respectivos equivalentes en servidor.

Por tanto, volviendo a nuestro ejemplo, dado que hemos creado un método Update() en el Hub (lado servidor), desde el cliente tendremos disponible el método update() en el hub, que podemos utilizar directamente para enviar mensajes al servidor. Así, en nuestra hoja de cálculo podemos capturar el evento de cambio de cada celda y enviar la actualización al servidor para que la distribuya al resto de clientes conectados:

        $("table.excel input").change(function() {
            var newValue = $(this).val(); // Get current cell’s value
            var x = $(this).data("x");    // Get current cell’s coords
            var y = $(this).data("y");
            hub.update(y, x, newValue);  // Broadcast this change
            updateTotal(x);               // Update column total
        });
Un detalle importante a tener en cuenta es que para adaptarse a las convenciones de nombrado de Javascript, aunque el método en servidor comience por una mayúscula, su correspondencia en cliente comienza en minúscula. Esta conversión sólo se realiza en el primer carácter, el resto debe escribirse exactamente igual en ambos extremos.

2.2. ¿Variables de script accesibles desde el servidor?

Recordad que decíamos que era posible acceder desde el servidor a propiedades existentes a nivel de script simplemente referenciándolas mediante la propiedad Caller del Hub. Pues bien, para que esto sea así, las propiedades deben estar definidas sobre el proxy:
    $(function() {
        var hub = $.connection.multiuserExcel;
        hub.userName = prompt("Username:");
        
        // .. resto del código
A partir de ese momento podremos hacer uso de Caller.userName desde el servidor para acceder al valor que se haya introducido en la misma desde el cliente que realice la llamada al hub. Ojo, que el acceso a las variables es sensible al uso de mayúsculas y minúsculas, deben escribirse igual en ambos extremos.

Otro comportamiento curioso de SignalR a este respecto es que también es capaz de propagar los cambios realizados en el servidor sobre estas variables, de forma que su nuevo valor pasará al lado cliente de forma automática:

    this.Caller.message = "Current time: " + DateTime.Now.ToShortTimeString();
Obviamente, podemos crear tantas propiedades como necesitemos sobre el proxy, y todas ellas las tendremos disponibles en el servidor de la misma forma, tanto para consultar su valor como para modificarlo.

2.3. Recepción en cliente de mensajes enviados por el servidor

A diferencia de lo que habíamos visto usando conexiones persistentes, utilizando hubs no es necesario implementar evento, es mucho más simple. Lo único que debemos hacer es definir sobre el objeto que representa al hub en cliente el método o función a la que estamos llamando desde el servidor utilizando el objeto dinámico Clients.

En nuestro ejemplo, si recordáis, en el servidor estamos enviando el mensaje a todos los clientes conectados al servicio de la siguiente forma:

    this.Clients.updateCell(this.Caller.userName, y, x, value);
Por lo tanto, en cliente debemos implementar un método exactamente con el mismo nombre, y que reciba justo los parámetros que se envían desde el servidor (el usuario que realiza el cambio, la celda modificada y el nuevo valor de ésta):
    hub.updateCell = function(username, y, x, value) {
        if (username != hub.userName) {
            var elem = $("#cell" + y + "-" + x);
            elem.val(value);
            updateTotal(x); // Update column total
        }
    };

¡Y eso es todo!

En el proyecto de demostración encontraréis bastante más código que el que hemos visto en este post, pero principalmente va destinado a conseguir un look&feel similar a las hojas de cálculo tradicionales (bueno, salvando las distancias, claro!). En lo que respecta a la comunicación asíncrona desde y hacia el servidor, estas pocas líneas que hemos visto aquí son prácticamente todo lo que necesitamos para el sistema funcione y el resultado sea espectacular.

Como os he recomendado otras veces, no dejéis de descargar el proyecto y probar el sistema con varias instancias del navegador abiertas, o desde varios equipos. Veréis lo sorprendente y espectacular que resulta y lo sencillo que es de implementar.

Publicado en: Variable not found.