Y el combate se decidió por KO (iv): Choque de viewmodels

Disclaimer: Este post es un poco distinto al resto de posts de esta serie sobre knockout. De hecho no tenía presente escribirlo pero lo he hecho cuando he visto la cantidad de preguntas relativas a ello que aparecen por Google. Aunque lo he reescrito varias veces, entiendo que puede ser un post durillo de leer, especialmente si no se tiene experiencia previa en ASP.NET MVC. Si NO quieres leerte este post, no te preocupes: no es necesario para nada para entender el resto de posts de la serie, ni explica nada nuevo sobre knockout que no hayamos vistos en los 3 anteriores. Se trata simplemente de divagaciones sobre el siguiente tema: ¿Se pueden usar los helpers de ASP.NET MVC para mostrar o editar datos junto con knockout?

En los tres primeros posts de esta serie sobre Knockout hemos estado viendo algunos de los aspecto básicos de dicha librería (en sucesivos posts iremos viendo más). Pero hasta ahora nos hemos limitado a un modelo de aplicación en el que la vista obtenía los datos llamando a un servicio REST, y a partir de dichos datos creaba su viewmodel y usaba knockout para representarlo en pantalla.

Pero este modelo, extremadamente útil si hacemos uso intensivo de Ajax, no es el único posible. Existe otro, más “clásico” en el que las vistas reciben datos de sus controladores (en lugar de tener que realizar una llamada ajax a un servicio REST). En el contexto de ASP.NET MVC llamamos también viewmodel a los datos que un controlador manda a la vista (y que esta accede a través de su propiedad Model). Así pues ahora vamos a lidiar con dos viewmodels:

  1. Los datos que el controlador manda a la vista y que esta accede usando la propiedad Model. Es el viewmodel de ASP.NET MVC o el viewmodel de servidor.
  2. El objeto javascript que usa knockout. Es el viewmodel de cliente.

En este post veremos como el “modelo clásico de ASP.NET MVC” choca con el modelo knockout y daremos algunas indicaciones de como podemos solucionarlo. No llegaremos a una solución completa porque simple y llanamente no la hay.

Escenario 1. Mostrando datos

Vamos a suponer que tenemos una clase Beer:

   1: public class Beer

   2: {

   3:     public string Name { get; set; }

   4:     public int Ibu { get; set; }

   5: }

Y luego tenemos un controlador con una acción tal que:

   1: public ActionResult Index()

   2: {

   3:     var beers = new List<Beer>

   4:                     {

   5:                         new Beer { Name = "Estrella Damm", Ibu = 30},

   6:                         new Beer { Name = "Marina Devil's IPA", Ibu = 150}

   7:                     };

   8:     return View(beers);

   9: }

La vista recibe esta lista de cervezas en su propiedad Model. Ahora la pregunta es, podemos usar knockout para mostrar los datos?

La respuesta es que sí, aunque para ello debemos crear el viewmodel de cliente a partir de los datos del viewmodel de servidor:

   1: @using System.Web.Script.Serialization

   2: @model IEnumerable<MvcApplication1.Models.Beer>

   3: @{

   4:     ViewBag.Title = "Index";

   5:     var ser = new JavaScriptSerializer();

   6:     var jscode = Model != null ? ser.Serialize(Model) : string.Empty;

   7: }

   8:  

   9: <h2>Index</h2>

  10:  

  11: <script type="text/javascript">

  12:     $(document).ready(function () {

  13:         var vm = JSON.parse('@Html.Raw(jscode)');

  14:         vm = completeViewModel(vm);

  15:         ko.applyBindings(vm);

  16:     });

  17:  

  18:     function completeViewModel(vm) {

  19:         return { items: vm };

  20:     }

  21: </script>

La vista contiene un bloque de código Razor y luego un tag <script> con código de cliente:

  1. El código Razor recoje el ViewModel de ASP.NET MVC y lo serializa a una cadena JSON.
  2. El código <script /> usa JSON.parse para obtener un objeto javascript a  partir de la cadena en JSON obtenida en el código anterior.

Si ejecutamos esta vista y ponemos un breakpoint en el javascript cuando se ha obtenido el viewmodel de cliente (yo he usado las herramientas de Chrome para ello), vemos que efectivamente tenemos un objeto javascript con los datos del viewmodel de ASP.NET MVC (excepto que le he añadido la propiedad items que me servirá para el enlace con knockout):

image

Finalmente tan solo nos queda mostrar los datos:

   1: <div id="beers" data-bind="foreach: items">

   2:     <span data-bind="text: Name"></span> - <span data-bind="text: Ibu"></span>

   3:     <br />

   4: </div>

En este punto es necesario hacer una mención importante: la cadena JSON resultado de convertir a JSON el viewmodel de ASP.NET MVC se está incluyendo en el código fuente de la página (a través del Html.Raw). Eso es lo que obtengo si hago un “ver codigo fuente”:

   1: var vm = JSON.parse('[{"Name":"Estrella Damm","Ibu":30},{"Name":"Marina Devilu0027s IPA","Ibu":150}]');

Por lo tanto, si mi viewmodel ASP.NET MVC es grande eso puede generar páginas grandes (en Kilobytes) y por lo tanto lentas.

Pregunta: Puedo usar los helpers de ASP.NET MVC (DisplayFor y similares) para mostrar estos datos?

Respuesta: Si… y no. Me explico.

Algunos helpers pueden no generar tag html. Por ejemplo DisplayFor para una propiedad de tipo string, no genera tag alguno (simplemente renderiza el texto tal cual). Si no hay tag HTML, no hay sitio donde poner el atributo data-bind para usar con knockout.

Si usas un helper que genere un tag (p.ej. pones un Html.TextboxFor), entonces puedes usarlo, pero ten presente que el propio helper ya genera el atributo value, así que realmente el enlace con knockout no tiene mucho sentido, si tan solo vas a mostrar datos.

Mi recomendación es que te olvides de los helpers de MVC si vas a mostrar datos usando knockout. Si de todos modos quieres usarlos debes hacer que dichos helpers generen el atributo data-bind para mostrar los datos usando knockout. P.ej. el siguiente código generaria un textbox con el atributo data-bind=”value: Name” y de solo lectura:

   1: @Html.TextBoxFor(x => x.Name, new { data_bind = "value: Name", @readonly = true })

(Fíjate en que como data-bind no es un nombre válido para una propiedad del objeto anónimo en C# se usa data_bind (ASP.NET MVC transforma el guión bajo en un guión al generar el código), y el uso de @readonly en lugar de readonly (que es palabra clave reservada de C#)).

No obstante… crees que tiene realmente sentido hacer esto? Ya tienes tus datos en el viewmodel de knockout, es mucho mejor usar los mecanismos de knockout para mostrarlos!

Escenario 2: Edición

Empecemos por hacer que nuestro controlador devuelva una sola cerveza y vamos a intentar editarla usando knockout. Ni corto ni perzoso he creado la acción nueva (Edit):

   1: public ActionResult Edit()

   2: {

   3:     var beer = new Beer() {Name = "Mezquita", Ibu = 50};

   4:     return View(beer);

   5: }

Bien, ahora vamos a crear una vista de edición, pero usando knockout, para ello usando el esquema anterior, obtenemos el viewmodel de knockout a partir del viewmodel de ASP.NET MVC:

   1: @using System.Web.Script.Serialization

   2: @model MvcApplication1.Models.Beer

   3: @{

   4:     ViewBag.Title = "Index";

   5:     var ser = new JavaScriptSerializer();

   6:     var jscode = Model != null ? ser.Serialize(Model) : string.Empty;

   7: }

   8:  

   9: <script type="text/javascript">

  10:     $(document).ready(function () {

  11:         var vm = JSON.parse('@Html.Raw(jscode)');

  12:         ko.applyBindings(vm);

  13:     });

  14:  

  15: </script>

Bien, ahora viene el siguiente punto. Intentemos usar los helpers de ASP.NET MVC para crear los controles de edición:

   1: @Html.LabelFor(x=>x.Name)

   2: @Html.TextBoxFor(x=>x.Name, new {data_bind="value: Name"})

   3: <br />

   4: @Html.LabelFor(x=>x.Ibu)

   5: @Html.TextBoxFor(x=>x.Ibu, new {data_bind="value: Ibu"})

Fijaos ya en el “primer choque”. A pesar de usar las versiones “strong typed” de los editores, tengo que especificar de nuevo el nombre de la propiedad como valor de data_bind. Eso es proclive a errores, ya que puedo hacer TextBoxFor(x=>x.Name) y en el data_bind poner otro nombre de propiedad. Por supuesto esto se podria arreglar con un helper propio.

Al margen de este detalle, parece que todo funciona. Incluso si envio el formulario me llegan los datos modificados:

image

Pero… reflexionemos. Hemos usado knockout para algo? Pues no. De hecho puedes comprobarlo comentando la línea ko.applyBindings y verás que todo sigue funcionando! Eso es porque los helpers generan el atributo value de los controles que ya establece su valor inicial. Luego una vez se envía el formulario entra en acción el ModelBinder que rellena el viewmodel de ASP.NET MVC a partir de los datos del POST. Knockout no hace nada ahí.

Así pues si nos limitamos a enviar (submit) los datos introducidos en unos campos de texto que están dentro de un formulario, knockout no pinta nada. El binding de knockout es en cliente. Knockout lo usaríamos si “antes” de enviar los datos queremos hacer algos con ellos en cliente. Así pues vamos a probar de hacer algo con los datos en cliente.

P.ej. mostrarlos (con un alert). Para ello asignamos un evento javascript en el submit del formulario:

   1: $("form").submit(function(evt) {

   2:     alert(vm.Name + " " + vm.Ibu);

   3: });

Y lo probamos… ¿Creéis que funcionará? La respuesta en la imagen siguiente:

image

Como era de esperar, funciona porque konockout modifica el viewmodel a partir de los datos de los controles. Recuerda que luego cuando enviamos el formulario, enviamos los datos que están en los controles (no usamos el viewmodel de knockout para nada). ¡Pero ahora sabemos que los tenemos sincronizados!

Ahora hagamos una cosilla… En el textbox de Ibu (que es un int) introduzcamos algo que no sea numérico, p. ej. “Pepe”. Y eso es lo que obtenemos de vuelta:

image

¿Qué os parece? ¿Os gusta? A mi no. ¿Por que no me gusta esto?

Pues muy fácil: he perdido el valor incorrecto que había entrado. Y es que ese es un comportamiento de los helpers de asp.net mvc: si entro algún valor incorrecto se preserva. Si queréis hacer la prueba basta con comentar la línea ko.applyBindings y lo veréis:

image

Este comportamiento es por diseño y es propio de los helpers de edición. ¿Como es que al usar knockout perdemos el valor “pepe” incorrecto y se nos sustituye por un 0? Pues muy sencillo:

  1. El helper genera el código de forma correcta, con el valor del atributo value a “pepe”.
  2. El viewmodel de ASP.NET MVC pasado a la vista tiene un 0 en la propiedad Ibu (no hay manera de convertir “pepe” a int, así que el ModelBinder no hace nada (y el valor por defecto de un int es 0) y deja un error en el ModelState).
  3. Al crear el viewmodel de knockout lo creamos a partir del viewmodel de ASP.NET MVC que tiene un 0 en la propiedad Ibu.
  4. Al llamar a ko.applyBindings() modificamos el valor inicial del control (que era “pepe”) por el valor del viewmodel de knockout (que es 0).

Por lo tanto “perdemos” esta capacidad de los helpers de mantener el dato incorrecto entrado (si este no era asignable al viewmodel de ASP.NET MVC).

Si has leído los posts anteriores de esta serie, probablemente habrás levantado una ceja cuando creábamos el viewmodel de knockout a partir de la cadena json obtenida de serializar el viewmodel de ASP.NET MVC. Por qué? Bueno… modifiquemos la vista para que el titulo sea así:

   1: <legend>Editando: <span data-bind="text: Name"></span></legend>

Ahora la idea es que al modificar el textbox que contiene el nombre de la cerveza, se modifique el título, pero eso no ocurre:

image

He modificado el textbox pero en el título sigue apareciendo el nombre anterior. La razón? Pues muy sencillo, el viewmodel de knockout es un objeto javascript plano. No tiene definido ningún observable.

Es posible crear observables de forma relativamente sencilla, a partir del viewmodel de ASP.NET MVC? La respuesta es que sí, pero para ello debemos usar un plugin de knockout (knockout mappings). No lo veremos en este post (sí en alguno futuro), quedaros con la idea de que se puede hacer de forma “relativamente” sencilla.

Resumiendo: en ediciones simples (de un solo elemento simultaneo) podemos usar los helpers de asp.net mvc, aunque con las salvedades vistas en este punto. Tu mismo debes decidir si te compensa o no usarlos.

Mi opinión: Los helpers de ASP.NET MVC existen para dar solución a un problema concreto. La verdad es que knockout, realmente, da solución al mismo problema pero lo hace desde una aproximación radicalmente distinta. Intentar aprovechar los helpers de ASP.NET MVC junto con knockout es posible, pero no es algo que yo haría. Recuerda que los helpers están para ayudar, en ningún caso estamos obligados a usarlos!

Si no te sientes a gusto creando “a mano” controles HTML (¡deberías sentirte a gusto con ello!) y quieres una “aproximación tipo helpers MVC” para knockout pues la solución pasa por implementarte tus propios helpers…

Bueno… dejemos de divagar por hoy!

Un saludo!!!!

Deja un comentario

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