Angular y React (1/n): Empezando

¡Buenas! Empiezo con esta una serie de posts, que no se lo larga que será, comparando (en el buen sentido, nada de buscar un ganador ni un perdedor) un poco Angular con React.

Antes que nada el típico disclaimer: Angular y React no cubren los mismos aspectos del desarrollo web. Sí Angular cubre todo el espectro MVC, MVVM o como quieras llamarlo, React cubre solo la V: las vistas. Es pues, de funcionalidad más limitada que Angular. Así React puede combinarse con otras librerías para obtener un framework MVC completo. Hay quien lo ha hecho con Backbone (lógico, Backbone se puede combinar con cualquier cosa) pero lo habitual es hacerlo con Flux. Pero bueno… hasta hay quien lo ha hecho con… ¡Angular!

Si no conoces nada de Angular, tranquilo que durante esta serie de posts, iremos explicando lo que sea necesario y lo mismo aplica a React. De todos modos para Angular Xavier Jorge Cerdá y Pedro Hurtado están escribiendo un tutorial en Louesfera. Échale un vistazo. De React cuesta mucho más encontrar documentación en formato tutorial en castellano…

Hello world

Por supuesto, vamos a empezar con el Hello World, un ejemplo que generalmente es tan soso que no dice nada sobre lo que se quiere analizar, pero bueno… las tradiciones son tradiciones.

Ahí va el Hello World de Angular:

  1. <!DOCTYPE html>
  2. <html ng-app="hello-app">
  3. <head lang="en">
  4.     <meta charset="UTF-8">
  5.     <title>Hello Angular</title>
  6.     <script src="bower_components/angular/angular.js"></script>
  7.     <script>
  8.           angular.module('hello-app', []).controller('HelloController',function HelloController($scope) {
  9.             $scope.name = "eiximenis";
  10.         });
  11.     </script>
  12. </head>
  13. <body>
  14.     <div ng-controller="HelloController">
  15.         Hello {{name}}
  16.     </div>
  17. </body>
  18. </html>

Al ejecutar esta página se mostrará “Hello eiximenis” por pantalla.

Es un código muy simple pero sirve para ver tres de las características fundamentales de Angular:

  1. En Angular las vistas son HTML. Es decir están formadas por el propio DOM de la página. Puede parecer obvio (HTML va muy bien para definir aspecto visual) pero bueno, hay otras librerías (p. ej. Backbone) que usan código para definir las vistas.
  2. Hay un sistema de bindings para transferir datos desde el controlador (HelloController) hacia la vista (DOM). En este caso hemos usado la sintaxis más simple, al estilo mustache.
  3. El sistema de inyección de dependencias que tiene Angular (el parámetro $scope) está inyectado automáticamente por Angular. La idea es que se pueden inyectar aquellos servicios que se desee a los controladores.

Veamos el código equivalente en React:

  1. <!DOCTYPE html>
  2. <html>
  3. <head lang="en">
  4.     <meta charset="UTF-8">
  5.     <title>Hello react</title>
  6.     <script src="bower_components/react/react.js"></script>
  7.     <script src="bower_components/react/JSXTransformer.js"></script>
  8. </head>
  9. <body id="example">
  10.     <script type="text/jsx">
  11.         /** @jsx React.DOM */
  12.         React.renderComponent(
  13.         <div>Hello, eiximenis!</div>,
  14.         document.getElementById('example')
  15.         );
  16.     </script>
  17. </body>
  18. </html>

El ejemplo es muy simple, pero luce más complejo que el equivalente de Angular. Y eso es debido a la filosofía de React:

  • React se basa en el uso de pequeños componentes que cada uno de ellos se puede renderizar independientemente (algo parecido a lo que proponen librerías como Polymer, pero con la diferencia de que Polymer se basa en Web Components y extiende el DOM, mientras que React se basa en JavaScript y se olvida del DOM).
  • Las vistas no son HTML (DOM), si no clases JavaScript. La idea es la misma que las vistas de Backbone, con la salvedad de que en Backbone se suele interaccionar con el DOM (usualmente a través de jQuery) y en React no.
  • React “extiende” JavaScript para permitir esta mezcla de html y JavaScript. Para ello se usa un parser específico, que está en el fichero JSXTransformer.js
  • No hay binding en React, porque no hay DOM hacia el que enlazar nada.
  • React tiene el concepto de “DOM Virtual”: no se interacciona nunca con el DOM real, si no con un “DOM Virtual” proporcionado por React. Luego es React quien se encarga de sincronizar este “DOM Virtual” con el DOM real del navegador. Eso es un “epic win” para SEO en SPA: react puede renderizar vistas sin necesidad de un navegador… se puede renderizar una vista React desde node p. ej. usando el mismo código en el servidor que en el cliente, y permitiendo así que los buscadores indexen nuestro sitio. Esto no es posible en Angular, ya que Angular está atado al DOM y el DOM requiere un navegador. Ojo, no digo que con Angular no se pueda generar aplicaciones SPA con buen soporte para SEO, solo digo que con React el mismo código sirve para renderizar en servidor y en cliente, mientras que con Angular la parte del servidor requiere código adicional. Además, teóricamente, este “DOM Virtual” permitiría a React generar otras cosas que no sean vistas en HTML… quien sabe.

Bueno… y para ser el primer post lo dejaremos aquí…

ASP.NET MVC–Traduciendo las validaciones de CompareAttribute

Muy buenas! Seguimos ese tour de force sobre las traducciones de los mensajes de validación de Data Annotations.

En el primer post de esta serie vimos como crear adaptadores de atributos para permitirnos fácilmente y a nivel centralizado establecer las propiedades ErrorMessageResourceName y ErrorMessageResourceType.

El post terminaba con una lista de los distintos adaptadores que tiene ASP.NET MVC y que podíamos usar como clases base. Hay adaptadores definidos para casi todos los atributos de Data Annotations (Required, StringLength,…) pero no hay ninguno para el CompareAttribute. El atributo Compare no se usa mucho, ya que valida que dos propiedades tengan el mismo valor. El clásico uso es en formularios de registro donde el usuario debe introducir una contraseña dos veces para evitar que haya ningún error.

Pero el hecho de que ASP.NET MVC no incluya ningún adaptador base para dicho atributo no nos impide crearnos el nuestro y aplicar la misma técnica para traducir los mensajes de validación de dicho atributo. Para ello derivaremos de la clase DataAnnotationsModelValidator<T> (siendo T el tipo del atributo, en este caso el CompareAttribute).

La principal diferencia es que ahora debemos generar las validaciones de cliente, sobreescribiendo el método GetClientValidationRules().

Pese a todo, el código sigue siendo muy sencillo:

  1. public class LocalizedCompareAdapter : DataAnnotationsModelValidator<System.ComponentModel.DataAnnotations.CompareAttribute>
  2. {
  3.     public LocalizedCompareAdapter(ModelMetadata metadata, ControllerContext context, CompareAttribute attribute)
  4.         : base(metadata, context, attribute)
  5.     {
  6.         attribute.ErrorMessageResourceName = "Compare";
  7.         attribute.ErrorMessageResourceType = typeof(Resources.Messages);
  8.     }
  9.  
  10.     public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
  11.     {
  12.         var other = Attribute.OtherProperty;
  13.         return new[] { new ModelClientValidationEqualToRule(base.ErrorMessage, other) };
  14.     }
  15. }

En el método GetClientValidationRules devolvemos las validaciones de cliente. En este caso, queremos comparar dos propiedades así que devolvemos una ModelClientValidationEqualToRule, a la cual le pasamos el nombre de la otra propiedad. Recuerda que el [Compare] se aplica a una  propiedad (p. ej. ConfirmPassword) y se coloca el nombre de la otra propiedad (p. ej. Password):

  1. public string Password { get; set; }
  2. [Compare("Password")]
  3. public string ConfirmPassword { get; set; }

En este caso es la propiedad ConfirmPassword la que está decorada con el CompareAttribute, por lo tanto esta es la contendrá la regla de validación en cliente. De hecho el código HTML generado por los helpers Html.TextBoxFor para esas dos propiedades es:

  1. <input id="Password" name="Password" type="text" value="" />
  2. <input data-val="true" data-val-equalto="Los valores de ConfirmPassword y Password deben ser iguales"
  3.        data-val-equalto-other="Password" id="ConfirmPassword" name="ConfirmPassword" type="text" value="" />

Se puede ver que el <input /> que se corresponde a Password no tiene validaciones aplicadas y que es el <input /> que corresponde a ConfirmPassword el que tiene las validaciones de cliente aplicadas.

Podemos ver que el mensaje de error (data-val-equalto) está en castellano porque en mi fichero de recursos (Messages.es.resx) tengo la entrada “Compare”que es la que usa nuestro adaptador de recursos:

image

¡Y listos! Hemos visto como el hecho de que ASP.NET MVC  no provea un adaptador base no nos impide usar DataAnnotationsModelValidator<T> para crearnos nuestro propio adaptador, con la salvedad de que debemos indicar las validaciones de cliente a generar (las de servidor no son necesarias).

PD: Por supuesto debemos registrar ese adaptador de atributo como vimos en el post dedicado a los adaptadores:

  1. DataAnnotationsModelValidatorProvider.RegisterAdapter(
  2.     typeof (System.ComponentModel.DataAnnotations.CompareAttribute),
  3.     typeof (LocalizedCompareAdapter));

Un saludo!

ASP.NET MVC–Traduciendo las validaciones implícitas

En el post anterior vimos como localizar los mensajes de validación de Data Annotations en ASP.NET MVC usando adaptadores de atributos. Pero además de esos mensajes ASP.NET MVC tiene algunos mensajes de traducción implícitos.

P. ej. si tenemos una propiedad de tipo Int y le intentamos poner un valor no numérico ASP.NET MVC mostrará un mensaje de error. Este mensaje de error no proviene de Data Annotations, por lo que no podemos usar la técnica descrita en el post anterior para traducirlo.

Vamos a ver como traducir los mensajes implícitos. Para ello basta con crear un fichero de recursos (en App_GlobalResources) con la entrada PropertyValueInvalid y MVC usará dicho mensaje para mostrar el error. Puedes usar {0} para mostrar el valor inválido y {1} para mostrar el nombre de la propiedad.

La entrada PropertyValueInvalid la usa el model binder cuando se encuentra un valor inválido para una propiedad. P. ej. si tenemos una propiedad definida como int e intentamos asignarle una cadena.

Debemos configurar el model binder para que use el fichero de recursos que hemos creado usando la propiedad ResourceClassKey:

  1. DefaultModelBinder.ResourceClassKey = "Messages";

El valor de ResourceClassKey es el nombre del fichero de recursos a usar.

Traducir el mensaje implícito de propiedad requerida

El atributo [Required] de Data Annotations nos permite especificar que el valor de una propiedad es obligatorio. Pero MVC trata automáticamente algunos campos como obligatorios: las propiedades cuyo tipo es un tipo por valor son tratadas como obligatorias automáticamente. Eso es lógico: si tengo una propiedad declarada como int, es obvio que debe tener siempre un valor, ya que int no admite el valor null.

Pero el tratamiento exacto que se da en esos casos depende del valor de la propiedad AddImplicitRequiredAttributeForValueTypes de la clase DataAnnotationsModelValidatorProvider:

  • Si vale true (valor por defecto) se añade automáticamente un atributo [Required] a cada propiedad cuyo tipo sea un tipo por valor.
  • Si vale false el tratamiento lo realiza el default model binder.

Si estamos en el primer caso, eso significa que dicho mensaje realmente tenemos que tratarlo a través de un adaptador de atributo porque, a todos los efectos, es como si la propiedad tuviese un atributo [Required] colocado.  Así nos podríamos crear un adaptador para el atributo Required, tal y como se comentaba en el post anterior.

Si estamos en el segundo caso, entonces es el model binder quien tratará ese caso. Para mostrar el mensaje (que por defecto es un insulso “A value is required”) usará la entrada PropertyValueRequired del fichero de recursos que hayamos especificado mediante la propiedad ResourceClassKey.

Así vemos que el DefaultModelBinder usa dos claves del fichero de recursos:

  • PropertyValueInvalid: Cuando se asigna un valor incorrecto a una propiedad
  • PropertyValueRequired: Cuando no se asigna valor a una propiedad cuyo tipo es un tipo por valor (siempre y cuando AddImplicitRequiredAttributeForValueTypes valga false).

Nota: Esas dos son las dos únicas claves que usa el Default Model Binder.

Validación en cliente

Si tienes habilitada la validación en cliente, las cosas se ponen un poco más interesante. Hasta ahora hemos visto como traducir los mensajes implícitos que usa el Default Model Binder, pero la validación en cliente es independiente del Model Binder y es necesario un paso más.

Si la tienes habilitada verás que no te funciona… Aparecen otros mensajes de errores. P. ej. en el caso de introducir un valor no numérico en una propiedad numérica (un int p. ej.) ahora aparece un mensaje “The field xxx must be a number”.

Mirando el código fuente de la página puedes ver que este mensaje es de la validación en cliente:

image

¿Quien ha generado este mensaje y de donde lo saca?

Pues bien, estos mensajes de errores los genera la clase ClientDataTypeModelValidatorProvider que es la encargada de gestionar las validaciones en cliente.

Por suerte dicha clase expone también una propiedad ResourceCssKey, al igual que el DefaultModelBinder que podemos usar para especificarle un fichero de recursos:

  1. ClientDataTypeModelValidatorProvider.ResourceClassKey = "Messages";

Ahora la pregunta a responder es: ¿qué claves espera encontrar en este fichero de recursos?

Pues las siguientes:

  • FieldMustBeNumeric: En el caso que se entre una cadena en campos que deban ser numéricos
  • FieldMustBeDate: En el caso de que se entre una cadena incorrecta en campos que deban ser fechas.

Si te preguntas “qué es un campo numérico”, pues cualquiera cuyo tipo en la propiedad .NET equivalente sea: byte, sbyte, short, ushort, int, uint, long, ulong, float, double y decimal o las versiones Nullable de esos tipos.

Un campo fecha es aquel cuya propiedad se haya declarado como DateTime (o DateTime?) (y que no tenga aplicado el atributo [DataType] con el valor “Time”).

Por lo tanto nos basta con agregar esas dos recursos a nuestro fichero de recursos para poder traducir los mensajes implícitos.

Vale. Un apunte final: si te fijas en el código HTML para el <input /> de la propiedad Age, verás que no ha generado el data-val-required. Eso es porque tenia la línea:

  1. DataAnnotationsModelValidatorProvider.
  2.     AddImplicitRequiredAttributeForValueTypes = false;

Recuerda que eso hace que MVC no añada automáticamente un [Required]. La validación en cliente no entiende de campos requeridos si no hay un [Required]. Es por ello que no se genera el código en cliente para asegurarse que deba entrar un valor en este campo. Si elimino esa línea y ejecuto de nuevo ahora si que me aparece la validación de campo obligatorio:

image

Si te preguntas ahora de donde sale el mensaje usado para la validación de campo obligatorio en el cliente, la respuesta es que del atributo [Required] que MVC ha añadido automáticamente a la propiedad.

Por lo tanto, si usas un adaptador de atributo para el [Required] este se aplicará (y dado que se aplica también en servidor verás el mismo mensaje en el cliente que en el servidor).

En resumen…

Resumiendo, los mensajes implícitos de MVC son generados por:

  1. El DefaultModelBinder en el servidor
  2. El ClientDataTypeModelValidatorProvider en la validación en cliente

Ambos usan una propiedad (ResourceCssKey) para especificar el fichero de recursos a utilizar. Y dentro de ese fichero de recursos podemos colocar las claves:

  1. PropertyValueInvalid: Cuando se asigna un valor inválido a una propiedad. Usado por el DefaultModelBinder
  2. PropertyValueRequired: Cuando no se ha informado el valor de una propiedad que es obligatoria porque su tipo es un tipo por valor (usado por el DefaultModelBinder solo si AddImplicitRequiredAttributeForValueTypes  es false).
  3. FieldMustBeNumeric: Cuando se introduce un valor no numérico en una propiedad numérica (usada por ClientDataTypeModelValidatorProvider. El equivalente en servidor sería PropertyValueInvalid).
  4. FieldMustBeDate: Cuando se introduce un valor que no es una fecha en una propiedad de tipo fecha (usada por ClientDataTypeModelValidatorProvider. El equivalente en servidor sería PropertyValueInvalid).

Espero que te haya sido útil! En otro post iremos un paso más allá y veremos como personalizar al máximo esos mensajes de error. Pero ya te avanzo que nos meteremos hasta la cocina y el baño de ASP.NET MVC.

Saludos!

ASP.NET MVC–Traducir los mensajes de error de DataAnnotations… otra vez.

Pues sí… la verdad es que esa es una cuestión recurrente en ASP.NET MVC. Y es que con las distintas versiones de MVC han aparecido distintas maneras de conseguir este propósito.

Nota 1: Para tener una idea de como eran las cosas en MVC2 echad un vistazo al post que publicó el Maestro hace tiempo: Modificar los mensajes de validación por defecto en ASP.NET MVC 2. Por favor léete dicho post, pues en cierto modo mi post es una “continuación”.

Nota 2: Una opción rápida es instalar paquetes de idioma de MVC. Esos paquetes vienen con los mensajes ya traducidos en varios idiomas. Podemos instalar tantos paquetes de idiomas como necesitemos y dependiendo de la cultura en el hilo del servidor se usará uno u otro. Eso nos permite tener los mensajes traducidos (aunque no podremos modificarlos, son los que son). De nuevo el Maestro publicó sobre ello: Errores de ASP.NET MVC 4 en distintos idiomas

El post de José María explicar muy bien como era la situación en MVC2. Pero en MVC3 y sobretodo en MVC4 hubieron algunos cambios significativos que voy a comentar en este post.

Por supuesto podemos seguir usando la propiedad ErrorMessage de los atributos de Data Annotations. Pero eso sigue sin ser multi-idioma y además es muy pesado. Otra opción que sigue siendo válida y de hecho es la que se sigue (indirectamente) usando son las propiedades ErrorMessageResourceName y ErrorMessageResourceType.

Herencia de atributos

Jose María menciona en su post la posibilidad de usar esas propiedades a cada atributo de DataAnnotations (lo que no es muy DRY) o bien crearse un atributo de DataAnnotations derivado que auto-asigne dichas propiedades. Es decir hacer algo como lo siguiente:

  1. public class LocalizedRangeAttribute : RangeAttribute
  2. {
  3.  
  4.     public LocalizedRangeAttribute(int min, int max) : base(min, max)
  5.     {
  6.         InitProps();
  7.     }
  8.  
  9.     public LocalizedRangeAttribute(double min, double max) : base(min, max)
  10.     {
  11.         InitProps();
  12.     }
  13.  
  14.     public LocalizedRangeAttribute(Type type, string min, string max) : base(type, min, max)
  15.     {
  16.         InitProps();
  17.     }
  18.  
  19.     private void InitProps()
  20.     {
  21.         ErrorMessageResourceName = "Range";
  22.         ErrorMessageResourceType = typeof (Resources.Messages);
  23.     }
  24. }

Por supuesto me he creado mi fichero Resources.resx dentro de la carpeta App_GlobalResources (tal y como cuenta José María en su post):

image

Lamentablemente eso rompe la validación en cliente de MVC3. A modo de ejemplo tengo dicha entidad, con dos propiedades, una decorada con el Range de toda la vida y otra con mi LocalizedRange:

  1. public class Ufo
  2. {
  3.     [Range(1,90)]
  4.     public int Age { get; set; }
  5.  
  6.     [LocalizedRange(1,90)]
  7.     public int LocalizedAge { get; set; }
  8. }

Creo una vista estándar para editar objetos de este modelo:

  1. @model WebApplication1.Models.Ufo
  2.  
  3.  
  4. @Html.LabelFor(m=>m.Age)
  5. @Html.TextBoxFor(m=>m.Age)
  6. <br />
  7. @Html.LabelFor(m => m.LocalizedAge)
  8. @Html.TextBoxFor(m => m.LocalizedAge)

Si nos vamos al código fuente de la página veremos lo siguiente:

  1. <label for="Age">Age</label>
  2. <input data-val="true" data-val-number="The field Age must be a number." data-val-range="The field Age must be between 1 and 90." data-val-range-max="90" data-val-range-min="1" data-val-required="The Age field is required." id="Age" name="Age" type="text" value="" />
  3. <br />
  4. <label for="LocalizedAge">LocalizedAge</label>
  5. <input data-val="true" data-val-number="The field LocalizedAge must be a number." data-val-required="The LocalizedAge field is required." id="LocalizedAge" name="LocalizedAge" type="text" value="" />

Observa como el segundo input, que se corresponde a la propiedad LocalizedAge (decorada con mi LocalizedRangeAttribute) no tiene los atributos para validar en cliente el rango (los data-val-range-*). Por lo tanto la validación en cliente de dicho campo no funcionará.

En servidor por supuesto la validación funcionará y además se puede ver que en el segundo caso se usa el mensaje del fichero de recursos:

image

De todos aunque la herencia funcionase bien existen motivos para no usarla (p. ej. si quieres enviar a una vista una entidad de EF, deberías decorar dicha entidad con los atributos heredados, lo que no es muy bonito y no sé si puede causar efectos colaterales en el propio EF).

Adaptadores de atributos

Vale, queda claro que la herencia de atributos no funciona bien con la validación remota. Pero que no cunda el pánico, ASP.NET MVC nos da otro mecanismo: los adaptadores de atributos.

Para crear una adaptador de atributo, debemos derivar de una clase de MVC, que depende del tipo de atributo. P. ej. para crear un adaptador para el atributo de Range, debemos derivar de System.Mvc.RangeAttributeAdapter:

  1. public class LocalizedRangeAttributeAdatper : RangeAttributeAdapter
  2. {
  3.     public LocalizedRangeAttributeAdatper(ModelMetadata metadata, ControllerContext context, RangeAttribute attribute) : base(metadata, context, attribute)
  4.     {
  5.         attribute.ErrorMessageResourceName = "Range";
  6.         attribute.ErrorMessageResourceType = typeof(Resources.Messages);
  7.     }
  8. }

El adaptador recibe en su constructor al propio RangeAttribute y allí aprovechamos para establecer las propiedades ErrorMessageResourceName y ErrorMessageResourceType.

Una vez hemos creado el adaptador, debemos declararlo en ASP.NET MVC. Para ello en el Application_Start metemos el siguiente código:

  1. DataAnnotationsModelValidatorProvider.
  2.     RegisterAdapter(typeof (RangeAttribute), typeof (LocalizedRangeAttributeAdatper));

La llamada RegisterAdapter acepta dos parámetros: el tipo del atributo a adaptar y el tipo del adaptador. Una vez hecho esto, automáticamente todos los atributos Range pasarán a usar, los recursos indicados. Ya no hay necesidad ninguna del LocalizedRange.

Otros adaptadores de atributos son los siguientes (todos en el namespace System.Web.Mvc):

image

¡Espero que os haya sido útil!

Saludos!

C#- Vitaminiza tus enums con métodos de extensión

Buenas, un post cortito y sencillito 😉

En C# los enums son relativamente limitados: básicamente se limitan a tener un conjunto de valores y nada más. En otros lenguajes como Java o Swift, los enums pueden declarar métodos.

A priori puede parecer que no es muy necesario que un enum tenga un método, y de hecho no es algo que se suela echar en falta. Pero en algunos casos puede ser útil, especialmente para tener nuestro código más bien organizado.

P. ej. imagina un enum que contuviese los valores de los puntos cardinales:

  1. public enum FacingOrientation
  2. {
  3.     North = 0,
  4.     East = 1,
  5.     South = 2,
  6.     West = 3
  7. }

Ahora podríamos requerir un método que nos devolviese el siguiente punto cardinal, en sentido horario. Es decir si estamos mirando al norte y giramos en sentido horario, estaremos mirando al este.

Este método sería un candidato para estar en el propio enum para que así pudiese hacer tener código como el siguiente:

  1. var orientation = FacingOrientation.South;
  2. var neworientation = orientation.Turn(1);

El método Turn devolvería la nueva orientación después de N giros en sentido horario.

Como he dicho antes en C# esto no es directamente posible porque los enums no pueden contener métodos. Pero por suerte si que podemos declarar un método de extensíón sobre un enum específico:

  1. public static FacingOrientation Turn(this FacingOrientation orientation, int steps)
  2. {
  3.     var idx = (int)orientation;
  4.     idx += steps;
  5.     return (FacingOrientation)(idx % 4);
  6. }

Y el resultado es a todos los efectos casi idéntico 🙂

Saludos!

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!