Establecer el foco automáticamente, versión declarativa

ASP.NET MVCHace poco escribía un post en el que mostraba cómo se podía conseguir establecer el foco inicialmente en un control de edición, algo que era posible con Webforms pero no directamente con las herramientas que ASP.NET MVC trae de fábrica.

La solución propuesta consistía en introducir en la vista código de script para desplazar el foco hasta el control indicado mediante una llamada al helper Html.SetFocusTo(), que implementábamos en el mismo post, aunque hay otras formas para conseguirlo.

Unos días después, un amigo de Variable not found preguntaba en un comentario si no había una forma de conseguir lo mismo utilizando una sintaxis más declarativa, al estilo de la especificación de metadatos o validadores basados en atributos o data annotations.

En primer lugar he de decir que no creo que sea muy conveniente hacerlo, puesto en casi todos los escenarios posicionar el foco en un elemento es pura presentación, no suele haber decisiones de negocio tras ello, y por tanto no es algo que debamos sacar de la Vista. Pero bueno, independientemente de eso, vamos a ver cómo podríamos conseguirlo.

Una forma relativamente sencilla de hacerlo sería utilizando atributos de metadatos personalizados de forma muy similar al ejemplo que vimos también en un post anterior.

Lo primero que necesitamos es definir un atributo que implemente IMetadataAware, lo que indica que incluye información sobre metadatos, y aprovechar el método OnMetadataCreated() para introducir en la colección de metadatos adicionales una señal que permita más adelante detectar la propiedad a “enfocar”:

public class FocusedAttribute : Attribute, IMetadataAware
{
    public const string MetadataKey = "Focused";
    public void OnMetadataCreated(ModelMetadata metadata)
    {
        metadata.AdditionalValues[MetadataKey] = true;
    }
}

De esta forma, para activar el foco inicialmente sobre el editor de una propiedad concreta, simplemente deberíamos marcarla con el atributo [Focused], como se muestra en el código siguiente:

public class Friend
{
    [Focused]
    public string Name { get; set; }
 
    public string Email { get; set; }
    public string Phone { get; set; }
    public string Fax { get; set; }
 
}

Una vez finalizados los preparativos, tenemos que ver cómo implementarlo en la vista. Lo más sencillo en este caso sería crear el helper Html.Autofocus(), que será encargado de localizar la propiedad con el atributo [Focused] y generar sobre la vista el script que mueva el foco a la misma:

public static class HtmlExtensions
{
    public static MvcHtmlString Autofocus(this HtmlHelper html)
    {
        var template = html.ViewData.TemplateInfo;
        var focusedProperty = 
             (from property in html.ViewData.ModelMetadata.Properties
              where property.AdditionalValues.Any(
                     a => a.Key == FocusedAttribute.MetadataKey && (bool) a.Value)
              select template.GetFullHtmlFieldId(property.PropertyName)
             ).FirstOrDefault();
        if (focusedProperty != null)
        {
            return createAutofocusScript(focusedProperty);
        }
 
        return null;
    }
 
    private static MvcHtmlString createAutofocusScript(string focusedProperty)
    {
        string script = "<script type='text/javascript'>" +
                        "$(function() { $('#" + focusedProperty + "').focus(); });" +
                        "</script>";
        return MvcHtmlString.Create(script);
    }
}

Por último, basta con introducir en la vista la siguiente llamada. O mejor aún, si queremos que esta funcionalidad esté activa en todas las vistas, podemos añadirla al final del Layout que estemos empleando para ellas:

@Html.Autofocus()

Hay que tener en cuenta que nada impide que el atributo Focused sea empleado en más de una propiedad, en cuyo caso el foco se establecerá en la primera ocurrencia localizada. Esto se podría evitar llevando el atributo a nivel de clase e indicando como parámetro el valor de la propiedad (algo así como [DefaultProperty("Name")] sobre la propia entidad). En el proyecto de demostración podréis encontrar también esta implementación.

Descargar proyecto de demostración (VS2010+MVC 3) desde Skydrive.

Publicado en Variable not found.

IMetadataAware, atributos de metadatos personalizados

Hace pocas semanas profundizamos en los mecanismos de obtención de metadatos del modelo en ASP.NET MVC y vimos cómo extender el framework para dotarlo de vías alternativas desde las que obtener esta información usando un proveedor personalizado.

Sin embargo, no es este el único mecanismo de extensión del framework a este respecto: también podemos crear fácilmente nuevos atributos que aporten información extra de metadatos a las clases y propiedades del Modelo. Y esta es la razón de ser del interfaz IMetadataAware.

El interfaz IMetadataAware

Definido en System.Web.Mvc e introducido con la tercera versión del framework, este interfaz permite crear atributos que encajen muy suavemente en el sistema de generación y obtención de metadatos de ASP.NET MVC, evitando en muchos escenarios la necesidad de escribir proveedores personalizados.
Como podemos ver a continuación, su definición es de lo más simple:

public interface IMetadataAware
{
    void OnMetadataCreated(ModelMetadata metadata);
}

ModelMetadata (parcial)¿Y cómo se utiliza esto internamente? Pues la respuesta la podemos encontrar buceando un poco en el código fuente del framework.

Durante el proceso de obtención de metadatos, el proveedor DataAnnotationsModelMetadataProvider (en conjunción con su tipo base AssociatedMetadataProvider), obtiene los metadatos “estándar” y los introduce en un objeto de tipo ModelMetadata.

Una vez tenemos ya esta información, obtenida según el comportamiento por defecto desde los atributos conocidos que decoran las propiedades del Modelo, se ejecuta el método estático ApplyMetadataAwareAttributes(), que tiene la siguiente pinta:

private static void ApplyMetadataAwareAttributes(
                          IEnumerable<Attribute> attributes, 
                          ModelMetadata result)
{
    foreach (IMetadataAware awareAttribute in attributes.OfType<IMetadataAware>())
    {
        awareAttribute.OnMetadataCreated(result);
    }
} 

Como se puede deducir a la vista del código, el método es invocado suministrándole, por una parte, todos los atributos localizados en la clase del Modelo, y por otra, el objeto ModelMetadata que contiene la información de metadatos obtenidos hasta el momento.

Ya en su interior, lo único que se hace es obtener todos aquellos atributos que implementen el interfaz IMetadataAware y llamar a su único método OnMetadataCreated() con objeto de que actualicen los metadatos con la información que necesiten.

Por tanto, en la práctica, si queremos crear un atributo que introduzca información adicional de metadatos, o simplemente modifique los existentes, debemos:

  • crear un atributo como siempre, heredando de Attribute,
  • hacer que la clase implemente el interfaz IMetadataAware,
  • implementar en el método OnMetadataCreated() para introducir la metainformación que nos interese en el objeto ModelMetadata que recibimos como parámetro .

Por ejemplo:

public class MyMetadataAttribute: Attribute, IMetadataAware
{
    public void OnMetadataCreated(ModelMetadata metadata)
    {
        // Set properties...
        metadata.DisplayName = "My text";
 
        // ... or add new properties to AdditionalValues
        metadata.AdditionalValues["MyKey"] = "My value";
    }
}

Observad que en el cuerpo del método tenemos acceso a todas las propiedades de metadatos, así como a su diccionario AdditionalValues, donde podemos introducir cualquier tipo de información que nos interese poner a disposición de la Vista.

¿Un ejemplo rápido?

Vamos a desarrollar un atributo de metadatos personalizado al que llamaremos Important, y que hará lo siguiente:

  • en primer lugar, añadirá al DisplayName de la propiedad (el texto que aparece en su correspondiente etiqueta) el sufijo “important!”.
  • añadirá a la colección AdditionalValues un valor que permita a la vista destacar el editor de la propiedad visualmente.

(Ojo, que hay otras formas para conseguir este mismo resultado, pero el post trata sobre IMetadataAware, así que tendremos que quedarnos con esta ;-))

public class ImportantAttribute: Attribute, IMetadataAware
{
    public void OnMetadataCreated(ModelMetadata metadata)
    {
        metadata.DisplayName = metadata.GetDisplayName() + " (important!)";
        metadata.AdditionalValues["Important"] = true;
    }
}

Eso es todo lo que necesitamos en nuestro atributo. Hecho esto, ya podemos utilizarlo en una clase del Modelo como se muestra a continuación:

public class Friend
{
    [Important]
    public string Name { get; set; }
 
    public string Email { get; set; }
 
    [Important]
    public string Phone { get; set; }
 
    public string Fax { get; set; }
 
}

El simple hecho de decorar las propiedades Name y Phone con nuestro flamante atributo [Important] hará que sus etiquetas descriptivas aparezcan ya con el sufijo “important!”, puesto que estamos modificando directamente su DisplayName. Si queremos además destacar los editores necesitaremos inyectar un poco de lógica en la vista, cosa que podemos hacer de forma relativamente sencilla implementando un helper o modificando las plantillas de edición por defecto para los tipos utilizados (en este caso, strings).

Una implementación rápida de un helper que genere código script para destacar las propiedades importantes podría ser la siguiente. Si os fijáis, lo único que hace es obtener aquellas propiedades en cuyos metadatos exista la entrada de AdditionalValues establecida en el atributo [Important], y establecer en sus editores la clase CSS “important” para darles un poco de color:

public static class HtmlExtensions
{
    public static MvcHtmlString HighlightImportant(this HtmlHelper html)
    {
        var template = html.ViewData.TemplateInfo;
        var importantProps = from property in html.ViewData.ModelMetadata.Properties
                                where property.AdditionalValues.Any(
                                    a => a.Key == "Important" && (bool)a.Value)
                                select template.GetFullHtmlFieldId(property.PropertyName);
                                       
        if (importantProps.Any())
        {
            string commaIds = string.Join(",#", importantProps);
            string script = "<script type='text/javascript'>" +
                            "$(function() { $('#" + commaIds + "').addClass('important'); });" +
                            "</script>";
            return MvcHtmlString.Create(script);
        }
        return null;
    }
}

El atributo Important en acciónObviamente, tendríamos que incluir una llamada al helper Html.HighlightImportant() en las vistas o, si queremos mayor comodidad, en el Layout del sitio web.

En la captura de pantalla de la derecha podéis ver el resultado de este ejemplo en funcionamiento. Y si queréis probarlo y juguetear un rato con él, podéis descargar el proyecto para Visual Studio y ASP.NET MVC 3 desde mi Skydrive.

Publicación original (20-dic-2011): http://www.variablenotfound.com/2011/12/imetadataaware-atributos-de-metadatos.html

Adzure, la nube gratuita soportada por publicidad

Microsoft es una compañía que nos tiene acostumbrados a giros extraños en sus políticas de comercialización y distribución de productos, y está claro que la nube no iba a permanecer ajena a ellos.

Windows AdzureHace un par de días, ante el asombro de los presentes, Fred Swarm (cloud manager para la región EMEA) anunció la próxima apertura de Adzure, la edición gratuita del servicio Windows Azure soportada por publicidad.

Esto supone una auténtica revolución en el mundo de los servicios en la nube, que pasan del tradicional enfoque pay-per-use hacia un modelo de negocio basado en los ingresos publicitarios. Es decir, serán los anunciantes los que soportarán los costes de mantener en funcionamiento los roles e instancias de nuestras aplicaciones en la nube, la ocupación en disco, transferencia, uso de CDN, etc., sólo a cambio de que dejemos que se inserten anuncios publicitarios en determinados puntos de nuestros sistemas.

A cambio, no tendremos restricción alguna en cuanto al número de procesadores, memoria, ni ocupación de la infraestructura del gigante de Redmond. Podemos utilizar todos los recursos que necesitemos, y de hecho, se anima a que así sea: si una aplicación web debe escalar de forma brutal es porque tiene muchos visitantes… y visitantes es igual a impactos publicitarios, por lo que sería el escenario ideal para todos.

Adzure insertando publicidad en el interior de imágenesUna de las cosas más curiosas de este servicio es precisamente que la inserción de la publicidad se realizará de forma automática, los desarrolladores no tendremos que hacer nada al respecto: simplemente, los anuncios aparecerán en puntos donde la plataforma considere oportuno.

Esto es posible gracias a la nueva tecnología AdInsight®, que se encarga de analizar nuestro software una vez lo hemos desplegado en Adzure e inyectar en ellos la lógica de generación de anuncios publicitarios. Está claro que este automatismo a veces puede provocar pequeñas intrusiones como la que veis en la captura de pantalla adjunta, pero estaréis de acuerdo en que es un precio bastante razonable teniendo en cuenta el valor y potencia de la infraestructura que estamos usando de forma gratuita 🙂

Según se desprende del documento de notas de la revisión, existen tres únicos puntos de inserción publicitaria automática:

  • las páginas web generadas por un WebRole podrán ser modificadas de forma automática para incluirle código de script para la generación de anuncios contextuales.
  • el servicio de almacenamiento blob para contenidos digitales (imágenes, html, PDF, PPTs, vídeos, etc.) podrá introducir contenidos publicitarios en el interior de estos archivos, aunque casi siempre de forma no intrusiva.
  • los mails enviados y recibidos a través del servicio Adzure Mail Connector®, el motor que hay por detrás de Hotmail, también llevarán un mensaje publicitario contextualizado a la temática del mensaje.

El resto de servicios en la nube (como el almacenamiento en tablas, SQL Server, queues, buses, etc., de momento están libres de publicidad y pueden ser utilizados libremente y sin restricciones

Los anuncios se seleccionan de forma automática teniendo en cuenta la temática de la aplicación, el contexto en el que se insertan, el perfil del usuario conectado, y varios factores más. Sin embargo, en lugar de ser gestionados por la plataforma Microsoft Advertising, han preferido llegar a un acuerdo con Google e integrar la inteligencia de marketing de Adsense, bastante depurada, certera y que tantas alegrías nos da a sus usuarios.

Pero, ¿y qué ocurre con los ingresos generados por publicidad en las aplicaciones desplegadas en Adzure? Pues una nueva sorpresa: Microsoft los compartirá con los desarrolladores o propietarios del sitio en una proporción del 40% al 60% dependiendo del tráfico. Es decir, no sólo no nos costará utilizar la infraestructura de Adzure, sino que además podemos ganar dinero con ello 🙂

Pues no sé qué os parecerá a ustedes, pero yo veo la idea interesantísima. Adzure permite el uso de todos los servicios de la nube de forma gratuita, lo que abre un campo enorme a pequeñas empresas y startups que necesitan disponer de la gran capacidad de la nube, pero sin incurrir en coste alguno, simplemente a cambio de permitir la inserción automática de estos anuncios publicitarios en sus servicios, lo cual en algunos escenarios no tiene por qué ser un problema grave; de hecho, Gmail lo hace y no nos quejamos demasiado 😉

Sitio web alojado en Adzure con publicidad. Pulsar para verlo "en vivo".Además, el hecho de que aspectos infraestructurales como los servidores, anchos de banda, seguridad, etc., pasen de ser un coste a una fuente de ingresos me parece una vuelta de tuerca absolutamente necesaria.

De hecho, ya he comenzado a migrar todos mis desarrollos a Adzure, y el resultado está bastante bien (podéis ver una captura en el lateral); sin tocar una línea de código, sólo desplegarlo en la nube, la publicidad insertada se integra perfectamente en los diseños e incluso diría que lo mejoran, y por no hablar de que en unos días ya he conseguido algunos centimillos que nunca vienen mal.

Por cierto, actualmente estos servicios están en fase de beta privada y sólo se puede acceder por invitación, pero tengo treinta invitaciones para repartir entre los lectores del blog que estén realmente interesados en probarlo. Simplemente poneos en contacto conmigo por la vía que os sea más cómoda (por ejemplo ésta) y os iré enviando la URL y password personal de acceso en riguroso orden de llegada.

Y si preferís esperar, el servicio se abrirá al público en general el próximo uno de abril.

 

 

[Actualizado 29/12]
Nota para despistados: obviamente la noticia no es real, se trata simplemente de una broma del Día de los Inocentes. Por tanto, lamento comunicaros a los (muchos) que estáis esperando una invitación que de momento no va a poder ser ;-P

 

Publicado en Variable not found.

ASP.NET MVC: establecer el foco en un control al cargar la página

ASP.NET MVCUno de los aspectos más criticados por los desarrolladores cuando comienzan a trabajar con ASP.NET MVC es el hecho de tener que volver a resolver problemas que estaban ya más que solucionados en Webforms.

Y uno de estos casos es un detallito muy simple pero útil: establecer el foco de edición en un control concreto al cargar una página. En Webforms era suficiente con asignar al atributo defaultFocus del tag <form> el nombre del control que nos interesara, y ya lo teníamos solucionado; ASP.NET MVC no trae ninguna solución “de serie” para conseguirlo, aunque, como veremos en este post, no es mucho más complicado que la alternativa Webforms una vez hemos preparado la infraestructura necesaria.

En líneas generales, la solución consiste en utilizar un pequeño script que, una vez cargada la página, mueva el foco hacia el control que nos interese. Por ejemplo, si quisiéramos que el campo con identificador “Nombre” obtenga el foco automáticamente, podríamos utilizar jQuery de la siguiente forma:

<script type="text/javascript">
    $(function () {
        $("#Nombre").focus();
    });
</script>

Aunque eficaz, es una solución demasiado trabajosa e introduce una dependencia en el código, difícilmente manejable, hacia el nombre del control. Si quisiéramos introducir este código en múltiples puntos de nuestra aplicación tendríamos que copiar y pegar en cada vista, y sustituir el id del control apropiado en cada caso, lo cual es bastante incómodo.

¿No podríamos mejorar esto un poco?

El helper SetFocusTo()

Una posible solución, realmente rápida de implementar, sería crear un helper personalizado, al que llamaremos SetFocusTo(), que genere el código por nosotros partiendo únicamente de la información de la propiedad a editar.

Y para evitar la dependencia hacia el nombre de la propiedad y hacerlo más flexible, incluso podríamos crear la versión fuertemente tipada del mismo, de forma que podamos utilizarlo de cualquiera de las siguientes formas:

// Usando el nombre de la propiedad:
@Html.SetFocusTo("Nombre")

// Usando la versión fuertemente tipada:
@Html.SetFocusTo(model=>model.Nombre)

El código del helper es el siguiente:

public static class HtmlHelpers
{
    public static MvcHtmlString SetFocusTo<TModel, TProperty>(
                    this HtmlHelper<TModel> html,
                    Expression<Func<TModel, TProperty>> expression)
    {
        var prop = ExpressionHelper.GetExpressionText(expression);
        return html.setFocusTo(prop);
    }

    public static MvcHtmlString SetFocusTo(this HtmlHelper html, 
                                           string propertyName)
    {
        var prop = ExpressionHelper.GetExpressionText(propertyName);
        return html.setFocusTo(prop);
    }

    private static MvcHtmlString setFocusTo(this HtmlHelper html, 
                                            string property)
    {
        string id = html.ViewData.TemplateInfo.GetFullHtmlFieldId(property);
        var script = "<script type='text/javascript'>" +
                        "$(function() {" +
                            "$('#" + id + "').focus();" +
                        "});" +
                        "</script>";
        return MvcHtmlString.Create(script);
    }

}

Recordad que para que estos helpers estén visible en las vistas y podáis usarlos normalmente, debéis incluir el espacio de nombres en el que están definidos bien sea añadiendo una directiva @using en la vista, o bien en la sección <namespaces> del archivo web.config que encontraréis en la carpeta /Views (o en la del área correspondiente).

¡Y eso es todo! De esta forma ya volvemos a disponer de la posibilidad de establecer el foco por defecto sobre un control de forma muy rápida, compacta, y cómoda de utilizar.

Publicación original (14-dic-2011): http://www.variablenotfound.com/2011/12/aspnet-mvc-establecer-el-foco-en-un.html

Mamá, ¿de dónde vienen los metadatos?

ASPNETMVCEn ASP.NET MVC normalmente utilizamos atributos para aportar información adicional a las propiedades del Modelo, incluyendo detalles como su descripción textual, formato de presentación, tipo de datos, etc. Esta información puede ser utilizada desde la capa vista para generar etiquetas, editores y, en algunos casos, incluso lógica de edición o presentación en la página.

Sin embargo, los atributos en el propio código de la clase no son la única vía para especificar metadatos en el framework. En este post veremos cómo extender el framework para crear nuevas vías para especificar esta información.

1. Proveedores de metadatos

Como decíamos, ASP.NET MVC viene de fábrica con componentes que nos permiten introducir anotaciones directamente sobre las propiedades de las clases que manejamos. En el siguiente ejemplo podemos ver una entidad en la que se están introduciendo metadatos según este mecanismo:
public class Friend
{
    [Required]
    [Display(Name="Full name")]
    public string FullName { get; set; }

    [Required]
    [DataType(DataType.EmailAddress)]
    [Display(Name = "Email address")]
    public string EmailAddress { get; set; }

    [Required]
    [DataType(DataType.Url)]
    [Display(Name = "Blog url")]
    public string BlogUrl { get; set; }

    [Display(Name="Birth year")]
    public int BirthYear { get; set; }
}
Cuando el framework necesita obtener los metadatos relativos a una clase, utiliza un metadata provider, un componente que se encarga de obtener los metadatos desde donde se encuentren definidos. El proveedor usado por defecto se llama DataAnnotationsModelMetadataProvider, y es el responsable de leer los atributos desde la definición de la clase, pero podemos sustituirlo fácilmente por otro proveedor que obtenga los metadatos desde otros orígenes, como archivos de configuración, bases de datos, o simplemente introducir lógica durante la generación de los mismos.

ModelMetadataSea cual sea su origen, los metadatos siempre se representan como objetos del tipo ModelMetadata, cuya estructura podéis ver a la derecha. Los providers deben incluir la lógica para obtener los metadatos desde donde corresponda, pero siempre retornarán objetos ModelMetadata, en los que, entre otra información, podemos encontrar los siguientes datos:

  • descripción del elemento,
  • cadena de formato cuando es visualizado,
  • tipo de datos que contiene (emails, urls, fechas, horas, etc.),
  • orden en el que debe aparecer la propiedad si se generan interfaces de edición o visualización de la entidad,
  • representación textual para los nulos,
  • si el dato es obligatorio, o de sólo lectura,
  • si debe ser mostrado en edición, o en visualización,
  • la plantilla de edición o visualización que debe utilizarse,
  • … y muchos más. Podéis consultar la referencia completa en MSDN.
Es importante tener en cuenta que el proveedor es invocado una vez para obtener los metadatos de la clase en sí, y otra vez por cada una de sus propiedades.

¿Y cómo determina el framework MVC qué proveedor de metadatos utilizar?

En primer lugar, intenta utilizar el dependency resolver para obtener una instancia de un tipo que implemente ModelMetadataProvider , que es la clase base para todos los proveedores. Este punto de extensión permite definir un proveedor personalizado muy fácilmente, y sobre todo si utilizamos contenedores IoC.

Si no ha sido posible obtener un proveedor desde el dependency resolver, el framework utilizará el establecido en la propiedad ModelMetadataProviders.Current. Por defecto, esa propiedad contiene una un objeto de la clase DataAnnotationsModelMetadataProvider, pero podemos sustituirlo por cualquier otro descendiente de ModelMetadataProvider .

Por tanto, para crear un proveedor personalizado de metadatos, basta con:

  • crear una clase descendiente de ModelMetadataProvider . Por comodidad, usaremos normalmente como base algunos de los siguientes tipos, más concretos:
    • DataAnnotationsModelMetadataProvider, si lo que pretendemos es extender el sistema por defecto, basado en la captura de metadatos desde las anotaciones (atributos) de la clase.
    • AssociatedMetadataProvider, si lo que queremos es saltarnos por completo la obtención de metadatos desde la propia clase e implementar otros mecanismos.
  • indicar al framework que debe utilizar nuestro nuevo proveedor, que podemos hacerlo:
    • o bien usando el dependency resolver,
    • o bien estableciendo una instancia del nuevo provider en ModelMetadataProviders.Current.
Pero mejor veámoslo con un ejemplo…

2. Creando un metadata provider

Vamos a implementar un proveedor de metadatos sencillo con objeto de que podáis entender su funcionamiento.

Como sabéis, al mostrar en una vista las etiquetas (labels) asociadas a una propiedad del Modelo, el texto que aparece es obtenido desde los metadatos de la entidad, desde el atributo Display o DisplayName (en ese orden); en caso de no existir, se asume como descripción el nombre de la propiedad.

Pues bien, nuestro objetivo es conseguir generar de forma automática estas las descripciones (DisplayName) de cada campo partiendo del nombre de la propiedad, y teniendo en cuenta el “camel casing”, de forma que podremos ahorrarnos el teclear esta descripción en muchos casos. Por ejemplo, a una propiedad que se llame FullName se le asociará automáticamente la descripción “Full Name”, o EmailAddress se describirá como “Email Address”. De esta forma la entidad anterior podremos simplificarla un poco:Resultado de EditorForModel() con la entidad Friend

public class Friend
{
    [Required]
    public string FullName { get; set; }

    [Required]
    [DataType(DataType.EmailAddress)]
    public string EmailAddress { get; set; }

    [Required]
    [DataType(DataType.Url)]
    public string BlogUrl { get; set; }

    public int BirthYear { get; set; }
}
Fijaos que hemos eliminado los atributos [Display], y el resultado en ejecución de un editor de esta entidad se mostrará como en la captura de pantalla lateral, donde las etiquetas de cada campo han sido generadas automáticamente.

Para ello, dado que queremos conservar el comportamiento del proveedor por defecto DataAnnotationsModelMetadataProvider, lo extenderemos y añadiremos la lógica deseada a su método CreateMetadata(), que es el invocado para obtener los metadatos de la entidad y cada una de las propiedades:

public class DisplayNameModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    protected override ModelMetadata CreateMetadata(
            IEnumerable<Attribute> attributes, Type containerType,
            Func<object> modelAccessor, Type modelType, string propertyName)
    {
        var metadata = base.CreateMetadata(attributes, containerType,
                                            modelAccessor, modelType, propertyName);
 
        if (metadata.PropertyName != null && metadata.DisplayName == null)
        {
            metadata.DisplayName = splitCamelCase(metadata.PropertyName);
        }
 
        return metadata;
    }
 
    static string splitCamelCase(string input)
    {
        return Regex.Replace(input, "([A-Z])", " $1", 
            System.Text.RegularExpressions.RegexOptions.Compiled).Trim();
    }
}
La implementación del método CreateMetadata() no es nada del otro mundo:
  • En primer lugar, llamamos a la clase antecesora para obtener los metadatos a partir de las anotaciones del modelo.
  • A continuación, si no se ha obtenido ningún valor de metadatos para el DisplayName, y siempre que se esté evaluando una propiedad, la establecemos, generándola mediante el método splitCamelCase().
  • El método splitCamelCase() no tiene mucho misterio (bueno, sí, usa expresiones regulares ;-)), y lo único que hace es buscar las letras mayúsculas e insertar delante de ellas un espacio. Es algo tosco, pero me vale para no desviar la atención del post a este detalle sin importancia (y por cierto, el mérito de este método no es mío, sino de Jon Galloway).
Y esto es todo. Ahora vamos a registrar el proveedor para el framework pueda utilizarlo, y veremos cómo hacerlo de las dos formas que he comentado antes: de forma directa, y usando el dependency resolver.

2.1. Registro del proveedor directamente

La fórmula más rápida y sencilla consiste en establecer directamente una instancia del nuevo proveedor en la propiedad ModelMetadataProviders.Current, durante la inicialización de la aplicación, por ejemplo así:
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
 
        ModelMetadataProviders.Current = new DisplayNameModelMetadataProvider();
 
        RegisterGlobalFilters(GlobalFilters.Filters);
        RegisterRoutes(RouteTable.Routes);
    }
Esto es todo lo que necesitamos hacer para que nuestro flamante proveedor entre en funcionamiento.

2.2. Registro del proveedor usando el dependency resolver

Si estáis utilizando un contenedor de IoC, quizás sea más coherente introducir la resolución de este proveedor en el mismo.

Cuando el framework intenta obtener el proveedor de metadatos actual, antes de nada intenta localizarlo utilizando el dependency resolver. Si estamos utilizando un contenedor IoC como Unity o StructureMap, podemos utilizarlos para gestionar esta dependencia, asociando la clase abstracta ModelMetadataProvider al tipo concreto DisplayNameModelMetadataProvider.

Por ejemplo, en el caso de Unity, lo más sencillo es instalar el paquete Unity.MVC3 desde Nuget, y ya simplemente tendríamos que incluir la siguiente línea en el registro de tipos (en el archivo bootstrapper.cs):

    private static IUnityContainer BuildUnityContainer()
    {
        var container = new UnityContainer();
        container.RegisterType<ModelMetadataProvider, DisplayNameModelMetadataProvider>();
        container.RegisterControllers();
        return container;
    }
Una vez registrado el provider, sea directamente o mediante este último mecanismo, el sistema obtendrá los metadatos desde éste. Así, tendríamos el siguiente resultado:

Clase del Modelo Vista Resultado
public class Friend
{
 [Required]
 public string FullName { get; set; }

 [Required]
 [DataType(DataType.EmailAddress)]
 public string EmailAddress 
               { get; set; }
 [Required]
 [DataType(DataType.Url)]
 public string BlogUrl { get; set; }
 
 public int BirthYear { get; set; }
}

@using (Html.BeginForm())
{
    @Html.EditorForModel()
 
    <div>
        <input type="submit" />
    </div>
}
image
Puedes descargar el proyecto de ejemplo (VS2010 + MVC 3) desde mi Skydrive.

En definitiva, en este post hemos visto que en ASP.NET MVC los metadatos no proceden de una ubicación fija, sino que son obtenidos a través de un proveedor configurable. Esto nos permite modificar el comportamiento por defecto del framework y adaptarlo a nuestras necesidades, generarlos de forma dinámica, como en el ejemplo que hemos visto, u obtenerlo desde algún almacén de persistencia (como podría ser un archivo de configuración o una bases de datos), y siempre de forma muy sencilla aprovechando la magnífica extensibilidad del marco de trabajo 🙂

Publicación original (29-11-2011): http://www.variablenotfound.com/2011/11/mama-de-donde-vienen-los-metadatos.html
Tip: ¿todavía no conoces la selección de enlaces interesantes de Variable not found?

 

ASP.NET MVC: Introducir lógica personalizada al detectar errores de validación en cliente

ASP.NET MVCEl sistema de validación en cliente de ASP.NET MVC, como sabemos basado en jQuery validate, es el encargado de mostrar u ocultar los mensajes de error asociados a cada campo conforme va comprobando su validez.

Los mensajes de error asociados a cada validador son almacenados inicialmente en atributos data-val-* sobre el control a comprobar, y cuando se detecta un problema de validación, son mostrados copiando su contenido al interior de la etiqueta <span> que el helper Html.ValidationMessage() habrá generado sobre la página.

Sin embargo, al hilo de una consulta reciente en los foros de ASP.NET MVC en MSDN, perfectamente contestada por el amigo Eduard Tomás, pensé que realmente tenemos poco control sobre cómo se muestran estos errores, así que me he puesto un rato a ver cómo podíamos conseguir introducir lógica personalizada en este punto aprovechando la flexibilidad que ofrece jQuery validate 1.9.

Salvo por la escasez de documentación de este componente, tomar el control en el momento de mostrar los mensajes de error es bastante sencillo. Basta con establecer una función en la propiedad showErrors de los settings del plugin, cosa que podemos hacer con el siguiente script de inicialización:

<script type="text/javascript">
    $(function () {
        var settings = $.data($('form')[0], 'validator').settings;
        settings.showErrors = function (errorMap, errorList) {
            // Aquí el código personalizado:
            [...]

            // Y si nos interesa, finalmente podemos
            // ejecutar el comportamiento por defecto

            this.defaultShowErrors();
        };
    });
</script>

(Por simplificar, estamos asumiendo que en el formulario hay un único tag <form>, que es el que capturamos con el selector).

La función showErrors() recibe dos parámetros. El primero es un “mapa” donde asociamos a cada clave (nombre del campo) el mensaje de error que tenga asociado. Así, por ejemplo, el valor de errorMap.Nombre será nulo si el campo “Nombre” del formulario no tiene ningún error (ha validado correctamente), o el texto del error en caso contrario.

En el segundo parámetro de la función encontraremos un array con los errores a mostrar. En cada elemento tendremos disponible la propiedad element, desde la que podemos acceder al control que ha generado el error, y message, donde podemos consultar o establecer la descripción del mismo.

Es importante tener en cuenta que la función showErrors() es invocada con mucha frecuencia durante el proceso de edición (pérdida de foco, obtención de foco, pulsación de teclas…), por lo que desde el punto de vista de la usabilidad no tiene demasiado sentido introducir en ella procesos bloqueantes (como puede ser un alert()) o demasiado largos en el tiempo, para evitar que se solapen.

Por ejemplo, en el siguiente código utilizamos el efecto “highlight” de jQuery UI para resaltar con un rápido destello el elemento en el que se ha detectado un error:

    settings.showErrors = function (errorMap, errorList) {
        for (var i = 0; i < errorList.length; i++) {
            var error = errorList[i];
            // error.element es el elemento que ha provocado el error
            $(error.element).effect("highlight", { times: 1 }, 100);
        }
        this.defaultShowErrors();
    };

En fin, algo no demasiado útil ;-P, pero interesante en cualquier caso para profundizar un poco en los misterios e interioridades de jQuery validate.

Publicación original (22/11/2011): http://www.variablenotfound.com/2011/11/aspnet-mvc-introducir-logica.html
Tip: ¿todavía no conoces la selección de enlaces interesantes de Variable not found?

 

Limpiar un input type=file

No es algo excesivamente frecuente, pero en ocasiones podemos necesitar limpiar el valor de un campo de tipo file (el que usamos para hacer los uploads) de un formulario, por ejemplo, para evitar que el usuario envíe un archivo que por cualquier motivo no deba ser subido al servidor.

O dicho de otra forma, imaginemos la siguiente porción de un formulario en pantalla, que podría ser generada con el código que podéis ver justo a continuación:
Campo de envío de archivos

<label for="archivo">Archivo a enviar:</label>
<input type="file" id="archivo" name="archivo" />
<input type="button" onclick="limpiarInputFile('archivo');" value="Limpiar" />

Y la pregunta en este momento sería, ¿qué código deberíamos implementar en la función limpiarInputFile() que estamos utilizando en el evento onclick si quisiéramos limpiar o inicializar el contenido del campo archivo?

Aunque de forma intuitiva podría parecer que basta con establecer el valor del campo a una cadena vacía, una prueba rápida nos demostrará que esto no es posible. Desde hace ya tiempo, por motivos de seguridad, los navegadores no permiten el acceso de escritura a la propiedad value en los campos de envío de archivos, por lo que nos encontramos una vía sin salida. Esto lo vemos con el siguiente código, utilizando jQuery:

    function limpiarInputfile(id) {
        var input = $('#' + id);
        var nuevoValor = "c:\windows\system32\mspaint.exe";
        
        alert(input.val());       // Muestra "C:Datos.dat"
        input.val(nuevoValor);    // Establecemos un nuevo valor
        alert(input.val());       // ¡¡Muestra "C:Datos.dat"!!
    }

Como vemos, somos vilmente ignorados cuando intentamos establecerle un valor.

Pues bien, una posible solución consiste en eliminar del DOM el elemento <input type="file"> y volver a crearlo justo después en el mismo lugar. He visto por ahí varias implementaciones que obligaban a introducir este elemento dentro de un contenedor, pero he creado otra que creo que es más sencilla e igual de efectiva:

    function limpiarInputfile(id) {
        var input = $('#' + id);
        var clon = input.clone();  // Creamos un clon del elemento original
        input.replaceWith(clon);   // Y sustituimos el original por el clon
    }

Y eso es todo :-). Observad que lo único que hacemos es crean un clon del elemento original cuyo value por supuesto estará en blanco (recordad que esta propiedad no se puede establecer), y justo a continuación eliminamos el elemento original sustituyéndolo por este clon.

Si queremos generalizar este código e implementar esta funcionalidad de forma no intrusiva podríamos hacer lo siguiente:

    $(function () {
        $("input[type=file]").after(
            "<input type='button' class='limpiar-inputfile' value='Limpiar'>"
        );
        $(".limpiar-inputfile").click(function () {
            var input = $(this).prev("input[type=file]");
            var clon = input.clone();
            input.replaceWith(clon);
            return false;
        });
    });

Este código añade automáticamente un botón “Limpiar” a continuación de todos los <input type=file> de la página, implementando en el manejador del evento click la lógica de inicialización del componente que hemos visto antes. De esta forma, sólo se introducirá en la página el botón de limpiado cuando estén activados los scripts, que es en el único momento en que su ejecución tendrá sentido con la solución propuesta.

Espero que os sea de utilidad.

Publicado en: Variable not found.

El Tao de la Programación

The Tao of ProgrammingHace unos días charlaba con un colega sobre aspectos filosóficos del desarrollo de software, y me pareció curioso que no conociera “The Tao of Programming”, todo un clásico de la literatura friki, y lectura indispensable para los que nos dedicamos a esto.

Este libro, escrito en el año 1987 por Geoffrey James, recoge, en un humorístico tono de misticismo oriental, distintas historias y parábolas con geniales mensajes de fondo sobre calidad en la programación, gestión de proyectos y otros temas que siguen siendo válidos veinticinco años después.

Como probablemente habrá más lectores que no lo conozcan, me ha parecido interesante reproducirlo aquí traducido en su totalidad, por supuesto contando previamente con el permiso expreso del Sr. James.

¡Que aproveche!

Validator providers en ASP.NET MVC

ASP.NET MVCEn la pasada charla sobre el sistema de validaciones de MVC 3 vimos un ejemplo, creo que bastante ilustrativo, de los proveedores de validación del framework. Concretamente, implementamos un proveedor capaz de obtener las anotaciones partiendo de las restricciones definidas en el web.config. Es decir, las reglas de comprobación como Required o StringLength no las definíamos a nivel de código mediante atributos, sino en el archivo de configuración, lo que podía aportar interesantes ventajas vistas a flexibilizar nuestras soluciones.

En este post vamos a ver un nuevo ejemplo de cómo utilizar el mismo mecanismo de proveedores, pero esta vez para conseguir de forma muy sencilla simplificar el código de clases del modelo en las que todas sus propiedades sean por defecto obligatorias, salvo aquellas en las que indiquemos expresamente lo contrario.

1. Empezando por el final…

Lo que pretendemos hacer es crear dos nuevos atributos, AllRequired y NotRequired. El primero, aplicado a una clase indicará la obligatoriedad de todas sus propiedades, mientras que el segundo, aplicado a una propiedad, indicará que ésta es opcional.

Observad en la siguiente tabla el efecto que podríamos conseguir sobre el código:

Enfoque tradicional Usando AllRequired y NotRequired
public class Friend
{
    [Required] 
    public string Name { get; set; }
    [Required]
    public string Surname { get; set; }
    [Required]
    public string Nickname { get; set; }
    [Required]
    public string Address { get; set; }
    [Required]
    public string City { get; set; }
    [Required]
    public string Country { get; set; }
    public string Phone { get; set; }
    [Required]
    public string MobilePhone { get; set; }
    [Required]
    public string Email { get; set; }
}
[AllRequired]
public class Friend
{
    public string Name { get; set; }
    public string Surname { get; set; }
    public string Nickname { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
    [NotRequired]
    public string Phone { get; set; }
    public string MobilePhone { get; set; }
    public string Email { get; set; }
}

2. Model validator providers

Cuando el framework necesita obtener los validadores asociados a una clase y sus propiedades, no lo hace de forma directa sino a través de unos componentes llamados model validator providers. Éstos son los encargados de acceder a las distintas fuentes en las que puede existir información sobre las reglas de validación para cada clase.

Así, existe “de serie” un model validator provider específico para obtener los validadores partiendo de las anotaciones realizadas sobre la clase (DataAnnotationsModelValidatorProvider ), que es el método utilizado normalmente; existe otro para obtener las implementaciones del interfaz IDataErrorInfo (DataErrorInfoModelValidatorProvider), y otro que se encarga de validaciones implícitas del tipo (ClientDataTypeModelValidatorProvider).

Todos ellos se encuentran registrados en la colección pública ModelValidatorProviders.Providers, de forma que cuando el framework necesita obtener los validadores asociados a una entidad, lo que hace es recorrer dicha colección e ir llamando uno por uno a los proveedores solicitándoles información de validación para cada una sus propiedades.

¡Todo encaja!Este proceso se realiza, cuando se tiene activa la validación en cliente, para obtener las reglas y parámetros que hay que llevar a la vista, o también durante el binding, para obtener los validadores, ejecutarlos y actualizar el ModelState con el resultado.

Pero sin duda lo mejor que tiene este sistema es su facilidad de extensión. Podemos añadir con suma facilidad nuevos proveedores a la colección ModelValidatorProviders a la que hacía referencia anteriormente, de forma que podemos introducir nuestra propia lógica de generación de validadores en el flujo de ejecución de peticiones del framework.

Y es lo que vamos a hacer a continuación: implementaremos nuestro proveedor para generar automáticamente validaciones de tipo Required para todas las propiedades de las clases decoradas con el atributo [AllRequired], exceptuando aquellas propiedades en las que se haya indicado lo contrario con [NotRequired].

3. Los atributos AllRequired y NotRequired

Antes que nada, necesitamos definir estos dos atributos para poder continuar. El código es tan simple como el que veis a continuación:

[AttributeUsage(AttributeTargets.Class)]
public class AllRequiredAttribute : Attribute
{
        
}
 
[AttributeUsage(AttributeTargets.Property)]
public class NotRequiredAttribute: Attribute
{
        
}

Observad que no es necesario que éstos hereden de ValidationAttribute; de hecho, no estamos creando anotaciones personalizadas, sólo atributos que nos permitan “marcar” en clases y propiedades para generar más adelante, desde el proveedor, las anotaciones necesarias.

Los [AttributeUsage] lo único que hacen es restringir su uso de forma que [AllRequired] se pueda utilizar únicamente sobre clases, mientras que [NotRequired] sea utilizable exclusivamente en propiedades, más que nada para que se comprueben en compilación estos aspectos.

4. Implementando nuestro model validator provider

La implementación de un model validator provider es bastante sencilla. Lo único que tenemos que hacer es heredar de ModelValidatorProvider e implementar su método GetValidators(), que es el que invocará el framework para obtener los validadores de cada una de las propiedades de la clase.

En nuestro caso, simplemente comprobaremos que la clase esté marcada como [AllRequired] , retornando un validador de tipo Required para todas aquellas propiedades que no se hayan marcado con [NotRequired]. Casi mejor lo vemos sobre el código:

public class AllRequiredValidatorProvider : ModelValidatorProvider
{
  public override IEnumerable<ModelValidator> GetValidators(
      ModelMetadata metadata, ControllerContext context)
  {
      var allRequiredAttribute =
              TypeDescriptor.GetAttributes(metadata.ContainerType)
              .OfType<AllRequiredAttribute>().FirstOrDefault();
 
      if (allRequiredAttribute != null)
      {
        var property = metadata.ContainerType.GetProperty(metadata.PropertyName);
        var isOptional = property.GetCustomAttributes(typeof(NotRequiredAttribute), false).Any();
        if (!isOptional)
        {
          var requiredValidator = new RequiredAttribute() { ErrorMessage = "Requerido" };
          yield return new RequiredAttributeAdapter(metadata, context, requiredValidator);
        }
      }
  }
}

Observad la firma del método: retorna una colección de objetos que heredan de ModelValidator, que son adaptadores de los distintos mecanismos de validación que podemos emplear en MVC. En la práctica permiten homogeneizar la diversidad de validadores vistas al framework, y a veces incluyen cierta lógica, como la generación de las reglas de validación en cliente.

El framework incluye ya adaptadores para cada una de las anotaciones habituales (RequiredAttributeAdapter, StringLengthAdapter, RegularExpressionAttributeAdapter …), que podemos instanciar directamente para ir construyendo la colección que retorna el método GetValidators(). En el código anterior, fijaos que en la última línea retornamos, usando yield, un nuevo adaptador de tipo RequiredAttributeAdapter, a cuyo constructor suministramos una instancia del atributo Required convenientemente inicializada.

Un último detalle: tened en cuenta que por simplificar el ejemplo no he prestado atención a aspectos como el rendimiento, que podría mejorarse introduciendo una caché, o la personalización de los mensajes de error, que sería fácilmente implementable permitiendo parámetros en el atributo [AllRequired] y suministrándolos más adelante en la instanciación de los RequiredAttribute que retornamos desde el proveedor, como hacemos ahora con la cadena de texto constante “Requerido”.

5. Registro del proveedor

Por último, ya sólo queda indicar al framework que debe utilizar nuestro proveedor para obtener los validadores, por lo que lo añadimos en el global.asax a la colección de providers:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
 
    ModelValidatorProviders.Providers.Add(new AllRequiredValidatorProvider());
 
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}

Casi todo obligatorio¡Y ahí lo tenemos! A partir de este momento, ya podemos hacer uso de estos nuevos atributos en nuestras clases del modelo o view models, como se muestra a continuación:

[AllRequired]
public class Friend
{
    public string Name { get; set; }
    public string Surname { get; set; }
    public string Nickname { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
    [NotRequired]
    public string Phone { get; set; }
    public string MobilePhone { get; set; }
    public string Email { get; set; }
}

En tiempo de ejecución tendremos, por supuesto, validación tanto en cliente como en servidor; de hecho, el resultado será idéntico al conseguido si hubiéramos decorado cada una de las propiedades anteriores con él atributo Required, puesto que en realidad estamos inyectándolo en todas las propiedades de forma automática, exceptuando en aquellas donde se ha indicado expresamente lo contrario.

Podéis descargar el proyecto de prueba desde Skydrive (VS2010+MVC3).

Publicación original (8-nov-2011): http://www.variablenotfound.com/2011/11/validator-providers-en-aspnet-mvc.html
Tip: ¿todavía no conoces la selección de enlaces interesantes de Variable not found?

Compactación y minimización de Javascript y CSS para ASP.NET

ASP.NET MVCHabía comentado que una de las novedades que ya podíamos disfrutar en la developer preview de MVC 4 era el sistema de compactación y minimización de scripts y CSS, y que, además de ser bastante útil y sencillo, podíamos utilizarlo a día de hoy en nuestras aplicaciones MVC 3 e incluso en ASP.NET Webforms.

Este mecanismo hace posible la creación de bundles, o paquetes de uno o varios archivos de scripts o estilos, que son optimizados en tamaño por el servidor y cargados desde las páginas en una única petición. El proceso de compactación se realizaría en servidor, pero sólo la primera vez, quedando almacenado en caché para las peticiones posteriores.

Por ejemplo, en el caso de los scripts, en lugar de tener en nuestras vistas o layouts referencias hacia decenas de archivos “.js” (jQuery, UI, validadores, plugins, etc.), simplemente incluiríamos la carga de un archivo, que sería generado por el servidor mediante el empaquetado y la compactación de todos ellos.

Y con las hojas de estilo CSS ocurriría lo mismo: podríamos cargar desde el cliente un único archivo .css, que contendría la suma compactada de todos los necesarios para el funcionamiento de la aplicación.

< disclaimer>

Todo lo que voy a contar a continuación está probado con la versión previa para desarrolladores de MVC. El ensamblado Microsoft.Web.Optimization es la versión 0.1, y de él puede cambiar hasta el nombre 😉

< /disclaimer>

Para empezar, el paquete Microsoft.Web.Optimization se incluye instalado de serie en MVC 4 (a día de hoy, hay que decir). En cambio, en ASP.NET MVC 3 o Webforms debemos descargarlo utilizando Nuget:

PM> Install-Package Microsoft.Web.Optimization
Successfully installed 'Microsoft.Web.Optimization 0.1'.
Successfully added 'Microsoft.Web.Optimization 0.1' to Mvc4Application49.

Una vez instalado, ya casi tenemos el trabajo hecho 😉

La forma rápida

Si lo que queremos es probar rápidamente el sistema de empaquetado, basta con añadir el siguiente código en la inicialización de nuestra aplicación:

   protected void Application_Start()
   {
       ... 
       BundleTable.Bundles.EnableDefaultBundles();
       ...

¡Y esto es todo! A partir de este momento, podemos acceder a las URL:

  • /scripts/js, que nos retornará un único archivo script conteniendo todos los scripts disponibles en la carpeta /scripts del proyecto (sin incluir subdirectorios), minimizados y compactados.
  • /content/css, que de la misma forma, retornará un único archivo .css conteniendo la suma compactada de todos los css que el sistema encuentre en la carpeta /content.

Vale, he simplificado un poco para que pudierais comprender rápidamente el ejemplo, pero en realidad es aún más potente:

  • Cualquier petición del estilo “{path}/js” hará que el sistema retorne en un único archivo compactado todos los scripts disponibles en la carpeta {path}.
  • De la misma forma, todas las peticiones a “{path}/css” retornará un único archivo .css con la suma compactada de todas las hojas de estilo presentes en la carpeta {path}.
  • Se eliminan las versiones de documentación (*-vsdoc.js, *.debug.js…), es decir, serán ignoradas todas ellas a la hora de generar el archivo compactado único.
  • Se tiene en cuenta la ordenación, sobre todo a la hora de anexar los scripts en un único archivo. De esta forma, scripts como jQuery se carguen antes que sus plugins, y otros habituales en el mundo CSS como reset.css o normalize.css sean cargados al final del bundle de estilos.

Si queremos hacer utilizar estos archivos minimizados en nuestro sitio web, simplemente tendremos que acudir al layout (o master) y modificar los enlaces hacia estas direcciones. Observad que tendréis que poner un enlace por cada carpeta:

   <link href="/Content/css" rel="stylesheet" type="text/css" />
   <script src="/Scripts/js" type="text/javascript"></script>

Ojo, muy Importante: para que todo funcione bien los scripts deben ser sintácticamente correctos, de lo contrario podemos encontrarnos con que la versión compactada nos dé fallos en tiempo de ejecución. Esto es así porque el proceso de compactación no sólo elimina espacios sobrantes, sino que acorta los nombres de variables y funciones privadas.

De hecho, a día de hoy este sistema no funciona bien con las bibliotecas que se encuentran por defecto en la carpeta /scripts de los proyectos MVC 3, es necesario actualizarlos (usando Nuget son sólo unos segundos). Con los scripts incluidos en MVC 4 no hay problema.

También los .css deben ser correctos, aunque en caso contrario lo único que ocurrirá es que se retornará un único archivo sin compactar.

¡Quiero más control!

El sistema de bundling es realmente potente y flexible. En el ejemplo anterior se trataba de poner en marcha este sistema de forma rápida, pero se puede conseguir un control mucho más exhaustivo sobre lo que se compacta y lo que no, y sobre la forma de acceder a estos recursos.

La siguiente porción de código muestra cómo podríamos inicializar la tabla de bundles de forma más detallada:

   Bundle cssBundle = new Bundle("~/estilo.css", typeof(CssMinify));
   cssBundle.AddDirectory("/content", "*.css", searchSubdirectories: true);
   BundleTable.Bundles.Add(cssBundle);

   Bundle jsBundle = new Bundle("~/scripts.js", typeof (JsMinify));
   jsBundle.AddFile("~/scripts/jquery-1.6.4.js");
   jsBundle.AddFile("~/scripts/modernizr-2.0.6.js");
   jsBundle.AddDirectory("~/scripts/site", "*.js", searchSubdirectories: false);
   BundleTable.Bundles.Add(jsBundle);

Como podemos observar, en primer lugar se crea un bundle de tipo CSS, en el que se añaden todos los archivos con el patrón *.css que se encuentren en la carpeta /content y todos sus subdirectorios. Este paquete compactado sería accesible a través de la URL “/estilo.css”.

En el segundo bloque creamos un bundle en la URL “/script.js” al que añadimos algunos archivos de forma manual, y todos los .js que encontremos en la carpeta /scripts/site, sin incluir subdirectorios.

¿Fácil, verdad?

En conclusión, aunque todavía tendrá que cambiar, en este momento ya podemos ver una herramienta muy potente y sencilla de utilizar, que seguro entrará a formar parte de nuestros desarrollos en breve. Ya el colmo sería que los contenidos se comprimieran en gzip, pero por la pinta que tienen los componentes probablemente sea una funcionalidad a la que podamos aspirar en breve 🙂

Publicación original (18-oct-2011): http://www.variablenotfound.com/2011/10/compactacion-y-minimizacion-de.html

Tip: ¿todavía no conoces la selección de enlaces interesantes de Variable not found?