ASP.NET MVC: WebGrid con filtro, paginación y ordenación

ASP.NET MVCHace unos días, el amigo Cadavid realizaba una consulta en los comentarios de la serie de posts que escribí hace unos meses sobre el helper Webgrid, sobre cómo podíamos implementar filtros personalizados sobre los datos mostrados, y dado que no es algo que se pueda explicar en un simple comentario, me comprometí a escribir un post sobre el tema, así que ahí va.

El problema fundamental es que WebGrid no incorpora ningún tipo de ayuda para realizar esta tarea tan frecuente; simplemente se encarga de mostrar los datos que le suministremos, por lo que si queremos filtrarlos debemos hacerlo de forma manual. En pura teoría bastaría con hacer llegar a Controlador los filtros a aplicar, y que éste consultara la información desde el Modelo teniendo en cuenta los criterios establecidos.

Sin embargo, algo que puede parecer relativamente simple se complica un poco si queremos, además, mantener intacta la capacidad de ordenación y paginación de datos, puesto que debemos encargarnos también del mantenimiento del estado entre peticiones. Pero vaya, no es nada que no podamos solucionar en unos minutos 😉


Partiendo del proyecto que desarrollamos en el último post de la serie vamos a ver cómo podemos añadir al grid un par de criterios de búsqueda, de forma que el resultado luzca tal que así:

Criterios de filtro con Webgrid
El campo Texto nos permitirá buscar subcadenas en el nombre y apellidos de las personas almacenadas en la base de datos, y los otros dos campos permitirán filtrar por rango (mín-máx) los hijos que tienen. Como es habitual, estas condiciones las combinaremos con un Y lógico.

1. La vista

En primer lugar introducimos en la vista, justo antes de generar el grid, el formulario que utilizaremos para solicitar al usuario los criterios de búsqueda:

Cuadro de criterios de búsqueda
El código es el siguiente:

@using(Html.BeginForm(null, null, FormMethod.Get))
{
    <fieldset>
        <legend>Criterios de búsqueda</legend>
        @Html.Label("buscar", "Texto:")
        @Html.TextBox("buscar") 
        @Html.Label("hijosMin", "Hijos mín:")
        @Html.TextBox("minHijos") 
        @Html.Label("maxHijos", "Hijos máx:")
        @Html.TextBox("maxHijos" )
 
        <input type="submit" value="Buscar" />
    </fieldset>
}
Observad la simplicidad del formulario. Por no usar, no usa ni siquiera la sintaxis lambda en los helpers de edición. Los controles vamos generarlos partiendo de campos cuyos valores existirán en el query string, y no desde el Modelo, que es lo habitual, y por esta razón fijaos que el formulario está configurado para ser enviado utilizando el método HTTP GET.

De esta forma podemos propagar muy fácilmente el valor de los controles (textbox) entre las llamadas:

  • si el usuario introduce criterios y pulsa el botón enviar, la URL a la que se realizará la petición será, por ejemplo, /personas/index?buscar=aguilar&hijosMin=0&hijosMax=8.
  • si el usuario utiliza los botones de navegación propios del grid (avance/retroceso de página, salto a página directa, o reordenación), estos parámetros serán añadidos a los anteriores, de forma que seguirán manteniendo sus valores entre las distintas llamadas. Esto es así gracias a que Webgrid genera los enlaces de estas acciones respetando los parámetros actuales del query string; es decir, si estamos filtrando y saltamos a la página 4, accederemos a una dirección que incluirá tanto la información de los criterios de búsqueda como la de paginación, algo como: /personas/index?buscar=aguilar&minHijos=0&maxHijos=8&page=4.
Y con esto, dejamos completada la capa vista.

2. El Controlador

Si recordáis, el método de acción encargado de obtener los datos de grid y enviar la vista con los datos al usuario recibía tres parámetros: la página actual, el campo de ordenación y el sentido de ésta (ascendente/descendente).

Dado que ahora necesitará también obtener los criterios de ordenación, debemos ampliar su definición añadiendo parámetros para estos valores:

public ActionResult Index(int page = 1, string sort = "Apellidos", string sortDir = "ASC", 
                          string buscar = null, int? minHijos = null, int? maxHijos = null)
Observad que son todos parámetros opcionales, y los establecemos a nulo para detectar fácilmente cuándo nos vienen rellenos.

¿Y en qué puntos necesitamos utilizar estos nuevos parámetros? Pues sólo en dos:

  • en la llamada que hacemos al Modelo para contar el total de filas del grid, a la que tendremos que informar sobre los criterios de filtrado para que el conteo se realice correctamente.
  • en la llamada que hacemos al Modelo para obtener las filas a mostrar en la página actual, donde obviamente también deberemos tener en cuenta los filtros.
La acción quedaría más o menos así:

public ActionResult Index(int page = 1, string sort = "Apellidos", string sortDir = "ASC",
                          string buscar = null, int? minHijos = null, int? maxHijos = null)
{
    var numPersonas = _services.ContarPersonas(buscar, minHijos, maxHijos);
    var personas = _services.ObtenerPaginaDePersonasFiltrada(page, PERSONAS_POR_PAGINA, 
                                   sort, sortDir, buscar, minHijos, maxHijos);
 
    var datos = new PaginaDePersonasViewModel()
                    {
                        NumeroDePersonas = numPersonas,
                        PersonasPorPagina = PERSONAS_POR_PAGINA,
                        Personas = personas
                    };
 
    return View(datos);
}
Y esto es todo en el controlador.

3. El Modelo

Y por último, ya en el Modelo, debemos hacer que los métodos utilizados desde el controlador (ContarPersonas y ObtenerPaginaDePersonasFiltrada) tengan en cuenta los parámetros en los que indicamos las condiciones de búsqueda.

En el primero de ellos, simplemente retornamos el número de personas que cumplan los criterios que nos llegan como parámetros:

public int ContarPersonas(string textoBuscar = null, int? minHijos = null, int? 
                          maxHijos = null)
{
    IQueryable<Persona> query = _datos.Personas;
    query = queryPersonasFiltrada(textoBuscar, minHijos, maxHijos, query);
    return query.Count();
}
El método de utilidad queryPersonasFiltrada() que estamos utilizando únicamente se encarga de añadir a la query las cláusulas where que necesitamos para tener en cuenta las condiciones especificadas:

private static IQueryable<Persona> queryPersonasFiltrada(string textoBuscar, int? minHijos, 
                                         int? maxHijos, IQueryable<Persona> query)
{
    if (!string.IsNullOrWhiteSpace(textoBuscar))
        query = query.Where(p => p.Nombre.Contains(textoBuscar) || p.Apellidos.Contains(textoBuscar));
    if (maxHijos != null)
        query = query.Where(p => p.NumeroDeHijos <= maxHijos);
    if (minHijos != null)
        query = query.Where(p => p.NumeroDeHijos >= minHijos);
    return query;
}
Por último, implementamos el método que obtiene los datos a mostrar en la página actual:

public IEnumerable<Persona> ObtenerPaginaDePersonasFiltrada(int paginaActual, int personasPorPagina, 
              string columnaOrdenacion,  string sentidoOrdenacion, 
              string textoBuscar, int? minHijos, int? maxHijos)
{
    // Comprobamos los datos de entrada
    sentidoOrdenacion = sentidoOrdenacion.Equals("desc", StringComparison.CurrentCultureIgnoreCase) ? 
                        sentidoOrdenacion : "asc";
 
    var validColumns = new[] { "apellidos", "fechanacimiento", "email", "numerodehijos" };
    if (!validColumns.Contains(columnaOrdenacion.ToLower()))
        columnaOrdenacion = "apellidos";
 
    if (paginaActual < 1) paginaActual = 1;
    if (personasPorPagina < 1) personasPorPagina = 10;
 
    // Generamos la consulta
    var query = (IQueryable<Persona>) _datos.Personas
                     .OrderBy("it." + columnaOrdenacion + " " + sentidoOrdenacion);
 
    query = queryPersonasFiltrada(textoBuscar, minHijos, maxHijos, query);
 
    return query
            .Skip((paginaActual - 1) * personasPorPagina)
            .Take(personasPorPagina)
            .ToList();
}

Hay poco que comentar sobre este código. En primer lugar se realiza una comprobación básica de los parámetros de entrada, para a continuación generar la consulta que se realizará sobre la base de datos. Como podéis observar, también se utiliza el método queryPersonasFiltrada() para aplicar los criterios de consulta.

En resumen…

Como hemos podido ver, la implementación de criterios de búsqueda en un WebGrid no difiere mucho de la que habíamos descrito en su momento. Sólo hay que tener en cuenta los siguientes puntos:
  • primero, incluir en la Vista un formulario donde se recojan los criterios de la consulta para enviarlos al controlador.
  • segundo, preparar el Controlador para que reciba estos criterios y los haga llegar al Modelo.
  • tercero, en el Modelo, simplemente aplicar esos criterios en el momento de contar las filas totales, y en la obtención de los datos a mostrar en la página del grid.
En SkyDrive he colgado un proyecto que incluye un ejemplo completo donde podéis ver todo esto en funcionamiento: WebGridFiltradoDemo (MVC 3 + SQL Express).

Publicación original (5-oct-2011): http://www.variablenotfound.com/2011/10/aspnet-mvc-webgrid-con-filtro.html

Este artículo y muchos más en: Variable not found.

ASP.NET MVC Request Pipeline

Iba a publicar un enlace hacia este documento en Twitter y Facebook, pero me ha parecido tan interesante que merece un post independiente.

El caso es que el gran Simone Chiaretta ha publicado un excelente diagrama con el ciclo de vida de una petición en el framework ASP.NET MVC. Simplemente imprescindible para comprender lo que pasa por ahí dentro desde que se recibe una petición en el servidor hasta que es enviada la vista con la respuesta al cliente.

ASP.NET MVC request pipeline
Puedes pulsar sobre la imagen para descargar la versión PDF, lista para ser impresa 🙂

Publicación original (29-sept-2011): http://www.variablenotfound.com/2011/09/aspnet-mvc-request-pipeline.html
Tip: ¿Sabías que en geeks.ms no publico ni la mitad de los artículos de Variable not found?

ASP.NET MVC: Binding de enums y EnumDataType

El pasado mes de julio publicaba el post “Binding de enums en ASP.NET MVC”, donde analizaba los problemas que podían aparecer cuando estamos intentando bindear tipos enumerados y los datos de entrada contienen valores incorrectos, y planteaba una solución basada en un model binder personalizado.

A raíz de dicho post, el amigo Héctor S. (¡gracias!) me envió una interesante pregunta: ¿por qué no utilizar la anotación System.ComponentModel.DataAnnotations.EnumDataType para ello?

Y la respuesta corta es: pues también se podría. Con sus particularidades, pero sería posible controlar los valores de entrada a un enum usando esta anotación.

Empecemos por el principio. EnumDataTypeAttribute es una anotación introducida en .NET 4 que nos permite indicar que una propiedad (no necesariamente de tipo enum) admite exclusivamente valores definidos en un enumerado. Mejor lo vemos con algo de código:

    public enum Sexo
    {
        Hombre = 1,
        Mujer  = 2
    }
 
    [EnumDataType(typeof(Sexo))]
    public int Sexo { get; set; }

En el ejemplo anterior observamos una propiedad Sexo de tipo int, lo que le permite virtualmente alojar cualquier valor entero posible. Sin embargo, el hecho de decorarla con el atributo EnumDataTypeAttribute y asociarla a la enumeración Sexo hará que en la validación de la entidad su valor sea considerado incorrecto si éste no pertenece a los permitidos en ella, en este caso, 1 y 2.

Su uso es interesante, por ejemplo, como método para acercar representaciones de la información. Por ejemplo, si la propiedad Sexo está mapeada hacia un campo de base de datos, es bastante habitual que éste sea de tipo entero, por lo que el hecho de declararla en la entidad como int facilita el movimiento de datos entre uno y otro. Al decorarla con EnumDataType lo que hacemos simplemente es asegurar que contendrá un valor correcto conforme al enumerado, pero ojo, siempre que la entidad sea validada considerando sus anotaciones (como ocurre durante el binding).

Y volviendo al tema del post, entonces, ¿por qué no utilizamos este método para asegurar la validez de los datos en el post al que hacía referencia, en lugar de montar un binder personalizado?

Pues el motivo principal sería la pereza. Realmente, me parece demasiado trabajoso el tener que decorar cada propiedad enum del modelo con una declaración tan redundante como la siguiente:

    public class Persona
    {
        public string Nombre { get; set; }
 
        [EnumDataType(typeof(Sexo))]
        public Sexo Sexo { get; set; }
 
        ...
    }

A ver, si estoy declarando una propiedad de tipo Sexo, me resulta pesado tener que decirle algo más arriba que ésta es un tipo de datos enumerado de tipo Sexo.

Pero bueno, salvando este incómodo detalle, EnumDataType es una buena fórmula para controlar la corrección de los valores de entrada, y sobre todo muy cómoda si la propiedad parámetro a actualizar es de un tipo no enum, como un int:.

    [EnumDataType(typeof(Sexo))]
    public int Sexo { get; set; }

Su uso aporta, además, otras posibilidades interesantes. Por ejemplo, sería posible crear editores personalizados sobre el tipo int que, basándose en este atributo, mostraran a nivel de vista desplegables u otros controles de selección de forma automática.

Es importante tener en cuenta que, a diferencia de mi enfoque, la comprobación del atributo se realiza durante el proceso de validación, que es posterior al binding en sí. La ventaja de esto es que se integra de forma natural en dicho proceso (por ejemplo, ModelState.IsValid retornará falso si hemos intentado introducir un «3» en Sexo), pero también pueden producirse errores previos generados por el propio binder si no es capaz de realizar la transformación de forma automática (por ejemplo, al intentar un valor textual no presente en la enumeración, como ‘Gato’), que de la otra forma sí sería posible controlar.

Publicación original (13-sep-2011): http://www.variablenotfound.com/2011/09/aspnet-mvc-binding-de-enums-y.html
Tip: ¿Sabías que en geeks.ms no publico ni la mitad de los artículos de Variable not found?

Membership, roles y profiles sobre SQL Compact con ASP.NET Universal Providers

Hace un par de días comentaba la aparición de Universal ASP.NET Providers, un componente que incluye proveedores de membership, roles, profiles y estado de sesión capaces de funcionar con todas las versiones de SQL Server desde la 2005, ediciones express, SQL Azure y SQL Compact 4.0, y utilizables en cualquier aplicación ASP.NET, ya sea Webforms o ASP.NET MVC.

En este post vamos a ver un ejemplo ómo podemos utilizar este nuevo componente para montar en pocos segundos el sistema de membresía, roles y perfiles de una aplicación web utilizando SQL Compact Edition, algo que por otra parte no es una idea nada descabellada para sitios de pequeño calibre. Recordad que esta edición de SQL Server no requiere un servicio funcionando en el servidor (se ejecuta en el mismo proceso que la aplicación web), por lo que puede resultar muy apropiada para su uso en pequeños sitios donde no se justifica la implantación de sus hermanas mayores (express o superiores).

1. Instalamos el tooling en Visual Studio 2010 SP1

Lo primero que vamos a hacer es instalar las herramientas de SQL Compact Edition para el Visual Studio. Éstas las podéis descargar desde este enlace, aunque antes debéis aseguraros de tener instalado el Service Pack 1 de Visual Studio.

Esto no es estrictamente necesario si no pensamos crear o acceder a la base de datos desde Visual Studio, aunque en cualquier caso es recomendable para poder echar un vistazo a la estructura y datos más adelante.

2. Creamos una aplicación web

Creamos ahora una aplicación Web desde Visual Studio. No importa si es ASP.NET MVC o WebForms: en la pila de tecnologías estamos un nivel por abajo, directamente jugando con características de ASP.NET, que, como sabéis, son comunes a ambos frameworks.

Creando la nueva aplicación web ASP.NETPara ilustrar nuestro ejemplo crearemos una aplicación Webforms seleccionando la plantilla “Aplicación Web ASP.NET” en el cuadro de diálogo de nuevo proyecto del IDE, pero podríamos perfectamente elegir la “Aplicación web de ASP.NET MVC” y todo sería exactamente igual.

3. Descargamos de Nuget los componentes que necesitamos

Para lograr nuestros objetivos sólo necesitaremos dos paquetes:
  • el proveedor de datos SQL CE, que nos permitirá conectarnos a esta base de datos y acceder a su información ya sea utilizando directamente las clases de ADO.NET como Entity Framework,
  • el paquete de Proveedores Universales para ASP.NET, que creará la abstracción sobre el origen de datos concreto y nos permitirá trabajar virtualmente con cualquier motor relacional (aunque, como comentaba en el post anterior, por las pruebas que he hecho de momento no sea así).
NugetDesde la consola de Nuget podemos conseguirlo así:

PM> Install-Package System.Web.Providers
[...]
Successfully installed 'System.Web.Providers 1.0.1'.

PM> Install-Package SqlServerCompact
[...]
Successfully installed 'SqlServerCompact 4.0.8482.1'.
Si en cambio sois más de utilizar el GUI, podéis buscar en la galería los paquetes “ASP.NET Universal Providers” y “SqlServerCompact”:

Instalando los componentes con Nuget

4. Modificamos la cadena de conexión

El proceso de instalación del paquete de proveedores habrá modificado nuestro web.config, dejándolo listo para su utilización con SQL Express. Como no es esto lo que queremos, acudimos a este archivo y modificamos la cadena de conexión usada por los proveedores:

<connectionStrings>
   <add name="DefaultConnection"
      connectionString="Data Source=|DataDirectory|Datos.sdf;"
      providerName="System.Data.SqlServerCe.4.0" />
</connectionStrings>
Observad que estamos indicando que el proveedor de datos es SqlServerCe (lo hemos descargado previamente usando Nuget), y que la base de datos se llamará datos.sdf y estará ubicada en la carpeta App_Data del proyecto.

De forma opcional, podemos aprovechar el momento para limpiar un poco el web.config, eliminando líneas que vienen en la plantilla de proyectos por defecto, pero que ya no vamos a necesitar.

De limpieza por el web.config

5. ¡Y esto es todo!

Pues sí, eso es todo lo que necesitamos hacer. En este momento ya tenemos configurados los proveedores de membresía, roles y perfiles. De hecho, dado que las plantillas de proyecto tanto para MVC como para Webforms ya incluyen un sistema de autenticación y registro básico, podemos directamente ejecutarla y utilizar sus funcionalidades para registrarnos y autenticarnos en el sistema:

Registro de usuarios en ASP.NET Webforms

Herramienta de administración de ASP.NETAsimismo, dado que se están utilizando mecanismos estándar en el framework, podemos utilizar la propia herramienta de gestión de ASP.NET para crear los usuarios, asignarles funciones o definir reglas de acceso por carpetas. Como ya sabéis, esta utilidad viene incluida de serie en el framework, y podemos acceder a ella a través del menú “Proyecto > Configuración de ASP.NET” de Visual Studio o pulsando sobre un icono que aparece en el explorador de soluciones.

Eso sí, para comenzar a definir roles y asignarlos a usuarios es necesario activar previamente el proveedor de roles (DefaultRoleProvider), ya sea desde la herramienta de administración, bien añadiendo en el nodo <RoleProvider> del web.config el atributo enabled="true".

De la misma forma, podemos activar la persistencia del estado de sesión sobre la misma base de datos estableciendo el atributo mode="Custom" en el nodo <sessionState> (por defecto está configurado como "InProc" ).

En cualquier caso, una vez accedamos a alguno de estos proveedores, podremos observar que se ha creado de forma automática la base de datos llamada Datos.sdf en la carpeta App_Data. Por supuesto, también podríamos haber indicado el nombre de un base de datos existente (creada por ejemplo desde el mismo VS) y el sistema habría creado únicamente las tablas e índices usados por los proveedores sin afectar al resto de objetos.

Recapitulando: si ya tenemos el tooling y el proveedor de datos para SQLCE en un proyecto, lo único que tenemos que hacer es, en primer lugar, descargar a través de Nuget el paquete de Proveedores Universales de ASP.NET, y a continuación ajustar la cadena de conexión en el web.config. Más sencillo, imposible.

He dejado en Skydrive una solución de ejemplo con un proyecto Webforms y otro MVC para que podáis probarlo de forma sencilla. En la base de datos hay un usuario creado (jmaguilar con clave jmaguilar, aunque podéis registraros desde las propias aplicaciones); para que podáis ver cómo se comporta el sistema de autenticación y membresía, no se permite el acceso a las páginas “Acerca de” a usuarios no identificados previamente.

Publicación original (7-sep-2011): http://www.variablenotfound.com/2011/09/membership-roles-y-profiles-sobre-sql.html

Tip: ¿Sabías que en geeks.ms no publico ni la mitad de los artículos de Variable not found?

ASP.NET Universal providers

ASP.NETPoco antes de verano comentábamos de pasada la aparición de su versión alfa, y ya desde mediados de agosto tenemos aquí la versión final de ASP.NET Universal Providers, un componente que incluye proveedores de membership, roles, profiles y estado de sesión capaces de funcionar con todas las versiones de SQL Server desde la 2005, ediciones express, SQL Azure y SQL Compact 4.0, y utilizables en cualquier aplicación ASP.NET, ya sea Webforms o ASP.NET MVC.

Lo interesante que tiene el ensamblado System.Web.Providers, que así es como se distribuye el componente, es que podemos modificar el motor de persistencia sobre el que trabajan simplemente tocando la cadena de conexión en el web.config, evitándonos el tener que crear proveedores en cuanto nos salíamos de los provistos por defecto en la plataforma (SQL Server). O en otras palabras, tenemos aquí un proveedor para gobernarlos a todos 😉

Como no podría ser de otra forma, la distribución del paquete de proveedores se realiza a través de Nuget, por lo que su instalación y puesta en marcha es sencillísima.

Instalando los proveedores universales con Nuget
Una vez descargado e instalado el paquete “ASP.NET Universal Providers”, además de añadirse el ensamblado System.Web.Providers y sus correspondientes referencias al proyecto, veremos que en el web.config se ha incluido una cadena de conexión, llamada “DefaultConnection”, que es la que será utilizada por estos nuevos proveedores. Por defecto podremos comprobar que esta conexión está configurada para trabajar sobre una instancia de usuario de SQL Express, pero simplemente modificándola podremos hacer que funcione sobre ediciones superiores de SQL Server, SQL Azure o SQL Compact.

Un detalle importante es que, para evitar daños colaterales, el proceso de instalación no elimina la cadena de conexión previa (esa que vemos con la denominación “ApplicationServices”), ni la configuración de proveedores previa: simplemente añade el nuevo proveedor y lo establece por defecto, por lo que podemos eliminar tranquilamente esas configuraciones que ya no utilizamos.

Eliminando del Web.config líneas que ya no usamos
Tablas creadas por los proveedoresUna vez configurada correctamente la cadena de conexión (“DefaultConnection”) ya no será necesario tocar nada más; el sistema se encargará de crear las tablas necesarias para almacenar la información de usuarios, roles, perfiles, e incluso para hacer persistente el estado de sesión (si utilizamos el proveedor incluido en este componente); además, si no existe la base de datos, será capaz de crearla automáticamente si tenemos permisos para ello.

Como nota negativa, seguro que habéis pensado que ya que es posible modificar el proveedor de datos a nivel de archivo de configuración, parece lógico que podamos utilizar cualquier origen de datos disponible, ¿verdad? Pues no necesariamente. He probado con el conector MySQL y no ha habido forma de echar a andar estos proveedores debido a un problema de incompatibilidad del tipo “image” en los metadatos. No sé si se debe a limitaciones del Universal Provider, o bien se trata de algún problema en la implementación del conector MySQL.

Sin duda, es un avance interesante que puede ahorrarnos bastante tiempo en esa tarea tan molesta que es montar la infraestructura de membresía y seguridad de nuestras aplicaciones. Y además, como comentaba Hanselman en su momento, estos proveedores es posible que formen parte de la próxima versión del framework, por lo que habrá que estar atentos a ellos.

Post original (6-sept-2011): http://www.variablenotfound.com/2011/09/aspnet-universal-providers.html

ASP.NET MVC 4 Developer Preview: un primer vistazo

ASP.NET MVC 4Es habitual que los eventazos que organiza Microsoft de vez en cuando sean aprovechados para presentar nuevas versiones de productos, y por supuesto el pasado \BUILD no ha decepcionado en ese aspecto: han sido liberadas versiones preliminares para desarrolladores de Windows 8, Windows 8 Server, Visual Studio 11, ASP.NET 4.5, MVC 4, Webpages 2, y seguro que muchas más cosas que ahora no recuerdo. Obviamente, el revuelo de estos últimos días ha sido, como siempre, brutal.

Pero bueno, centrándonos en ASP.NET MVC 4, se trata de un adelanto de lo que ya se nos avanzó en el Roadmap del producto, y que se ha distribuido en dos paquetes distintos: uno para Visual Studio 2010, y otro para el flamante Visual Studio 11 Developer Preview, también lanzado durante el Build. Ambos pueden descargarse a través de Web Platform Installer, o desde los enlaces proporcionados en esta página.

Este post vamos a dedicarlo a comentar algunas de las cosas que me han ido llamando la atención durante los primeros paseos por el producto. Y ojo, que se trata de una versión muy preliminar: puede tener fallos, cambiar conforme vayamos aproximándonos a la versión final, o incluso hay características que podrían desaparecer.

Por ejemplo, en el caso de la distribución para Visual Studio 2010, no debería interferir con otras versiones del producto instaladas, por lo que deberíais poder probarla en vuestra máquina habitual sin miedo a que os rompa nada. Sin embargo, sí que me ha dado algún problemilla al compilar proyectos MVC 3 existentes, puesto que se introducían dependencias a la versión 2 de System.Web.Webpages (distribuida con MVC 4), pero vaya, nada que no pueda solucionarse en un par de minutos retocando las referencias del proyecto.

1. Primeras novedades

Una vez instalado, lo primero que encontramos al crear un proyecto de tipo Web es una nueva plantilla llamada “ASP.NET MVC 4 Web Application”.

Creación de nuevo proyecto MVC 4

Hasta aquí, todo normal. Pero las primeras sorpresas aparecen en el habitual cuadro de diálogo posterior:

Selección de plantilla de proyecto

Como podéis ver, hay varias novedades. Primero, el checkbox “Use HTML5 semantic markup” que fue introducido con la última actualización de MVC aparece deshabilitado en todas las plantillas exceptuando la vacía. Esto parece dar a entender que HTML5 ya no es una opción… es la opción; habrá que ver si en la versión final incluyen plantillas no-HTML5, o se confirma esta tendencia.

Y segundo, además de las habituales plantillas (Vacía, Aplicación Internet y Aplicación Intranet) podemos ver la nueva “Mobile Application”. Sin duda, como ya se adelantaba en el Roadmap, las aplicaciones web para móviles toman bastante protagonismo en esta entrega.

Seguimos avanzando. Creamos un proyecto de tipo Internet Application, y nos encontramos con una plantilla mucho más moderna y elegante. La verdad es que estaba ya un poco cansado de ver siempre la misma:

Nueva plantilla de proyectos MVC 4
image
Pero no se trata únicamente de un cambio estético. La maquetación utiliza una técnica llamada renderizado adaptativo, que en la práctica significa que se adapta a distintos anchos de pantalla, por lo que es válida tanto para ser visualizada correctamente en dispositivos grandes (captura de pantalla de arriba) como en móviles. Es fácil de comprobar simplemente cambiando el tamaño de la ventana del navegador.

Obviamente esta capacidad no tiene que ver con MVC 4, sino con la forma en que ha sido maquetada la página, pero de nuevo pone de manifiesto la orientación del producto hacia los dispositivos móviles.

También se ha dado un giro radical al habitual sistema de membresía que venía con las plantillas tradicionales MVC. Ahora, si el usuario tiene activado el scripting (lo que es habitual), las funcionalidades de acceso y registro de usuarios se realizan utilizando cuadros de diálogo basados en jQuery UI, y llamadas Ajax. Si en cambio no existe la posibilidad de utilizar scripts, estas funcionalidades se realizarán a pantalla completa, como siempre.

Login de usuarios basado en jQuery UI y Ajax
Plantilla para dispositivos móviles
En cuanto a la plantilla específica para dispositivos móviles, utiliza jQuery Mobile, una popular biblioteca para componer interfaces web específicos para dispositivos táctiles como smartphones y tablets.

La funcionalidad es idéntica a la plantilla de aplicaciones para Internet, pero optimizada para la interacción desde estos dispositivos.

2. Display Modes

Sin embargo, el hecho de tener dos plantillas de proyecto independientes dependiendo del dispositivo no parece demasiado natural, y por eso MVC 4 incluye una nueva característica llamada display modes (modos de visualización).

Esta característica permite crear una única aplicación y renderizar una u otra vista en función del dispositivo o las condiciones que deseemos.

Por ejemplo, si la acción “Index” del controlador “Home” retorna la vista por defecto, lo habitual es que el framework devuelva al usuario el resultado de procesar la plantilla /Views/Home/Index.cshtml. Pues bien, gracias a los display modes, si la petición ha sido realizada desde un dispositivo móvil, se retornará la vista Index.mobile.cshtml y sólo en caso de no existir, la habitual Index.cshtml. Y lo mismo ocurre con los layouts y vistas parciales: se seleccionarán en función del cliente.

También se permite un control más granular sobre los dispositivos. El siguiente código (tomado de las notas de la revisión), registra un nuevo display mode en el sistema. Básicamente, lo que hacemos es asociar el nombre “iPhone” al dispositivo que ha realizado una petición en cuyo user-agent aparece ese mismo texto:

DisplayModes.Modes.Insert(0, new DefaultDisplayMode("iPhone")
{
    ContextCondition = (context => context.Request.UserAgent.IndexOf
        ("iPhone", StringComparison.OrdinalIgnoreCase) >= 0)
});
Una vez hecho esto, y siguiendo con el ejemplo anterior, la vista que se intentaría renderizar en este caso sería Index.iPhone.cshtml. Qué bueno, eh?

Otra capacidad muy interesante que vamos a tener a nuestra disposición es el browser overriding, o lo que es lo mismo, el poder establecer el navegador del usuario independientemente del utilizado por el mismo. Esto permitirá, por ejemplo, forzar la visualización en modo completo a alguien que está accediendo desde un dispositivo móvil, o justo lo contrario, forzar la visualización del modo móvil a alguien accediendo desde desktop.

El agente de usuario sobrescrito será persistente entre las sucesivas peticiones puesto que se almacenarán por defecto en una cookie, aunque, como es habitual, será posible utilizar otros mecanismos de persistencia personalizados.

El view switcher en acción
Para facilitar el desarrollo de este tipo de sistemas, se ha puesto a disposición de los desarrolladores el paquete jQuery.Mobile.MVC (descargable a través de Nuget con esta denominación), que incluye un Layout para móviles, un controlador, algunos helpers y una útil vista parcial llamada ViewSwitcher que permite conmutar entre el modo móvil y modo desktop.

3. Bibliotecas de scripting

Hasta MVC 3, en todas las plantillas de proyectos incluían las bibliotecas de scripting de Microsoft relativas a Ajax y los sistemas de validación. Ya en la última versión del framework era más recomendable utilizar las soluciones basadas en jQuery, pero estas bibliotecas seguían pululando por ahí; en ASP.NET MVC 4, simplemente las bibliotecas javascript de Microsoft han dejado de distribuirse en las plantillas de proyectos MVC.

Y como sorpresa, en la carpeta /Scripts, además de los tradicionales Modernizr, jQuery, jQueryUI, jQuery validate, y jQuery unobtrusive, nos encontramos ahora con Knockout, una biblioteca javascript que permite crear interfaces de usuario dinámicos utilizando el patrón MVVM.

Claramente se intenta poner un poco de orden en ese caos en lo que a veces se convierten las vistas cuando son muy dinámicas y utilizan mucho scripting.

4. Microsoft.Web.Optimizations

Curioseando por las referencias que se añaden a los nuevos proyectos, muchas de ellas en forma de paquetes Nuget preinstalados, me encuentro con Microsoft.Web.Optimizations, una joya que ya deja ver cómo va a resolverse la promesa hecha en el roadmap sobre la compactación y minimización de archivos javascript y estilos.

Se trata de un sistema mediante el que podemos crear paquetes (bundles) de archivos .js o .css y asignarles una URL única. Cuando se realice una petición a esta dirección, se compactarán y minimizarán los archivos originales y se retornará al usuario la copia reducida. Obviamente esto sólo se producirá una vez, puesto que el resultado quedará almacenado en caché.

Por ejemplo, podemos asociar la URL “/myscripts.js” a todos los scripts presentes en la carpeta “~/scripts” del proyecto incluyendo subdirectorios, con lo que podremos descargar todo el javascript que necesita una página simplemente referenciando desde ella al archivo myscripts.js. E igual ocurre con las hojas de estilo.

No es nada que no pudiera hacerse antes incluyendo los procesos de compactación en compilación, pero la verdad es que este nuevo sistema está muy bien, y facilita la creación de

En el caso de los scripts, la compactación no consiste únicamente en reducir los comentarios, espacios y saltos de línea, sino que también son transformados los nombres de las variables, métodos y funciones privadas para reducirlos a su mínima expresión. O sea, que de paso estamos ofuscando el código.

Y lo que es mejor, al tratarse de un paquete independiente que no tiene dependencias con MVC 4 ni con ASP.NET 4.5, podemos usarlo ya en nuestras aplicaciones MVC 3, simplemente descargando el paquete desde Nuget y poniéndolo en marcha. La forma de usarlo, para no extenderme mucho aquí, la dejaré para otro post, aunque ya os adelanto que es realmente sencillo.

5. ASP.NET Universal providers

El sistema de membresía de los proyectos ASP.NET MVC 4 utiliza ahora los nuevos proveedores universales (ASP.NET Universal Providers), lo cual facilita enormemente el cambio desde el motor por defecto (SQL Express) a otros más apropiados para nuestra aplicación, como SQL Server, SQL Azure, o SQL Compact.

Aquí puedes leer algo más sobre los proveedores universales de ASP.NET.

6. Más características incluidas

Durante las pruebas, me he encontrado en las vistas con nuevos helpers como los imprescindibles Html.IdFor() y Html.NameFor() que ya vimos por aquí hace tiempo, y que son útiles para averiguar respectivamente el id y name que serán asignados al campo de edición de una propiedad en un formulario. Viene también con sus versiones no tipadas, nombrados como Id() y Name(), y las versiones para el modelo completo IdForModel() y NameForModel().

También encontramos un interesante helper Html.Value(), ValueFor() y ValueForModel() que permite obtener el valor para una propiedad siguiendo la lógica de los habituales helpers de edición, teniendo en cuenta tanto el ModelState como los datos de la vista.

Por último, aunque no las he comprobado, en el documento de Release Notes se destacan también los siguientes aspectos:

  • simplificación de controladores asíncronos, utilizando C# 5 y los tipos Task y Task<ActionResult>.
  • soporte para la release 1.5 del SDK de Azure.

7. Pero… ¿y las cosas que faltan?

Selección de recetasDe momento no he encontrado ni rastro de las famosas recetas (recipes). Bueno, el IDE incorpora una nueva opción “Run recipe…” en el menú contextual que nos lleva a un cuadro de diálogo en el que en principio deberían aparecer las recetas disponibles, pero está vacío. Tal y como se indicaba en el Roadmap, estas recetas serían descargables a través de Nuget, pero no he conseguido dar con ninguna.

Tampoco se generan aún las etiquetas <input> con el type ajustado según el tipo de datos para aprovechar las nuevas capacidades de HTML5, otra de las características que acompañarán a MVC 4.

En fin, que mirando el Roadmap está claro que estos chicos tienen todavía mucho trabajo por delante. Pero no olvidemos que, como indica Phil Haack en su post de presentación, se trata de una versión preliminar para desarrolladores. Ni siquiera se puede considerar una beta, por lo que hay que tomárselo como tal. Faltan muchas cosas, y probablemente las que vemos cambiarán bastante antes de la versión definitiva.

Enlaces:

Publicación original (20-sept-2011):
http://www.variablenotfound.com/2011/09/aspnet-mvc-4-developer-preview-un.html
Este artículo y muchos más en: Variable not found.

 

ASP.NET MVC: retorno de archivos .Zip creados al vuelo

ASP.NET MVCASP.NET MVC viene acompañado de serie por un buen número de subtipos de ActionResult que podemos utilizar como retorno de nuestras acciones (FileResult, ContentResult, ViewResult, RedirectResult, etc…) y que cubren la mayoría de escenarios de uso frecuente al desarrollar aplicaciones para este framework.

Pero sin duda, lo mejor de todo es lo fácilmente que podemos extender este conjunto para lograr resultados muy potentes, reutilizables y respetuosos con el patrón.

En este post vamos a ver cómo crear en unos minutos un nuevo tipo de resultado para nuestras acciones llamado ZipResult, que nos permitirá generar al vuelo archivos en formato comprimido .ZIP, en cuyo interior podremos añadir los ficheros que queramos.

Así, nuestro ZipResult recibirá una serie nombres de archivo, los comprimirá, y los retornará al usuario empaquetados en un único fichero .ZIP. A nivel de código, su uso será así de simple:

public ActionResult DescargarArchivos()
{
    return new ZipResult("c:\archivo1.dat", "c:\archivo2.dat");
}
¡Vamos allá!

1. Creación de zips desde .NET

Desde la llegada de Nuget, nada ha vuelto a ser lo mismo. En unos segundos, sólo abriendo la herramienta de gestión de paquetes, seleccionando la opción “online” y haciendo una búsqueda sobre el término “zip” tenemos acceso a la oferta de paquetes relacionados con el mismo:

Resultado de búsqueda de "zip" en Nuget

Como podemos ver, existen muchas opciones para tratar con archivos .zip que tenemos al alcance de un clic. En este caso vamos a usar DotNetZip, una potente biblioteca open source que nos ofrece todo lo que necesitamos en este proyecto y mucho más ;-), pero a diferencia de otras, es bastante más sencilla y cómoda de utilizar.

Y como muestra el siguiente método, que recibe una lista de rutas de archivo y los comprime sobre un fichero .zip:

public void Comprime(IEnumerable<string> files)
{
    using (ZipFile zf = new ZipFile())
    {
        zf.AddFiles(_files, false, "");
        zf.Save(@"d:prueba.zip");
    }
 
}
El código es bastante conciso y fácil de comprender; creamos un nuevo archivo zip, representado por la instacia del tipo ZipFile, llamamos a su método AddFiles() suministrándole la colección de rutas de los ficheros a comprimir, y salvamos el resultado al disco. El segundo parámetro de AddFiles() se usa para indicar si se respetan las rutas originales de los archivos y el tercero especifica el nombre de carpeta donde se almacenarán dentro del fichero .zip.

2. Creación de ActionResults personalizados

Aunque estrictamente hablando no tendría por qué ser así, la práctica totalidad de las acciones en ASP.NET MVC retornan un objeto de tipo ActionResult, como en el siguiente ejemplo:
public ActionResult About()
{
    return View();
}
La clase abstracta ActionResult se define en el espacio de nombres System.Web.Mvc de la siguiente forma:
public abstract class ActionResult
{
    public abstract void ExecuteResult(ControllerContext context);
}
Cuando una acción retorna un subtipo de ActionResult, el framework se encarga de invocar a su método ExecuteResult() para que envíe el resultado al cliente. Por ejemplo, en el caso de un ViewResult (el retorno generado por el método View() del controlador), su método ExecuteResult() es el responsable de ponerse en contacto con el motor de vistas para generar el HTML, y retornar el marcado al cliente; un RedirectResult, en cambio, sólo se encargará de retornar una redirección (temporal o permanente).

Por tanto, lo único que necesitamos para crear nuestro tipo de resultado personalizado es crear una clase que herede de ActionResult e implementar en ella el método ExecuteResult(). En la práctica, normalmente encontraremos en este método la lógica de generación del resultado, establecimiento de encabezados de la respuesta (content-type, content-disposition, status code, etc.), y el envío a través del canal de salida de la información deseada.

3. ZipResult, el ActionResult que retorna archivos .zip

A continuación se muestra el código de la clase ZipResult, que se encarga de retornar al cliente un archivo comprimido en formato .zip en cuyo interior se encontrarán todos los archivos indicados en el momento de su instanciación.

Lo que vale la pena leer está prácticamente al final de la porción de código, el método ExecuteResult(), que es el que realmente realiza el trabajo de comprimir y enviar al cliente el archivo resultante:

public class ZipResult : ActionResult
{
    private IEnumerable<string> _files;
    private string _fileName;
 
    public string FileName
    {
        get
        {
            return _fileName ?? "archivo.zip";
        }
        set { _fileName = value; }
    }
 
    public ZipResult(params string[] files)
    {
        this._files = files;
    }
 
    public ZipResult(IEnumerable<string> files)
    {
        this._files = files;
    }
 
    public override void ExecuteResult(ControllerContext context)
    {
        using (ZipFile zf = new ZipFile())
        {
            zf.AddFiles(_files, false, "");
            context.HttpContext
                .Response.ContentType = "application/zip";
            context.HttpContext
                .Response.AppendHeader("content-disposition", "attachment; filename=" + FileName);
            zf.Save(context.HttpContext.Response.OutputStream);
        }
    }
 
} 
Observad que el método ExecuteResult() es prácticamente idéntico al Comprime() que mostrábamos más arriba para ver lo fácil que resultaba comprimir archivos con DotNetZip. Sólo le estamos añadiendo los encabezados para la respuesta HTTP, y estamos haciendo que el archivo sea salvado directamente sobre el stream de salida en lugar de hacerlo en disco.

4. Uso desde el controlador

Y para utilizar nuestro flamante ActionResult, lo único que debemos hacer es instanciarlo desde la acción, suministrarle las rutas hacia los archivos que deseamos comprimir y retornarlo como resultado:
public ActionResult Descargar()
{
    return new ZipResult(
        Server.MapPath("~/Archivos/fich1.txt"),
        Server.MapPath("~/Archivos/fich2.txt"),
        Server.MapPath("~/Archivos/fich3.txt")
    );
}
Demo de ZipResultPara que podáis verlo en vivo y en directo, he colgado en SkyDrive una demo algo más completita en la que es posible elegir los archivos de una carpeta, que son comprimidos y retornados por ZipResult.

image Descargar proyecto de demostración.

Espero que os resulte interesante.

Publicación original (6 de julio de 2011): http://www.variablenotfound.com/2011/07/aspnet-mvc-retorno-de-archivos-zip.html

Este artículo y muchos más en: Variable not found.