sábado, 5 de diciembre de 2015

ASP.NET MVC. htmlAttributes en los métodos Editor y EditorFor

En muchos de los métodos de extensión de la clase HtmlHelper de ASP.NET MVC podemos encontrar un parámetro htmlAttributes que nos permite indicar valores a atributos HTML adicionales a incluir en el código HTML generado por el método.

Estos parámetros suelen aceptar un objeto IDictionary<string,object> en los que la clave de cada elemento representa el nombre del atributo y el valor del elemento representa el valor del atributo, o un objeto en el que el nombre de sus propiedades se corresponden con los nombres de los atributos y el valor de éstas con el valor de los atributos.

Sin embargo hay dos métodos de gran uso, y en los que resultaría extremadamente útil, en los que no se incluye en ninguna de sus sobrecargas: los métodos Editor y EditorFor.

Efectivamente, si hacemos una búsqueda en el código fuente de ASP.NET MVC podemos encontrar multitud de métodos de extensión para la clase HtmlHelper que admiten este parámetro htmlAttributes:

  public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name, bool isChecked, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, bool>> expression, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString Hidden(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString HiddenFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString Password(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString PasswordFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value, bool isChecked, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString RadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object value, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, string format, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string format, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString Label(this HtmlHelper html, string expression, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString Label(this HtmlHelper html, string expression, string labelText, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString LabelForModel(this HtmlHelper html, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString LabelForModel(this HtmlHelper html, string labelText, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString ListBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, string value, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, string value, int rows, int columns, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString TextAreaFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString TextAreaFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, int rows, int columns, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, string validationMessage, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString ValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string validationMessage, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string message, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string message, IDictionary<string, object> htmlAttributes, string headingTag)
  public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, bool excludePropertyErrors, string message, IDictionary<string, object> htmlAttributes)
  public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, bool excludePropertyErrors, string message, IDictionary<string, object> htmlAttributes, string headingTag)

Sin embargo, si examinamos los métodos Editor y EditorFor encontraremos que ninguna de sus sobrecargas tienen un parámetro htmlAttributes.

  public static MvcHtmlString Editor(this HtmlHelper html, string expression)
  public static MvcHtmlString Editor(this HtmlHelper html, string expression, object additionalViewData)
  public static MvcHtmlString Editor(this HtmlHelper html, string expression, string templateName)
  public static MvcHtmlString Editor(this HtmlHelper html, string expression, string templateName, object additionalViewData)
  public static MvcHtmlString Editor(this HtmlHelper html, string expression, string templateName, string htmlFieldName)
  public static MvcHtmlString Editor(this HtmlHelper html, string expression, string templateName, string htmlFieldName, object additionalViewData)
  public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
  public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object additionalViewData)
  public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName)
  public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, object additionalViewData)
  public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, string htmlFieldName)
  public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, string htmlFieldName, object additionalViewData)

Por suerte el equipo de desarrollo de ASP.NET MVC se dio cuenta de que podía ser muy útil poder añadir atributos HTML desde estos métodos y, a partir de la versión 5, incorporaron esta posibilidad. La forma elegida es la de mirar si existe una clave htmlAttributes en el ViewData y, si es así, incorporar los atributos al código generado.

De esta forma podríamos añadir un atributo HTML a través del parámetro additionalViewData:

@Html.EditorFor(m => m.Foo, new { htmlAttributes = new { attributeKey = atributeValue } })
@Html.EditorFor(Function(m) m.Foo, New With {.htmlAttributes = New With {.attributeKey = value}})
De esta forma los editores por defecto de ASP.NET MVC reconocerán la clave del ViewData e incorporarán los atributos al código HTML generado.

Sin embargo, si tenemos editores personalizados, deberemos implementar en ellos esta nueva funcionalidad: comprobar si existe una clave htmlAttributes en el ViewData y, de ser así, incorporarlos al código HTML.

Supongamos que tenemos definido un editor para fechas como el del artículo ASP.NET MVC. Plantilla de editor para fecha y hora.

@model DateTime

<script>
    $(function () {
        $(".datepicker").datetimepicker({
            lang: 'es',
            format: 'd/m/Y',
            formatDate: 'd/m/Y',
            dayOfWeekStart: 1,
            mask: true,
            timepicker: false
        });
    });
</script>

@Html.TextBox("", (Model == DateTime.MinValue ? null : Model.ToString("dd/MM/yyyy"))
    , new { @class = "datepicker" })
@ModelType DateTime

<script>
    $(function () {
        $(".datepicker").datetimepicker({
            lang: 'es',
            format: 'd/m/Y',
            formatDate: 'd/m/Y',
            dayOfWeekStart: 1,
            mask: true,
            timepicker: false
        });
    });
</script>

@Html.TextBox("", IIf(Model = DateTime.MinValue, Nothing, Model.ToString("dd/MM/yyyy")) _
                   , New With {.class = "datepicker"})

Podríamos modificarlo para que tome los atributos provenientes del ViewData.

@{
    var htmlAttributes = ViewData["htmlAttributes"] ?? new { };
}
@Html.TextBox("", (Model == DateTime.MinValue ? null : Model.ToString("dd/MM/yyyy"))
    , htmlAttributes)
@Code
    Dim htmlAttributes = If(ViewData("htmlAttributes"), New Object())
End Code

@Html.TextBox("", IIf(Model = DateTime.MinValue, Nothing, Model.ToString("dd/MM/yyyy")) _
                               , htmlAttributes)

Sin embargo de esta forma perderíamos el atributo class que estábamos añadiendo en el editor.




Así que deberemos implementar un método para poder añadir los atributos HTML recibidos a través del ViewData a los definidos en la plantilla.

Para implementar el método deberemos tener en cuenta algunos puntos:

  • El objeto recibido a través del ViewData puede ser un objeto anónimo o un objeto IDictionary<string, object>.
  • Si un atributo está definido tanto en el editor como en el ViewData debería prevalecer el recibido a través del ViewData
  • El atributo class debe tener un tratamiento especial: si se indican dos valores diferentes deberían incluirse ambos como valor del atributo separados por un espacio.


@functions{

        public static IDictionary<string, object> MergeHtmlAttributes(object htmlAttributes, object editorAttributes)
        {
            var editorAttributesDict = editorAttributes as IDictionary<string, object>;
            var htmlAttributesDict = htmlAttributes as IDictionary<string, object>;

            RouteValueDictionary defaultAttributes = (editorAttributesDict != null)
                ? new RouteValueDictionary(editorAttributesDict)
                : HtmlHelper.AnonymousObjectToHtmlAttributes(editorAttributes);
            RouteValueDictionary viewDataAttributes = (htmlAttributesDict != null)
                ? new RouteValueDictionary(htmlAttributesDict)
                : HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);

            foreach (var item in viewDataAttributes)
            {
                if (item.Key != "class" || defaultAttributes[item.Key] == null)
                {
                    defaultAttributes[item.Key] = item.Value;
                }
                else
                {
                    defaultAttributes[item.Key] = string.Format("{0} {1}", defaultAttributes[item.Key], item.Value);
                }
            }

            return defaultAttributes;
        }

}

@{
    var defaultAttributes = new { @class = "datepicker" };
    var viewDataAttributes = ViewData["htmlAttributes"] ?? new { };
    var htmlAttributes = MergeHtmlAttributes(viewDataAttributes, defaultAttributes);
}
@Html.TextBox("", (Model == DateTime.MinValue ? null : Model.ToString("dd/MM/yyyy"))
    , htmlAttributes)
@Functions
    Public Function MergeHtmlAttributes(htmlAttributes As Object, editorAttributes As Object) _
        As IDictionary(Of String, Object)
        Dim editorAttributesDict = TryCast(editorAttributes, IDictionary(Of String, Object))
        Dim htmlAttributesDict = TryCast(htmlAttributes, IDictionary(Of String, Object))

        Dim defaultAttributes As RouteValueDictionary = IIf(editorAttributesDict Is Nothing _
            , HtmlHelper.AnonymousObjectToHtmlAttributes(editorAttributes) _
            , New RouteValueDictionary(editorAttributesDict))
        Dim viewDataAttributes As RouteValueDictionary = IIf(htmlAttributes Is Nothing _
            , HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes) _
            , New RouteValueDictionary(htmlAttributesDict))

        For Each item In viewDataAttributes
            If item.Key <> "class" OrElse defaultAttributes(item.Key) Is Nothing Then
                defaultAttributes(item.Key) = item.Value
            Else
                defaultAttributes(item.Key) = String.Format("{0} {1}", defaultAttributes(item.Key), item.Value)
            End If
        Next

        Return defaultAttributes

    End Function

End Functions

@Code
    Dim defaultAttributes = New With {.class = "datepicker"}
    Dim viewDataAttributes = If(ViewData("htmlAttributes"), New Object())
    Dim htmlAttributes = MergeHtmlAttributes(viewDataAttributes, defaultAttributes)
End Code

@Html.TextBox("", IIf(Model = DateTime.MinValue, Nothing, Model.ToString("dd/MM/yyyy")) _
                                                                 , htmlAttributes)

Sin querer abrir el debate sobre si es buena idea o no incluir funciones y métodos en el código de las vistas, en este caso concreto: una función que necesitaremos reutilizar en todos los editores, decididamente no es buena idea.

Así que lo que voy a hacer es crear un método de extensión para el objeto HtmlHelper que podamos llamar desde cualquier editor.

    public static class HtmlHelperExtensions
    {

        public static IDictionary<string, object> MergeHtmlAttributes(this HtmlHelper helper, object htmlAttributes, object editorAttributes)
        {
            var editorAttributesDict = editorAttributes as IDictionary<string, object>;
            var htmlAttributesDict = htmlAttributes as IDictionary<string, object>;

            RouteValueDictionary defaultAttributes = (editorAttributesDict != null)
                ? new RouteValueDictionary(editorAttributesDict)
                : HtmlHelper.AnonymousObjectToHtmlAttributes(editorAttributes);
            RouteValueDictionary viewDataAttributes = (htmlAttributesDict != null)
                ? new RouteValueDictionary(htmlAttributesDict)
                : HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);

            foreach (var item in viewDataAttributes)
            {
                if (item.Key != "class" || defaultAttributes[item.Key] == null)
                {
                    defaultAttributes[item.Key] = item.Value;
                }
                else
                {
                    defaultAttributes[item.Key] = string.Format("{0} {1}", defaultAttributes[item.Key], item.Value);
                }
            }

            return defaultAttributes;
        }

    }
Module HtmlHelperExtensions

    <Extension>
    Public Function MergeHtmlAttributes(helper As HtmlHelper, htmlAttributes As Object, editorAttributes As Object) _
        As IDictionary(Of String, Object)
        Dim editorAttributesDict = TryCast(editorAttributes, IDictionary(Of String, Object))
        Dim htmlAttributesDict = TryCast(htmlAttributes, IDictionary(Of String, Object))

        Dim defaultAttributes As RouteValueDictionary = IIf(editorAttributesDict Is Nothing _
            , HtmlHelper.AnonymousObjectToHtmlAttributes(editorAttributes) _
            , New RouteValueDictionary(editorAttributesDict))
        Dim viewDataAttributes As RouteValueDictionary = IIf(htmlAttributes Is Nothing _
            , HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes) _
            , New RouteValueDictionary(htmlAttributesDict))

        For Each item In viewDataAttributes
            If item.Key <> "class" OrElse defaultAttributes(item.Key) Is Nothing Then
                defaultAttributes(item.Key) = item.Value
            Else
                defaultAttributes(item.Key) = String.Format("{0} {1}", defaultAttributes(item.Key), item.Value)
            End If
        Next

        Return defaultAttributes

    End Function

End Module

De esta forma el código de la plantilla quedaría:

@{
    var defaultAttributes = new { @class = "datepicker" };
    var viewDataAttributes = ViewData["htmlAttributes"] ?? new { };
    var htmlAttributes = Html.MergeHtmlAttributes(viewDataAttributes, defaultAttributes);
}
@Html.TextBox("", (Model == DateTime.MinValue ? null : Model.ToString("dd/MM/yyyy"))
    , htmlAttributes)
@Code
    Dim defaultAttributes = New With {.class = "datepicker"}
    Dim viewDataAttributes = If(ViewData("htmlAttributes"), New Object())
    Dim htmlAttributes = Html.MergeHtmlAttributes(viewDataAttributes, defaultAttributes)
End Code

@Html.TextBox("", IIf(Model = DateTime.MinValue, Nothing, Model.ToString("dd/MM/yyyy")) _
                                                                 , htmlAttributes)



No hay comentarios:

Publicar un comentario