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