lunes, 23 de mayo de 2016

ASP.NET MVC. Crear un ModelBinder personalizado.

Una de las herramientas más útiles de las que disponemos en ASP.NET MVC es el data binding o enlace de datos, que se encarga de transformar de manera automática los datos recibidos del navegador a los tipos de datos usados en los argumentos de las acciones de nuestros controladores.

Sin embargo el proceso de binding que realiza por defecto MVC se nos puede quedar corto cuando utilizamos tipos de datos complejos.

En este artículo voy a mostrar cómo podemos indicarle a MVC cómo debe realizar el enlace de datos con un tipo de datos predeterminado a través de la implementación de un ModelBinder.

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

Códigos de muestra - Ejemplos MSDN. Crear un Model Binder personalizado

En primer lugar trataré de mostrar el problema y a continuación veremos cómo solucionarlo.

Para preparar el escenario voy a crear un nuevo proyecto de aplicación ASP.NET añadiendo las librerías de MVC con nombre CustomModelBinder.

En la carpeta Model creo una nueva clase Person que utilizaré como modelo en el ejemplo:

using System.ComponentModel;

namespace CustomModelBinder.Models
{
    public class Person
    {

        public Person(string name, string surname)
        {
            Name = name;
            Surname = surname;
        }

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

        [DisplayName("Apellido")]
        public string Surname { get; }
    }
}
Imports System.ComponentModel

Public Class Person

    Public Sub New(personName As String, personSurname As String)
        Name = personName
        Surname = personSurname
    End Sub

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

    <DisplayName("Apellido")>
    Public ReadOnly Property Surname As String

End Class

La clase Person es muy simple: tiene dos propiedades de sólo lectura Name y Surname (nombre y apellido), y un constructor que recibe como parámetros los valores de ambas propiedades.

A continuación crearé el controlador Home y la vista Index que utilizaremos. También muy simples.

El controlador tendrá dos acciones Index: una que responde al método GET y que simplemente llama a la vista Index, y otra que responde al método POST que recibe como argumento un objeto del tipo Person y simplemente lo devuelve a la propia vista Index.

using System.Web.Mvc;
using CustomModelBinder.Models;

namespace CustomModelBinder.Controllers
{
    public class HomeController : Controller
    {
        // GET: Homme
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(Person person)
        {
            return View(person);
        }
    }
}
Imports System.Web.Mvc

Namespace Controllers
    Public Class HomeController
        Inherits Controller

        ' GET: Home
        Function Index() As ActionResult
            Return View()
        End Function

        <HttpPost>
        Function Index(person As Person) As ActionResult
            Return View(person)
        End Function

    End Class
End Namespace

En cuanto a la vista Index.cshtml/Index.vbhtml utilizará como no la clase Person como modelo y mostrará un formulario para editar el modelo y, si recibe un objeto Person del servidor (a través de la acción del método POST) lo muestra a continuación, simplemente para mostrar que los datos se han enviado correctamente:

@model CustomModelBinder.Models.Person

@using (Html.BeginForm())
{
    @Html.EditorForModel()
    <br/>
    <input type="submit" value="Validar"/>
}

@if (Model != null)
{
    <p>
        Nombre: @Model.Name <br />
        Apellido: @Model.Surname
    </p>
}
@ModelType CustomModelBinder.Person

@Using (Html.BeginForm())
    @Html.EditorForModel()
    @:<br />
    @:<input type="submit" value="Validar" />
End Using

@If Model IsNot Nothing Then
    @:<p>
        @:Nombre: @Model.Name <br />
        @:Apellido: @Model.Surname
    @:</p>
End If

Ya tenemos el ejemplo preparado. Si arrancamos la aplicación se mostrará e formulario para introducir los datos del objeto Person.

Formulario

Si introducimos valores en los campos e intentamos enviarlos a la correspondiente acción pulsando en el botón Validar comprobaremos que se produce un error en la aplicación:

No hay constructor sin parámetros definido para este objeto.

Descripción: Excepción no controlada al ejecutar la solicitud Web actual. Revise el seguimiento de la pila para obtener más información acerca del error y dónde se originó en el código.

Detalles de la excepción: System.MissingMethodException: No hay constructor sin parámetros definido para este objeto.


¿Qué es lo que ha pasado?

MVC realiza el proceso de enlace de datos a través de la clase DefaultModelBinder. Esta clase, cuando se encuentra un tipo de datos complejo como parámetro de la acción, intenta crear una instancia del objeto utilizando un constructor sin parámetros y a continuación trata de asignar valores a sus propiedades utilizando los valores recibidos del navegador.

Nuestra clase Person no tiene un constructor sin parámetros por lo que se produce el error indicando que no ha encontrado este constructor. De hecho, aunque tuviese un constructor sin parámetros, la acción no recibiría los datos del formulario ya que la clase DefaultModelBinder no podría asignar los valores a las propiedades por tratarse de propiedades de sólo lectura.

¿Qué podemos hacer entonces?

Por supuesto que la solución más sencilla es la de crear un constructor sin parámetros para la clase Person y convertir las propiedades en propiedades de lectura/escritura, pero esto no siempre es posible.

Así que vamos a ver una alternativa: crear nuestro propio ModelBinder para el tipo de datos Person.

Un ModelBinder no es más que una clase que implementa la interfaz IModelBinder. Esta interfaz cuenta con un único método BindModel definido. Este método recibe como parámetros un objeto ControllerContext y otro ModelBindingContext, y debería devolver un objeto del tipo especificado en el ModelBindingContext creado a partir de los valores recibidos (o cualquier otro dato que queramos utilizar en nuestro ModelBinder).

Lo veremos mejor con un ejemplo. Me he creado una nueva carpeta Infrastructure en el proyecto y he incluido en esta carpeta una nueva clase PersonModelBinder que implementa la interfaz IModelBinder. El método BindModel recupera los valores para las propiedades Name y Surname a partir del objeto ModelBindingContext y con ellos crea una nueva instancia de la clase Person.

using System.Web.Mvc;
using CustomModelBinder.Models;

namespace CustomModelBinder.Infrastructure
{
    public class PersonModelBinder: IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var name = bindingContext.ValueProvider.GetValue("Name").AttemptedValue;
            var surname = bindingContext.ValueProvider.GetValue("Surname").AttemptedValue;
            return new Person(name, surname);
        }
    }
}
Imports System.Web.Mvc

Public Class PersonModelBinder
    Implements IModelBinder

    Public Function BindModel(controllerContext As ControllerContext, bindingContext As ModelBindingContext) _
        As Object Implements IModelBinder.BindModel
            Dim name = bindingContext.ValueProvider.GetValue("Name").AttemptedValue
            Dim surname = bindingContext.ValueProvider.GetValue("Surname").AttemptedValue
            Return New Person(name, surname)
    End Function
End Class

Con esto únicamente nos quedaría indicar a MVC que debe utilizar la clase PersonModelBinder cuando quiera enlazar los datos recibidos por el navegador con un parámetro del tipo Person. Esto lo haremos en el método Application_Start del archivo Global.asax.

MVC proporciona una clase estática ModelBinders con una única propiedad Binders que contiene una colección (del tipo ModelBinderDictionary) con todos los ModelBinders registrados en la aplicación.

Podemos añadir un nuevo ModelBinder a través del método Add de esta colección que recibe dos parámetros: el tipo de datos para el que se utilizará el ModelBinder y una instancia del ModelBinder a utilizar:

ModelBinders.Binders.Add(typeof(Person), new PersonModelBinder());
ModelBinders.Binders.Add(GetType(Person), New PersonModelBinder())

Si ejecutamos de nuevo la aplicación e introducimos valores en el formulario veremos que ahora sí se envían correctamente al servidor y son devueltos al navegador:

Formulario procesado

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

Códigos de muestra - Ejemplos MSDN. Crear un Model Binder personalizado

2 comentarios:

  1. Hola Asier muy bueno tu articulo, y explicado de una manera muy sencilla. Te hago una consulta, yo estoy haciendo el mismo ejemplo que el tuyo pero sumándole que envió una List() de objetos a la vista y ademas estoy paginando con PagedList.Mvc, pero justamente me tira error cuando intento pasar de la vista al controlador la lista de objetos y que los reciba mi action httppost, sabrías como puedo resolverlo, desde ya muchas gracias!!!

    ResponderEliminar
  2. eso también se resuelve colocando
    Public Sub New()
    End Sub

    osea

    Public Class HomeController
    Inherits Controller

    Public Sub New()
    End Sub

    ' GET: Home
    Function Index() As ActionResult
    Return View()
    End Function


    Function Index(person As Person) As ActionResult
    Return View(person)
    End Function

    End Class

    ResponderEliminar