ASP.NET MVC–Vigila los nombres de los parámetros de tus acciones

Muy buenas! Un post cortito para contaros un problemilla que nos hemos encontrado en un proyecto ASP.NET MVC5. Aunque seguro que aplica a todas las versiones de MVC desde la 2 al menos.

Es uno de aquellos casos en que, evidentemente hay algo que está mal, pero a simple vista todo parece correcto. Luego das con la causa del error puede que o bien no entiendas el porqué o bien digas “¡ah claro!” dependiendo de si conoces o no como funciona el Model Binder por defecto de MVC.

Reproducción del error

Es muy sencillo. Create una clase llamada Beer tal y como sigue:

  1. public class Beer
  2. {
  3.     public int Id { get; set; }
  4.     public string Name { get; set; }
  5.     public int BeerTypeId { get; set; }
  6. }

En una aplicación real, para editar una cerveza quizá usaríamos un viewmodel que contendría la cerveza que estamos editando y datos adicionales, p. ej. una lista con los tipos válidos para que el usuario pueda seleccionarlos de una combo:

  1. public class BeerViewModel
  2. {
  3.     public Beer Beer { get; set; }
  4.     public SelectList BeerTypes { get; set; }
  5. }

En el controlador rellenaríamos un BeerViewModel y lo mandaríamos para la vista de edición:

  1. public ActionResult Edit(int id)
  2. {
  3.     // Cargaramos la cerveza de la BBDD
  4.     var beer = new Beer() {Id = id, Name = "Beer " + id};
  5.     var model = new BeerViewModel()
  6.     {
  7.         Beer = beer,
  8.         // Cargaramos los tipos de cerveza de algn sitio
  9.         BeerTypes = new SelectList(new[]
  10.         {
  11.             new {Id = 1, Name = "Pilsen"},
  12.             new {Id = 2, Name = "Bock"},
  13.             new {Id = 3, Name = "IPA"}
  14.         }, "Id", "Name")
  15.     };
  16.     return View(model);
  17. }

La vista de edición por su parte se limita a mostrar un formulario para editar el nombre y el tipo de cerveza:

  1. @model WebApplication17.Models.BeerViewModel
  2.            
  3. <h2>Edit a Beer</h2>
  4.  
  5. @using (Html.BeginForm())
  6. {
  7.     @Html.LabelFor(m => m.Beer.Name)
  8.     @Html.TextBoxFor(m => m.Beer.Name)
  9.     <br />
  10.     <p>Choose beer type:
  11.         @Html.DropDownListFor(m => m.Beer.BeerTypeId, Model.BeerTypes)
  12.     </p>
  13.     <input type="submit" value="edit"/>
  14. }

El funcionamiento de la vista es, como era de esperar, correcto:

image

Ahora creamos la acción para recibir los datos de la cerveza y miramos que datos recibimos en el controlador:

image

¡No se ha producido el binding! Los datos que envía el navegador en el POST son correctos (no podía ser de otra forma ya que he usado los helpers para formulario):

image

¿Cuál es la causa del fallo?

Pues que el parámetro de la acción se llama “beer”. Cámbialo para que tenga otro nombre y… voilá:

image

Todos los datos enlazados (excepto el Id vale, al final lo comentamos).

¿Porque no puede mi parámetro llamarse beer?

Porque el ViewModel que estamos usando BeerViewModel tiene una propiedad con ese nombre. De hecho si cambias el nombre de la propiedad del BeerViewModel te funcionará todo de nuevo. Y eso tiene que ver en como funciona el Model Binder. Déjame que te lo cuente de forma simplificada para que tengas clara el porque eso falla.

El Model Binder es el encargado de enlazar los valores de la request con los parámetros del controlador. Los parámetros que recibe el Model Binder de la request (en el form data) son los siguientes:

Beer.Name y Beer.BeerTypeId

Cuando el Model Binder va a enlazar Beer.Name hace lo siguiente:

  • Dado que “Beer.Name” tiene un punto el model binder busca si existe algún parámetro en el controlador llamado “Beer” (case insensitive). Esto es porque un controlador puede tener varios parámetros en la acción.
    • Si lo encuentra entonces buscará una propiedad que se llame Name en dicho parámetro y la enlazará.
    • Si no lo encuentra buscará el primer parámetro posible que tenga una propiedad llamada Beer.
      • Dentro de la propiedad llamada Beer buscará una propiedad llamada Name para enlazarla.

Por eso cuando en la acción el parámetro se llamaba “beer”, el Model Binder al enlazar el parámetro “Beer.Name” intentaba enlazar la propiedad “Name” del propio parámetro. Pero esa propiedad no existe. La clase BeerViewModel solo tiene una propiedad “Beer” y otra “BeerTypes”. Lo mismo ocurre con el parámetro Beer.BeerTypeId (intenta enlazar la propiedad BeerTypeId del propio BeerViewModel).

Al final el Model Binder encuentra que no hay nada que enlazar, así que no hace nada y por eso no recibimos datos.

Cuando hemos cambiado el parámetro del controlador para que se llame “data” entonces el Model Binder al enlazar “Beer.Name” busca un parámetro llamado “beer” en la acción. Pero como NO lo encuentra, entonces busca un parámetro en el controlador que tenga una propiedad llamada “Beer”. Y lo encuentra, porque el parámetro “data” (el BeerViewModel) tiene una propiedad llamada Beer. Luego busca si el tipo de dicha propiedad (la clase Beer) tiene una propiedad llamada Name. Y la encuentra, y la enlaza.

Por eso en el segundo caso recibimos los datos.

Bonus track: ¿Por qué el Id no se enlaza? Esa es sencilla: porque el route value se llama “id” (el POST está hecho a /Beers/Edit/{id}. El Model Binder soporta enlazado de route values, pero el nombre id no lo puede enlazar porque:

  1. No existe ningún parámetro llamado Id en la acción
  2. Ningún parámetro de la acción tiene una propiedad llamada Id.

Espero este post os haya sido interesante y si alguna vez os pasa eso… pues bueno, ya sabéis la razón! 😀

Saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *