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!!!!

Y el combate se decidió por KO (iii)

Bueno, continuamos aquí nuestra serie explorando las maravillas de Knockout. Todos los posts de esta serie los podéis encontrar en: http://geeks.ms/blogs/etomas/archive/tags/knockout/default.aspx

Serializando viewmodels

En el post anterior, vimos los observables de knockout y como funcionaban. Vimos como crear un formulario, enlazarlo a un viewmodel que usara observables y como mandar el viewmodel serializado en json hacia un servicio REST.

Ciertamente, el tema de la serialización a JSON de nuestro viewmodel era un poco peliagudo. Dado que los observables son funciones debíamos “crear” un objeto adicional a partir de nuestro viewmodel, invocando a los observables de forma manual, ya que usar JSON.stringify sobre nuestro viewmodel no funcionaba (los observables no se serializaban). El código que usábamos era:

   1: var jsonBeer = JSON.stringify({

   2:    Name : this.Name(),

   3:    Ibu : this.Ibu(),

   4:    Abv : this.Abv()

   5: });

Tener que crear otro objeto “con la misma” estructura que nuestro viewmodel e ir llamando manualmente a los observables es posible para viewmodels sencillitos como este, pero para viewmodels más grandes es, como mínimo, un peñazo.

Como ya debes estar pensando, knockout ofrece una solución a ese problema, ya que como comentamos en el post anterior enviar objetos via JSON es bastante común hoy en día. Si vuestro viewmodel contiene observables entonces la mejor manera de serializarlos es usar uno de los métodos siguientes:

  1. ko.toJS: Convierte el viewmodel a un objeto “plano” javascript (es decir hace justo lo que hemos hecho nosotros a mano, es decir invocar los observables). El objeto devuelto puede serializarse a JSON de la forma que se prefiera.
  2. ko.toJSON: Llama a ko.toJS y serializa el objeto usando JSON.stringify.

Así, en lugar del código anterior podemos usar tranquilamente:

   1: var jsonBeer = ko.toJSON(this);

Para la deserialización (es decir, obtención del objeto viewmodel a partir de los datos JSON), Knockout no trae de serie nada. Esto implica que si nuestro viewmodel usa observables debemos manualmente crear nuestro viewmodel y llamar a los observables para inicializar el valor. Eso también puede ser un problema y para lidiar con ello existe un plugin de knockout. Hablaremos de él más adelante en esta serie de posts.

Observables calculados

Recuerda que los viewmodels están fuertemente atados a la vista. De hecho son la abstracción del modelo para una vista en particular y su tarea es “facilitar” al máximo el código de la vista.

Vamos a añadir en el formulario de modificación, el tipo de cerveza (si es una IPA, una stout o una pale ale, p. ej.). A nivel del servicio REST hemos de modificar la clase Beer:

   1: public class Beer

   2: {

   3:     public string Name { get; set; }

   4:     public decimal Abv { get; set; }

   5:     public int Ibu { get; set; }

   6:     public string Style { get; set; }

   7: }

Por el momento esto nos basta. Ahora, modificamos nuestra vista de Edicion (Edit.cshtml) para que tenga un campo adicional más donde entrar el tipo de cerveza:

   1: <label for="Name">Style</label>

   2: <input type="text" name="Name" id="Style" data-bind="value: Style" />

   3: <br />

Bien, fijaos en el uso del data-bind para enlazar este <input /> al valor de la propiedad Style del viewmodel.

Por supuesto debo modificar en la vista cuando creamos el viewmodel, para inicializar el observable Style, a partir de los datos JSON devueltos por el servicio:

   1: $.getJSON(uri, function(data) {

   2:     vm = {

   3:         Name  : ko.observable(data.Name),

   4:         Ibu  : ko.observable(data.Ibu),

   5:         Abv : ko.observable(data.Abv),

   6:         Style : ko.observable(data.Style),

   7:         editBeer : function() {

   8:     // Continua...

Finalmente modifico el método GetById del controlador ApiBeerController para que me informe del valor del campo Style:

   1: public Beer GetById(int id)

   2: {

   3:     return new Beer { Name = "Imperial Stout #" + id, Abv = 10.0M, Ibu = 120, Style="Imperial Stout"};

   4: }

Si ahora ejecuto obtengo lo esperado:

image

Hasta ahí no hemos hecho nada nuevo

Bien, ahora imaginad que queremos presentar en el título el nombre y el tipo de la cerveza, algo como “Heineken (American Lager)”. Una solución es componer esto en la vista. Para ello modifico el <h2>:

   1: <h2 id="title"></h2>

Y luego en el código javascript, cuando he cargado el viewmodel, justo antes (o después) de la llamada a ko.applyBindings coloco el siguiente código javascript:

   1: $("#title").text(vm.Name() + "(" + vm.Style() + ")");

El resultado es el esperado (se muestra el nombre y el tipo de cerveza dentro del <h2>. Pero… no os suena eso a un enlace? La función de un viewmodel es facilitar al máximo la creación de la vista, siendo una representación de lo que la vista muestra. No podría tener el viewmodel una propiedad que devolviese esta cadena? Es una propiedad especial cierto, ya que es de solo lectura y su valor está calculado a partir del valor de otras propiedades.

Esto en knockout se conoce como un observable calculado y se crean usando el método ko.computed. Este método recibe un parámetro que es la función que calcula el valor del observable:

   1: vm.Title = ko.computed(function() {

   2:     return vm.Name() + "(" + vm.Style() + ")";

   3: });

Aquí debo hacer un apunte importante sobre la sintaxis Javascript. Como quizá ya sabéis hay dos maneras en Javascript de inicializar objetos: usando un constructor o bien usando notación literal. Yo he usado en los ejemplos de esta serie he usado la notación literal (mientras que en los ejemplos de la propia web usan más la notación de constructor). Pues bien, los observables calculados son incompatibles con la notación literal. Eso significa que NO puedes declarar observables calculados a la vez que declaras el resto del viewmodel, y los tienes que añadir después. De hecho el código completo de la creación del viewmodel es:

   1: vm = {

   2:     Name  : ko.observable(data.Name),

   3:     Ibu  : ko.observable(data.Ibu),

   4:     Abv : ko.observable(data.Abv),

   5:     Style : ko.observable(data.Style),

   6:     editBeer : function() {

   7:        // Codigo...

   8:     }

   9: };

  10: vm.Title = ko.computed(function() {

  11:     return vm.Name() + "(" + vm.Style() + ")";

  12: });

Fijaos como las propiedades Name, Ibu, Abv, Style y editBeer son definidas en notación literal (propiedad : valor), pero el observable calculado lo he añadido luego.

Aclarado este aspecto “notacional”,  tan solo nos queda enlazar el <h2> con el valor del campo Title de nuestro viewmodel:

   1: <h2 id="title" data-bind="text: Title"></h2>

Y ahora podemos ver como todo funciona correctamente:

image

Pero recordad que los observables son “bidireccionales” no? Basta con modificar el textbox de Name o el de Style para que… ¡se cambie el título!

image

¿No os parece una pasada?

Bueno, lo dejamos aquí por hoy, en futuros posts seguiremos explorando las capacidades de knockout!

Saludos!

El problema de la WebGrid con VS2012RC y ASP.NET MVC4

Nota: Este post está basado en la versión RC de VS2012 y la versión RC de MVC4 y es posible (o eso espero, vaya!) que en la versión final no haya los problemas que este post menciona!

Buenas! Coje un VS2102RC y crea un nuevo proyecto ASP.NET MVC4, con la plantilla “Basic”.

Crea el HomeController, crea la acción Index y añádele un código tal como:

   1: public ActionResult Index()

   2: {

   3:     var data = new List<dynamic>() { new { Name = "Edu", Twitter = "eiximenis" } };

   4:     return View(data);

   5: }

Finalmente crea la vista Index.cshtml:

   1: @{

   2:     var grid = new WebGrid(Model, new [] {"Name", "Twitter"});

   3: }

   4: @grid.GetHtml()

Seguro que esperas ver una grid con las dos columnas y una fila no? ¡Pues no! Lo que verás es:

image

Obtendrás el error “CS0246: The type or namespace name ‘WebGrid’ could not be found (are you missing a using directive or an assembly reference?)”.

Si repites este mismo procedimiento pero cojes la plantilla “Empty” el código funcionará sin problemas 🙂

¿Y donde está el problema?

Pues eso me he estado preguntando un buen rato. Por supuesto antes de intentar hacer nada he usado el comodín de Google pero esta vez me ha fallado. La única referencia que he encontrado es un post en los blogs de ASP.NET (http://forums.asp.net/t/1823940.aspx/1?MVC4+WebGrid+problem+in+View+Razor+) donde alguien más experimenta el problema pero no se llega a ninguna solución.

Al final lo que he hecho para solucionar el problema ha sido:

  1. Desde NuGet desinstalar el paquete ASP.NET MVC4 RC 4.0.20505. Eso ha desinstalado también los paquetes Microsoft.AspNet.WebPages 2.0.20505.0 y Microsoft.AspNet.Razor 2.0.20505.0
  2. Volver a instalar el paquete ASP.NET MVC4 RC 4.0.20505 desde NuGet. Me ha dado un error y ha hecho un rollback.
  3. Intentar de nuevo volver a instalar el paquete ASP.NET MVC4 RC 4.0.20505. Ahora todo ha funcionado correctamente.

Después del tercer punto, la WebGrid ya funcionaba correctamente.

Honestamente desconozco la causa, pero bueno… si alguien se encuentra con ello, ya lo sabe. Que pruebe esto a ver si le funciona! 🙂

Un saludo!

Y el combate se decidió por KO (ii)

Como indica el título del post, ese es el segundo post de la serie que he empezado sobre knockout. Honestamente no sé cuantos posts habrá ni donde me (nos) llevará, pero espero que os sea útil!

En el post anterior (el primero) vimos un poco que era knockout y como mostrar datos devueltos a partir de un servicio REST implementado con WebApi. Ahora toca ir un poco más allá…

Formulario que te quiero formulario

La web está llena de formularios. Están en todos los sitios: para login y password, para darse de alta en un sitio, para solicitar información, para reservar tus vacaciones a Playa Bávaro… Los formularios son el mecanismo más sencillo para enviar información desde el cliente (navegador) al servidor. Usualmente (aunque ello no es obligatorio) se envían via POST, es decir conteniendo sus datos en el cuerpo de la petición, en lugar de GET donde los datos deben ir en la querystring. La razón es que los datos de un formulario pueden ser abritrariamente largos y podríamos tener URLs demasiado largas. Aunque realmente la especificación de http no impone un límite a las URLs (ver RFC2616 punto 3.2.1), también deja claro que no tienen porque ser “arbitrariamente largas”. Es decir, cada servidor puede establecer su tamaño máximo de URL y devolver un error 414 en caso de que la URL enviada por el user-agent sea demasiado larga. Y luego están los propios user-agents claro. Cada uno de ellos puede “unilateralmente” truncar la URL o rechazar peticiones si la URL excede cierto tamaño.

Cuando se envía un formulario via POST se usa por defecto el content-type application/x-www-form-urlencoded que básicamente significa codificar (en el cuerpo de la petición) todos los parámetros del formulario en la codificación clásica de nombre=valor&nombre2=valor2&

Es una práctica (relativamente) habitual cuando se crean APIs REST que si alguien tiene que enviar datos a dicha API (sea a través de POST o PUT p.ej.) no codifique dichos datos usando el content-type application/x-www-formurlencoded sino que codifique dichos datos usando algún otro formato (como p. ej. JSON). Esto es por desligarnos de “los formularios” (una API no trabaja con formularios, trabaja con datos) y también por coherencia: si mi API devuelve datos en JSON lo suyo es que los acepte también en este formato.

Bien, vamos a crear un formulario de edición. Pero esta vez no haremos un formulario normal, enviado via POST a través del content-type application/x-www-form-urlencoded, sino que vamos a enviar el contenido de este formulario a través de un objeto JSON (content-type application/json) enviado por POST. Por supuesto dicho formulario será tratado por un servicio REST que crearemos con WebApi.

Primero lo haremos “a la vieja usanza” es decir usando jQuery para crear el objeto JSON y luego haremos que entre en escena knockout.

A la vieja usanza…

Primero vamos a crear el método de nuestra API que nos devuelva una cerveza según su ID. Así añadimos al controlador ApiBeersController el método GetById:

public Beer GetById(int id)
{
return new Beer {Name = "Imperial Stout #" + id, Abv = 10.0M, Ibu = 120};
}

Ahora toca crear la vista para edición. Dicha vista será retornada por el controlador Beers:

public ActionResult Edit(int? id)
{
ViewBag.BeerId = id ?? 0;
return View();
}

Bien, vayamos a lo que importa, el código de la vista:

@{
ViewBag.Title = "Edit Beer " + ViewBag.BeerId;
}

<script type="text/javascript">
$(document).ready(function () {
var uri = "@Url.RouteUrl("DefaultApi", new {httproute="", controller="ApiBeers", id=ViewBag.BeerId})";
$.getJSON(uri, function(data) {
$("#Name").val(data.Name);
$("#Abv").val(data.Abv);
$("#Ibu").val(data.Ibu);
});

$("#frm").submit(function(evt) {
// Crea el objeto JSON a partir de los datos del formulario
var beer = { };
beer.Name = $("#Name").val();
beer.Abv = $("#Abv").val();
beer.Ibu = $("#Ibu").val();
var jsonBeer = JSON.stringify(beer);
// Enviamos el objeto json
var uriEdit = "@Url.RouteUrl("DefaultApi", new {httproute="", controller="ApiBeers"})";
$.ajax({
url: uriEdit,
dataType: 'json',
contentType: 'application/json',
type: 'post',
data: jsonBeer
}).done(function(data) {
alert("Beer " + data.Id + " has been edited " + data.Status);
});

evt.preventDefault();
});
});

</script>

<h2>Edit a beer! ;-)</h2>

<form method="POST" id="frm">
<label for="Name">Name:</label>
<input type="text" name="Name" id="Name" />
<br />

<label for="Name">Abv:</label>
<input type="text" name="Abv" id="Abv" />
<br />

<label for="Name">Ibu</label>
<input type="text" name="Name" id="Ibu" />
<br />

<input type="submit" value="edit!" />
</form>

Si lo analizamos vemos que hace lo siguiente:

  1. Nada más cargarse la página, llama a la API al método de obtener una cerveza usando $.getJSON. Una vez recibe la cerveza rellena los campos del formulario.
  2. Se suscribe al evento “submit” del formulario. En dicho evento:
    1. Crea un objeto beer y lo rellena con los valores del formulario
    2. Crea la cadena en formato json de dicho objeto (usando JSON.stringify).
    3. Finalmente usa $.ajax para enviar la petición POST hacia la API de editar cervezas pasándole la cerveza a editar.
      1. Una vez la petición ajax se haya realizado muestra un alert con la información devuelta por el servidotr (que a su vez es otro objeto JSON).

Veamos la información que se manda a través de la red:

image 

Vemos tres peticiones:

  1. GET a /beers/edit/10. Es la que termina llamando la acción Edit del BeersController y devuelve la vista.
  2. GET a /api/apibeers/10. Es la que termina llamando a la acción GetById del ApiBeersController
  3. POST a /api/apibeers. Es la que termina llamando a la acción Post del ApiBeersController.

La siguiente imágen muestra la respuesta de la segunda petición, que son los datos de la cerveza en JSON que envía el servidor:

image

Y esta otra imagen muestra los datos enviados en la tercera petición (el $.ajax). Fijaos como el “request payload” es un json y como el content-type es application/json:

image

Y la respuesta recibida del servidor es el objeto JSON que devuelve la acción Post:

image

Bien, ahora vamos a mantener toda la infraestructura de servidor (no vamos a tocar ningún controlador) pero vamos a usar knockout en la vista.

Desplegamos el poder de knockout

En el post anterior vimos como usar data-bind para enlazar una propiedad de nuestro viewmodel a un elemento del DOM. Ahora básicamente vamos a hacer lo mismo. Tendremos un viewmodel con 3 propiedades (Name, Abv, Ibu) y lo enlazaremos a los 3 controles del formulario:

<form method="POST" id="frm">
<label for="Name">Name:</label>
<input type="text" name="Name" id="Name" data-bind="value: Name" />
<br />

<label for="Name">Abv:</label>
<input type="text" name="Abv" id="Abv" data-bind="value: Abv" />
<br />

<label for="Name">Ibu</label>
<input type="text" name="Name" id="Ibu" data-bind="value: Ibu" />
<br />

<input type="submit" value="edit!" />
</form>

Fijaos que ahora usamos data-bind=”value: xxx” para indicar que lo que estamos enlazando es el valor de la propiedad “value” del objeto del DOM al valor de la propiedad xxx de nuestro viewmodel.

Ahora ha llegado el momento de crear nuestro viewmodel. Pero ¡ey! recuerda que esto es un formulario de edición, eso significa que desde los controles podremos editar nuestro viewmodel. Así pues necesitamos un enlace bidireccional. Por supuesto knockout tiene soporte para estos enlaces de forma automática. Es decir, no haría falta que hicieramos nada especial (crear las propiedades en el viewmodel y listos).

Observables

Vamos a introducir un concepto nuevo: los observables. Si vienes del mundo de Silverlight o WPF debes entender que un observable de Knockout viene a ser lo mismo que una propiedad que implemente INotifyPropertyChanged, es decir la UI se enterará de los cambios de dicha propiedad. Insisto en que para que una propiedad del viewmodel sea editable a partir de un control no es necesario que sea un observable. Un observable es solo para que la UI se entere de las modificaciones del viewmodel.

Este es el código para crear nuestro viewmodel a partir de los datos devueltos por la acción GetById de ApiBeersController:

var uri = "@Url.RouteUrl("DefaultApi", new {httproute="", controller="ApiBeers", id=ViewBag.BeerId})";
$.getJSON(uri, function(data) {
vm = {
Name : ko.observable(data.Name),
Ibu : ko.observable(data.Ibu),
Abv : ko.observable(data.Abv)
};
ko.applyBindings(vm);
});

Fijaos en el uso de ko.observable para crear una propiedad observable (el parámetro es el valor inicial). Realmente en este momento Name, Ibu y Abv ya no son variables miembro del viewmodel. Ahora son funciones:

image

Para enviar los datos en el servidor no hay mucha diferencia respecto al caso anterior:

$("#frm").submit(function(evt) {
// Crea el objeto JSON a partir de los datos del formulario
var jsonBeer = JSON.stringify({
Name : vm.Name(),
Ibu : vm.Ibu(),
Abv : vm.Abv()
});
// Enviamos el objeto json
var uriEdit = "@Url.RouteUrl("DefaultApi", new {httproute="", controller="ApiBeers"})";
$.ajax({
url: uriEdit,
dataType: 'json',
contentType: 'application/json',
type: 'post',
data: jsonBeer
}).done(function(data) {
alert("Beer " + data.Id + " has been edited " + data.Status);
});
evt.preventDefault();
});

Creamos la representación en JSON del viewmodel. Fijaos que dado que Name, Ibu y Abv son realmente funciones, no puedo serializar el viewmodel directamente a JSON, en su lugar creo un objeto intermedio, con las propiedades Name, Ibu y Abv invocando a las funciones del viewmodel para obtener el valor actual. Y el resto ya es el mismo código de antes.

Aunque a priori no haya mucha diferencia en cuanto a “la cantidad” de código, hay una diferencia conceptual enorme. En el caso en que usábamos jQuery el código javascript estaba muy atado al DOM. Los datos con los que trabajábamos estaban dispersos (en #Name, en #Abv, …). Ahora en este segundo caso el código javascript es totalmente ignorante del DOM y trabaja tan solo con el viewmodel. Separación de responsabilidades.

¿Y ese submit?

Incluso ahora aunque estemos “aislados” del DOM, seguimos gestionando una parte de nuestro código con eventos vinculados a éste: en concreto estamos suscritos al evento “submit” de un objeto del DOM (el formulario).

¿Nos puede ayudar knockout aquí? Pues claro 🙂

El patrón MVVM también recomienda que el código de interacción esté en el viewmodel. Es decir evitar tener código que no sea exclusivo de presentación en las vistas. En el mundo WPF y Silverlight eso se traduce en evitar el code-behind y enlazar los controles del XAML con comandos que se ejecutan en el viewmodel. Pues knockout nos ofrece algo parecido: ¡Podemos enlazar eventos del DOM a acciones de nuestro viewmodel!

Para empezar le indicamos al formulario que hay un enlace a través del evento submit:

<form method="POST" id="frm" data-bind="submit: editBeer">

Fijaos de nuevo en el uso de data-bind. Ahora enlazamos el evento submit con la función editBeer del viewmodel. Esto es una de las cosas que más me gustan de Knockout: la sintaxis para establecer todos los bindings es muy simple (hemos establecido bindings de texto, bindings bidireccionables y ahora nos enlazamos a una función con la misma sintaxis).

¿Y como es esta función editBeer? Pues bueno es una función de nuestro viewmodel que tiene básicamente el mismo código que teníamos antes dentro del evento “submit” que gestionábamos con jQuery:

vm = {
Name : ko.observable(data.Name),
Ibu : ko.observable(data.Ibu),
Abv : ko.observable(data.Abv),
editBeer : function() {
var jsonBeer = JSON.stringify({
Name : this.Name(),
Ibu : this.Ibu(),
Abv : this.Abv()
});
// Enviamos el objeto json
var uriEdit = "@Url.RouteUrl("DefaultApi", new {httproute="", controller="ApiBeers"})";
$.ajax({
url: uriEdit,
dataType: 'json',
contentType: 'application/json',
type: 'post',
data: jsonBeer
}).done(function(data) {
alert("Beer " + data.Id + " has been edited " + data.Status);
});
}
};

¡Fantástico! Ahora ya NO tenemos nada fuera de nuestro viewmodel. Hemos establecido una separación total entre la vista (el HTML) y el viewmodel (el objeto vm) que mantiene no solo los valores que tiene la vista, si no también gestiona los comandos que la vista puede realizar.

Puede seguir pareciendo que en cuanto a “cantidad” de código javascript no hemos mejorado mucho respecto a la versión inicial que no usaba knockout, pero es indudable que a nivel de claridad y mantenibilidad nuestro código es ahora mucho, mucho, mucho mejor.

Espero que os haya resultado interesante.

¡En el siguiente post seguiremos explorando las maravillas de knockout!

PD: He dejado el código (VS2012 RC) del proyecto (KoDemo1) que se corresponde a este post. En la carpeta Views/Beers vereis que hay una Edit.cshtml y otra Edit-no-ko.cshtml. La segunda contiene el código sin usar knockout.