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.
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:
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
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!!!
ResponderEliminareso también se resuelve colocando
ResponderEliminarPublic 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