lunes, 25 de julio de 2016

ASP.NET MVC. ModelBinderProvider para Tipos Genéricos (I). Creando el escenario

Descargar código de ejemplo
Códigos de muestra - Ejemplos MSDN. ModelBinderProvider para tipos genéricos

En un post anterior mostré cómo podíamos crear y registrar un ModelBinder personalizado en una aplicación MVC (ASP.NET MVC. Crear un ModelBinder personalizado). Sin embargo, el tipo de datos que indicamos a MVC a la hora de registrar el ModelBinder debe ser un tipo de datos específico, no podemos indicar un tipo genérico.

Entonces, ¿cómo podemos crear un ModelBinder para un tipo de datos genérico que ASP.NET MVC no es capaz de enlazar por defecto?

Por suerte ASP.NET MVC nos permite también la opción de registrar proveedores de ModelBinders o ModelBinderProviders los cuales se encargan de crear las instancias de ModelBinders para ciertos tipos de datos.

En este artículo mostraré cómo crear y registrar un ModelBinderProvider para el tipo genérico KeyValuePair<TKey, TValue>. Veremos porqué ASP.NET MVC no es capaz de enlazarlo por defecto, cómo crear el ModelBinder para el tipo y cómo podemos indicarle a MVC que debe utilizar nuestro ModelBinder para enlazar estos tipos de datos utilizando un ModelBinderProvider.

Creando el escenario

Para crear el ejemplo empezaremos creando un nuevo proyecto GenericModelBinderProvider ASP.NET vacío añadiendo las librerías de MVC:

Nuevo proyecto ASP.NET MVC

A continuación crearé una clase Item en la carpeta Models que utilizaré como modelo en el ejemplo:


    public class Item
    {

        [DisplayName("Código")]
        public string Code { get; set; }

        [DisplayName("Nombre")]
        public string Name { get; set; }

        [DisplayName("Características")]
        public List<KeyValuePair<string, string>> Characteristics { get; set; }

        [DisplayName("Medidas")]
        public List<KeyValuePair<string, double>> Measures { get; set; }

    }
Public Class Item

    <DisplayName("Código")>
    Public Property Code As String

    <DisplayName("Nombre")>
    Public Property Name As String

    <DisplayName("Características")>
    Public Property Characteristics As List(Of KeyValuePair(Of String, String))

    <DisplayName("Medidas")>
    Public Property Measures As List(Of KeyValuePair(Of String, Double))

End Class

La clase Item tiene dos propiedades de tipo String (Code y Name) que, por tratarse de tipos básicos de .NET, MVC será capaz de enlazar automáticamente, y dos propiedades cuyo valor son listas de objetos KeyValuePair y que, como veremos, MVC no será capaz de enlazar.

Para mantener el ejemplo lo más simple posible voy a crear dos vistas, una que mostrará un formulario web para editar un elemento Item y una segunda que mostrará los datos de un objeto Item.

En primer lugar crearé un controlador HomeController con dos acciones Index. La primera responderá al método GET y creará una instancia vacía de un objeto Item (al que le añadiré un par de elementos para editar en las listas de las propiedades Características y Medidas) que enviará a la vista con el formulario de edición.

La segunda responderá al método POST y recibirá el elemento Item del formulario de edición enviándolo a una segunda vista Result que se encargará de mostrar el resultado en el navegador.


        [HttpGet]
        public ActionResult Index()
        {
            var characterisitics = new List<KeyValuePair<string, string>>
            {
                new KeyValuePair<string, string>("Color", ""),
                new KeyValuePair<string, string>("Tamaño", "")
            };
            var measures = new List<KeyValuePair<string, double>>
            {
                new KeyValuePair<string, double>("Altura", 0),
                new KeyValuePair<string, double>("Ancho", 0)
            };
            var item = new Item
            {
                Characteristics = characterisitics,
                Measures = measures
            };
            return View(item);
        }

        [HttpPost]
        public ActionResult Index(Item item)
        {
            return View("Result", item);
        }
        ' GET: Home
        Function Index() As ActionResult
            Dim characteristics As New List(Of KeyValuePair(Of String, String)) From {
                new KeyValuePair(Of String,String)("Color", ""),
                new KeyValuePair(Of String, String)("Tamaño", "")
                }
            Dim measures As New List(Of KeyValuePair(Of String, Double)) From {
                New KeyValuePair(Of String,Double)("Altura", 0),
                New KeyValuePair(Of String,Double)("Ancho", 0)
                }
            Dim item As New Item With{ 
                .Characteristics= characteristics, 
                .Measures = measures
            }

            Return View(item)
        End Function

        <HttpPost()>
        Function Index(item As Item) As ActionResult
            Return View("Result", item)
        End Function

Las vistas serán igualmente muy simples, ambas utilizarán la clase Item como modelo. La vista Index tendrá un formulario en el que añadiré un editor para cada propiedad de la clase Item con el método de extensión EditorFor, mientras que la vista Result mostrará la información de estas mismas propiedades utilizando el método DisplayFor:


@model GenericModelBinderProvider.Models.Item

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <title>Item</title>
</head>
<body>
<div>
    @using (Html.BeginForm())
    {
        @Html.LabelFor(m => m.Code)
        @:&nbsp;
        @Html.EditorFor(m => m.Code)
        <br/><br/>
        @Html.LabelFor(m => m.Name)
        @:&nbsp;
        @Html.EditorFor(m => m.Name)
        <br/><br/>
        @Html.LabelFor(m => m.Characteristics)
        <br/>
        @Html.EditorFor(m => m.Characteristics)
        <br/><br/>
        @Html.LabelFor(m => m.Measures)
        <br/>
        @Html.EditorFor(m => m.Measures)
        <br/><br/>
        <input type="submit" value="Validar" />
    }
</div>
</body>
</html>
@ModelType GenericModelBinderProvider.Item

@Code
    Layout = Nothing
End Code

<!DOCTYPE html>

<html>
<head>
    <title>Index</title>
</head>
<body>
    <div>
        @Using (Html.BeginForm())
            @Html.LabelFor(Function(m) m.Code)
            @:&nbsp;
            @Html.EditorFor(Function(m) m.Code)
            @:<br /><br />
            @Html.LabelFor(Function(m) m.Name)
            @:&nbsp;
            @Html.EditorFor(Function(m) m.Name)
            @:<br /><br />
            @Html.LabelFor(Function(m) m.Characteristics)
            @:<br />
            @Html.EditorFor(Function(m) m.Characteristics)
            @:<br /><br />
            @Html.LabelFor(Function(m) m.Measures)
            @:<br />
            @Html.EditorFor(Function(m) m.Measures)
            @:<br />
            @:<input type="submit" value="Validar" />
        End Using
    </div>
</body>
</html>
@model GenericModelBinderProvider.Models.Item

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <title>Result</title>
</head>
<body>
<div>
    @Html.LabelFor(m => m.Code)
    &nbsp;
    @Html.DisplayFor(m => m.Code)
    <br /><br />
    @Html.LabelFor(m => m.Name)
    &nbsp;
    @Html.DisplayFor(m => m.Name)
    <br /><br />
    @Html.LabelFor(m => m.Characteristics)
    <br />
    @Html.DisplayFor(m => m.Characteristics)
    <br /><br />
    @Html.LabelFor(m => m.Measures)
    <br />
    @Html.DisplayFor(m => m.Measures)
</div>
</body>
</html>
@ModelType GenericModelBinderProvider.Item

@Code
    Layout = Nothing
End Code

<!DOCTYPE html>

<html>
<head>
    <title>Result</title>
</head>
<body>
    <div>
        @Html.LabelFor(Function(m) m.Code)
        &nbsp;
        @Html.DisplayFor(Function(m) m.Code)
        <br /><br />
        @Html.LabelFor(Function(m) m.Name)
        &nbsp;
        @Html.DisplayFor(Function(m) m.Name)
        <br /><br />
        @Html.LabelFor(Function(m) m.Characteristics)
        <br />
        @Html.DisplayFor(Function(m) m.Characteristics)
        <br /><br />
        @Html.LabelFor(Function(m) m.Measures)
        <br />
        @Html.DisplayFor(Function(m) m.Measures)
    </div>
</body>
</html>

Ya tenemos el ejemplo casi listo. Si arrancamos el sitio web veremos que se muestra el formulario para editar el elemento Item:

Formulario sin editor para Características y Medidas



Sin embargo podemos ver que no se han generado controles de edición para los elementos de Características y Medidas. Esto es porque MVC no tiene un editor predeterminado para los elementos KeyValuePair. Para solucionarlo crearé dos vistas parciales (Characteristics y Measures) que utilizaré como editores de las propiedades. Las vistas las crearé en una carpeta EditorTemplates dentro de la carpeta del proyecto Views/Home.


@model List<KeyValuePair<string, string>>

@for (int i = 0; i < Model.Count; i++)
{
    @Html.EditorFor(m => m[i].Key, new { htmlAttributes= new { readOnly = true } })
    @Html.EditorFor(m => m[i].Value)
    <br />
}
@ModelType List(Of KeyValuePair(Of String, String))

@For i = 0 To Model.Count - 1
    @Html.EditorFor(Function(m) m(i).Key, New With {.htmlAttributes = New With {.readOnly = True}})
    @Html.EditorFor(Function(m) m(i).Value)
    @:<br />
Next
@model List<KeyValuePair<string, double>>

@for (int i = 0; i < Model.Count; i++)
{
    @Html.EditorFor(m => m[i].Key, new { htmlAttributes= new { readOnly = true } })
    @Html.EditorFor(m => m[i].Value)
    <br />
}
@ModelType List(Of KeyValuePair(Of String, Double))

@For i = 0 To Model.Count - 1
    @Html.EditorFor(Function(m) m(i).Key, New With {.htmlAttributes = New With {.readOnly = True}})
    @Html.EditorFor(Function(m) m(i).Value)
    @:<br />
Next

Los editores simplemente crean controles de edición para las propiedades Key y Value de cada elemento, permitiendo editar únicamente el de la propiedad Value.

Para indicarle a MVC que debe utilizar estos editores para las propiedades Characteristics y Measures utilizaremos el atributo UiHintAttribute en la definición de la clase Item:


        [DisplayName("Características")]
        [UIHint("Characteristics")]
        public List<KeyValuePair<string, string>> Characteristics { get; set; }

        [DisplayName("Medidas")]
        [UIHint("Measures")]
        public List<KeyValuePair<string, double>> Measures { get; set; }
    <DisplayName("Características")>
    <UiHint("Characteristics")>
    Public Property Characteristics As List(Of KeyValuePair(Of String, String))

    <DisplayName("Medidas")>
    <UIHint("Measures")>
    Public Property Measures As List(Of KeyValuePair(Of String, Double))

Si volvemos a arrancar la aplicación veremos que ya podemos editar los elementos de estas propiedades:

Formulario de edición

Sin embargo su pulsamos el botón "Validar" para enviar el contenido del formulario al controlador veremos que en la vista que muestra el resultado no se muestran los valores de estos elementos:

Resultado sin valores
El motivo es que MVC  no es capaz de realizar el enlace de datos de los elementos KeyValuePair. En el siguiente artículo veremos cómo podemos solucionar esta situación.

Artículo siguiente:
ASP.NET MVC. ModelBinderProvider para Tipos Genéricos (y II). KeyValuePair DataBinding

El código completo tanto en C# como en Visual Basic .NET está disponible en:

Códigos de muestra - Ejemplos MSDN. ModelBinderProvider para tipos genéricos


No hay comentarios:

Publicar un comentario