Binding la historia interminable

Este post surge a raíz de una pregunta de un amigo de gusenet en el grupo de facebook y que básicamente lo que quería es bindear el siguiente modelo de JavaScript a un controlador MVC.

1. Modelo JavaScript

image

En un principio listStr se llamaba listInt y contenía valores integer, solo que adultere inicialmente su proyecto para ver si ese “Ho,la2” al contener una “,” me generaba 5 elementos en vez de cuatro, cosas mías.

También agregue el campo name con valor “Hola”, para comprobar ciertas cosas en el modelo de Binding de MVC.

2. Firma del método del controlador.

Originalmente tenía la siguiente firma, cosa que ya me pareció un tanto rara al no estar todos los parámetros agrupados en una clase, pero bueno, la única consecuencia y es lo que vamos a ver es que esa firma no funciona en WebApi.

image

Bueno sabiendo que ese método, no era compatible con WebApi lo que hice fue crear un modelo con la siguiente estructura y dejar el método lo más parecido a un método WebApi.

A estas alturas te preguntas por qué?, pues sencillo, intento no escribir más código del absolutamente necesario y cuanto más líneas escriba en un controlador MVC y WebApi más voy a tirar.

Modelo que agrupa cada una de los parámetros.

image

Quiero que os fijéis en la parte que está comentada, un constructor que asigna cada uno de los parámetros a propiedades de la clase definidas con el modificador private set, esta ha sido siempre mi forma de definir mis modelos tanto en WebApi como en MVC. Evidentemente sin el el atributo “JsonProperty” que no es más que un requerimiento de Json.Net si quieres no definir un constructor con parametros. Cosa que evidentemente no me gusta, con lo cual mi modelo ideal sería algo como esto.

image

Pero vamos no consigues que sea compatible entre MVC y WebApi excepto que hagas algún que otro cambio y es lo que voy a hacer y ver si me aceptan un Pull Request

Una vez visto todo esto, vamos a mostrar los métodos de los controladores tanto de WebApi como de MVC y vemos lo que ocurre, enviando datos desde JQuery.

Método del controlador WebApi

image

Método del controlador MVC

image

Como podéis ver me han quedado los métodos, bastante parecidos y ahora vamos con las tres llamadas de JQuery.

1. Usando traditional=true sobre el controlador de MVC.

image

Solamente bindea la propiedad “Name” del modelo y listStr. Person nos llegan con valor  null

2. Usando traditional=true sobre el controlador WebApi.

image

Bindea “Name” y ListStr crea el objeto Person con todas las propiedades a null.

El motivo está claro cuando tu utilizas $.param de JQuery lo que hace es recorrer todas las propiedades del objeto JavaScript y hacer toString de cada una de ellas ,con lo cual el toString() de person es [object Object] y eso es lo que nos llega al servidor.

En resumen intenta olvidar el uso de traditional=true, puesto que para objetos complejos no funciona.

3. Usando traditional=false sobre el controlador MVC.

image

Solamente hace binding correctamente sobre la propiedad Name, listStr llega a null y Person se crea peso todas sus propiedades llegan a null

4. Usando traditional=false sobre un controlador WebApi.

image

Bien, hemos conseguido nuestro objetivo. Por fin tenemos todas las propiedades bindeadas correctamente. Y ahora es el momento de reflexiones.

1. Porque funciona en WebApi y no en MVC. Sencillo MVC para hacer esto necesita un ValueProvider que no existe, es decir alguien ha escrito un ValueProvider y un MediaTypeFormatter en WebApi y no ha pensado en MVC. El responsable de que todo esto funcione es el JQueryMvcFormUrlEncodedFormatter.

2. Como he comentado la idea de hacer un Pull Request os voy a pasar el código necesario para que esto mismo funcione en MVC.

Escribimos las siguiente clases

   1: public class MyValueProvider : IValueProvider

   2: {

   3:  

   4:     readonly Dictionary<string, ValueProviderResult> _dictionay = new Dictionary<string, ValueProviderResult>(StringComparer.OrdinalIgnoreCase);

   5:     public MyValueProvider(NameValueCollection collection)

   6:     {

   7:         foreach (var item in collection.AllKeys)

   8:         {

   9:            

  10:             if (item.Contains("[]"))

  11:             {

  12:                 MakeArray(collection, item);

  13:             }

  14:             else

  15:             {

  16:                 var name = string.Empty;

  17:                 if (TryGetComplexObject(item, out name))

  18:                 {

  19:                     MakeComplexObject(collection, item, name);

  20:                 }

  21:              

  22:             }

  23:         }

  24:     }

  25:     private bool TryGetComplexObject(string item,out string name)

  26:     {

  27:         var values = item.Split('[');

  28:         name = null;

  29:         if(values.Length>1)

  30:         {

  31:             name = GetName(values);

  32:             return true;

  33:         }

  34:         return false;

  35:     }

  36:     private void MakeComplexObject(NameValueCollection collection,string item, string name)

  37:     {               

  38:         var value = collection[item];

  39:         _dictionay[name] = GetValueProvider(value, CultureInfo.CurrentCulture);              

  40:     }

  41:     private void MakeArray(NameValueCollection collection, string item)

  42:     {

  43:         var culture = CultureInfo.CurrentCulture;

  44:         var values = collection.GetValues(item);     

  45:         var name = item.Replace("[]", "");           

  46:         

  47:         for(var i =0;i<values.Length;i++)

  48:         {

  49:             var key = string.Format("{0}[{1}]", name, i);

  50:             _dictionay[key] = GetValueProvider(values[i], culture);

  51:             

  52:         }

  53:     }

  54:     private ValueProviderResult GetValueProvider(string value,CultureInfo culture)

  55:     {

  56:         string attemptedValue = Convert.ToString(value, culture);

  57:         return  new ValueProviderResult(value, attemptedValue, culture);

  58:     }

  59:     private string GetName(string[] values)

  60:     {           

  61:  

  62:         var sb = new StringBuilder();            

  63:         var separator = string.Empty;

  64:         foreach (var segment in values)            

  65:         {                

  66:             var name = segment.Replace("]", "");

  67:             var number = 1;

  68:             bool isArray = int.TryParse(name, out number);               

  69:             if (isArray)

  70:             {

  71:                 name = string.Format("[{0}]", name);

  72:                 separator = string.Empty;

  73:             }

  74:             sb.Append(separator);

  75:             sb.Append(name);                

  76:             separator = ".";

  77:         }

  78:         return sb.ToString();

  79:     }

  80:  

  81:     public bool ContainsPrefix(string prefix)

  82:     {

  83:         var result = (_dictionay.Keys.Contains(prefix) || _dictionay.Keys.Where(c => c.ToUpper().StartsWith(prefix.ToUpper())).Count() > 0);

  84:         return result;           

  85:     }

  86:     public ValueProviderResult GetValue(string key)

  87:     {

  88:         ValueProviderResult result;

  89:         _dictionay.TryGetValue(key, out result);

  90:         return result;

  91:     }

  92: }

  93: public class MyValueProviderFactory : ValueProviderFactory

  94: {

  95:     public override IValueProvider GetValueProvider(ControllerContext controllerContext)

  96:     {

  97:         if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))

  98:         {                

  99:             return null;

 100:         }            

 101:         return new MyValueProvider(controllerContext.HttpContext.Request.Form);

 102:     }

 103: }

y agregamos esta linea al Global.asax de MVC.

ValueProviderFactories.Factories.Add(new MyValueProviderFactory())

Si ahora probáis, os va a funcionar exactamente igual en MVC como en WebApi.

4. Utilizando Application/Json con JQuery, puesto que los dos intentos anteriores envían  application/x-www-form-urlencoded y ya había una propuesta para mi equivocada de como se podía solucionar esto en MVC.

Model Binding To A List no es que no funcione que si funciona ,pero a la postre no es más que ir contra corriente y para eso no mejor que leerte esto jQuery.param() y concretamente este párrafo.

image

Es decir, que nosotros siendo más modernos que estos framework no soportamos algo que ya estaba desde JQuery 1.4. Pero no es que no lo soportemos, es que hasta hace poco era como un querer diferenciarnos y al final darnos cuenta que esa diferenciación no ha llegado a ningún sitio, me apena de verdad Triste.

Cuando realmente lo único que había que hacer es transformar algo como esto

person[Address][0][Street] a esto otro person.Addres[0].Street que no es otra cosa que lo que yo hago en el código que os he pasado y lo que hace el NameValuePairsValueProvider en el MediaTypeFormatter JQueryMvcFormUrlEncodedFormatter.

Vamos a ver que esto está en extension Method en la dll System.Web.Http y lo mismo costaba mucho introducirla en la dll System.Web.Mvc Triste.

Comprendéis ahora esta entrada en twitter mía

image

https://twitter.com/_PedroHurtado/status/464073404438814720

Bueno dicho todo esto vamos a ver que pasa cuando utilizamos application/json.

image

Pues ya os lo digo, que funciona tanto en MVC como en WebApi y este es el camino que el amigo Javier Troubleshooter opto por utilizar para este caso, pero tambien deja mucho que desear el planteamiento de MVC. JavaScriptSerializer en vez de haber implementado en la nueva versión Json.Net.

Como cierre me preguntaba que si este último forma podría tener alguna consecuencia, pues ya te digo amigo Javier que excepto que tengas habilitado CORS y te autentiques por Cookie, no. Es más quien tiene consecuencias y bastante serias sino se utiliza un mecanismo para prevenir CRSF son los otros dos.

Pero esto lo voy a dejar para el siguiente post que no es otra cosa que poder contar cual era mi charla en el gusenet, que por motivos conocidos no logre darSonrisa.

Conclusiones.

Que no se hagan las cosas a trozos que al final tenemos un framework super pesado por tener cosas repetitivas en varias dll’s para resolver el mismo problema.

Que se revise todo el modelo de binding de MVC y WebApi que le hace falta.

Mucha gente me ha pedido que escriba de Gusenet y haga un resumen de que tal fue el evento y el porque. Bueno no me gusta escribir de esas cosas y prefiero transmitir otra idea de lo que es el Gusenet.

1. Ayuda a cualquier persona.

2. Diviértete.

3. No lo utilices como medio para nada, ni a nivel personal ni a nivel de empresa, es muy desagradable escuchar alguna cosa como que yo quiero dar la charla en el palacio y no en el bar. Y que más te da, si piensas en eso no has entendido ni un pelo lo que es la idea de comunidad del Gusenet.

Compartir y Divertirse.

Deja un comentario

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