Y el combate se decidió por KO (ii)

Publicado 1/8/2012 13:36 por Eduard Tomàs i Avellana

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.

Comparte este post:

Comentarios

# re: Y el combate se decidió por KO (ii)

Saturday, August 4, 2012 9:45 PM by Omar del Valle Rodríguez

Que buena pinta tiene esto... :)

¿Le ves aplicación en vistas q solo muestran datos?

Salu2

# re: Y el combate se decidió por KO (ii)

Sunday, August 5, 2012 12:45 PM by Eduard Tomàs i Avellana

Buenas Omar!

Depende del paradigma de aplicación web. Si tus vistas que muestran datos son generadas en servidor, entonces no tiene mucho sentido usar knockout la verdad.

Ahora bien, si tus vistas muestran datos que recogen via ajax de un servicio (usualmente en json) entonces si que usar knockout te facilitará la vida.

De hecho el uso de knockout se justifica en cuanto los datos de nuestras vistas provienen via ajax. Así en lugar de la forma "de siempre" de cojer los datos y "rellenar el DOM", usando knockout especificamos los enlaces de forma declarativa, creamos el viewmodel y listos. Ten presente también que knockout puede crear DOM si es necesario (algo que vislumbramos en el primer post de esta serie pero que veremos más en profunidad luego).

Un saludo y gracias por el comentario! ;-)

# re: Y el combate se decidió por KO (ii)

Sunday, August 5, 2012 7:48 PM by Omar del Valle Rodríguez

Gracias Eduard!!...  

imagino q igual se puedan mezclar los dos modelos sin problema si la página requiere parte de los datos generados en el servidor y otros que no.

A la espera de más..

Un salu2

# POO–Responsabilidades

Wednesday, August 8, 2012 3:43 PM by Omar del Valle Rodríguez

“Hay una diferencia entre programar orientado a objetos y pensar orientado a objetos” Debatiendo hoy

# re: Y el combate se decidió por KO (ii)

Monday, October 21, 2013 2:33 PM by Kiquenet

Entre tantos frameworks, cuál sería la elección adecuada: Angular.js vs Knockout.js vs Backbone.js ?

Además que luego hay muchos más.

Saludos.

# hair spray

Monday, November 10, 2014 7:22 PM by hair spray

Y el combate se decidi� por KO (ii) - Burbujas en .NET