Tutorial jQuery Mobile + Knockout (III): Definiendo el ViewModel con Knockout

Este post forma parte de una serie de cuatro:

Definiendo el ViewModel

Después las dos primeras partes de este tutorial en las que presentamos jQuery Mobile y Knockout y creamos las vistas con jQuery Mobile ha llegado el momento de definir el ViewModel de nuestra pequeña aplicación.

En la primera parte del tutorial explicaba que el ViewModel es el encargado de adaptar el modelo de la aplicación (el dominio en terminología DDD) a las necesidades de la vista. Además contiene toda la lógica relacionada con el interfaz de usuario, como por ejemplo cuándo habilitar un control o qué hacer cuando se pulsa un botón.

Teniendo clara esa idea, veamos cómo debería ser el ViewModel de nuestra aplicación. Recordemos primero cómo era W.I.Z.A.R&D:

Weapon Inventory for Zombie Apocalypse Research & Development

Pantallas de W.I.Z.A.R&D

Para cada pantalla de la aplicación tendremos un ViewModel diferente. Esto no es estrictamente necesario, aunque suele ser lo más habitual para evitar crear un único ViewModel excesivamente complejo.

Definiendo el ViewModel

En esta pequeña aplicación, necesitamos poder cubrir las siguientes funcionalidades:

  • Mostrar una lista con todas las armas.
  • Mostrar el peso acumulado de todas las armas.
  • Editar el arma sobre el que pulsamos.
  • Eliminar el arma sobre el que pulsamos.
  • Añadir un nuevo arma

Para ello, vamos a montar la siguiente estructura de ViewModels:

ViewModels de W.I.Z.A.R&D

ViewModels de W.I.Z.A.R&D

Tendremos un MainViewModel que servirá como ViewModel principal de la aplicación. Desde él se expondrán las propiedades y métodos necesarios para la pantalla con la lista de armas.

Para la edición de un arma, se usara EditWeaponViewModel, que incluye toda la información asociada a un arma. Por mantener el ejemplo sencillo, vamos a usar instancias de EditWeaponViewModel para almacenar en MainViewModel la lista de armas. En un caso más real, lo lógico sería usar distintos tipos de objetos, ya que para mostrar la lista no necesitamos toda la información que aparece en EditWeaponViewModel y, en el momento en que se vaya empezar a editar un arma, se podría acceder al servidor para descargar toda la información necesaria.

Por último, para añadir un arma usaremos NewWeaponViewModel.

El código usado para implementar todo esto es el siguiente:

function MainViewModel() {

	var self = this;
	
	self.weapons = ko.observableArray();
	self.weapons.push(new EditWeaponViewModel('Martillo', 1, 'Ilimitada', 4, 0));
	self.weapons.push(new EditWeaponViewModel('Machete', 1, 'Ilimitada', 5, 0));
	self.weapons.push(new EditWeaponViewModel('Pistola', 1, 'Frecuente', 0, 3));
	self.weapons.push(new EditWeaponViewModel('Carabina', 3, 'Normal', 0, 4));

	self.newWeapon = new NewWeaponViewModel(self.weapons);

	self.selectedWeapon = ko.observable();
	
	self.remove = function() {
		// Aquí, knockout hace que "this" sea un EditWeaponViewModel
		self.weapons.remove(this);
	};
	
	self.edit = function() {
		// Aquí, knockout hace que "this" sea un EditWeaponViewModel
		self.selectedWeapon(this);
	};
	
	self.totalWeight = ko.computed(function() {
		var total = 0;
		ko.utils.arrayForEach(self.weapons(), function(w) {
			total = total + w.weight();
		});
		return total;
	}, this);
};

function EditWeaponViewModel(name, weight, ammoType, closeCombatRating, rangeRating) {

	this.name = ko.observable(name);
	this.weight = ko.numericObservable(weight || 0);
	this.ammoType = ko.observable(ammoType || 'Ilimitada');
	this.closeCombatRating = ko.observable(closeCombatRating || 0);
	this.rangeRating = ko.observable(rangeRating || 0);
	this.ammoTypes = ['Ilimitada', 'Frecuente', 'Normal', 'Rara'];
};

function NewWeaponViewModel(existingWeapons) {

	var self = this;
	self.name = ko.observable('');

	this.clear = function() {
		self.name('')
	};

	this.add = function() {
		existingWeapons.push(new EditWeaponViewModel(self.name()));
		self.clear();
	};
}

Pese a lo sencillo del ejemplo, nos permite ver unos cuantos conceptos básicos a la hora de crear modelos con Knockout:

  • Un ViewModel es simplemente una función usada para construir objetos en Javascript (me resisto a llamarlo clase porque no lo es).
  • Las propiedades que van a enlazarse usando data biding se encapsulan en objetos ko.observable(). Sería el equivalente a implementar INotifyPropertyChanged en .NET, ya que permite a Knockout detectar los cambios en la propiedad para aplicarlos a los controles a los que está enlazada. Hay que tener en cuenta que dejan de ser propiedades “normales” y deben usarse de forma un poco especial:
    • Para asignarles un valor: model.name('ruperta')
    • Para leer el valor que contienen: var n = model.name()
  • Para definir una lista que queremos enlazar con data binding, debemos encapsularla en un objeto ko.observableArray(), que no es más que un observable que encapsula un array. El equivalente en .NET sería INotifyCollectionChanged.
  • Podemos tener propiedades observables que dependen de otras propiedades, como totalWeight. Para definirlas se usa la función ko.computed(), que en su versión más simple recibe como parámetro la función usada para el cálculo del valor de la propiedad. Knockout detecta automáticamente el grafo de dependencias para actualizar el control asociado a esta propiedad cuando sea necesario (en este caso, cada vez que cambia el array de armas o el peso de un arma).
  • Las acciones que se pueden realizar quedan representadas con métodos del ViewModel, como los casos de edit y remove. Es importante tener en cuenta que, al usar estos métodos asociados al binding de una lista, Knockout es lo bastante listo como para invocarlos sobre el elemento actual de la lista. Simplificando, que al invocar el método edit o remove, this será una referencia al elemento de weapons que vamos a eliminar.

Es también importante destacar que este código no tiene ninguna dependencia sobre la vista, no referencia ningún elemento html ni utiliza APIs relacionadas directamente con la vista. Esto más que una característica de Knockout es algo genéricos al patrón MVVM, pero merece la pena recordarlo.

Un aspecto especialmente interesante del código anterior es la forma en que se declara el peso (weight) de cada arma. Por defecto, al enlazar una propiedad a un elemento html, Knockout va a realizar el enlace como si fueran strings. Sin embargo, el peso queremos almacenarlo como un float para que luego se pueda sumar correctamente en la propiedad calculada totalWeight.

Para cambiar la forma en que se enlaza una propiedad se puede definir un nuevo tipo de observable, que en este caso llamaremos ko.numericObservable:

ko.numericObservable = function(initialValue) {
	var _actual = ko.observable(initialValue);

	return ko.computed({
		read: function() {
			return _actual();  
		},
		write: function(newValue) {
			var parsedValue = parseFloat(newValue);
			_actual(isNaN(parsedValue ) ? newValue: parsedValue);
		} 
	});
};

Estamos añadiendo al objeto ko una nueva función, numericObservable que internamente se encarga de crear una propiedad calculada, similar a la del peso total que vimos un poco más arriba, pero que además hace la conversión de string a float al escribir sobre ella, permitiéndonos así mantener su valor interno como float.

Podríamos haber hecho esto dentro del ViewModel, exponiendo una propiedad formattedWeight y traduciendo en el propio ViewModel, pero esta solución resulta más elegante y reutilizable. Otra alternativa (quizá más correcta) hubiera sido emplear un custom binding, pero como eso lo veremos para otras cosas en el próximo post, he preferido usar este método para ver más opciones.

Próximamente…

Ya tenemos creadas las vistas y el ViewModel de la aplicación, pero todavía nos falta engancharlo todo para que la aplicación haga algo interesante. En la próxima parte de este tutorial veremos como hacer esto y cómo solucionar algunos problemas que surgen al mezclar jQuery Mobile con Knockout, pero no nos adelantemos.

De todas formas, si quieres ver el código completo del tutorial lo puedes encontrar ya en su repositorio de github o acceder directamente a la demo online de W.I.Z.A.R&D.


4 comentarios en “Tutorial jQuery Mobile + Knockout (III): Definiendo el ViewModel con Knockout

  1. Enhorabuena por el ejemplo. Si es posible una consulta. Todo se implenta en el index.html en una pagina interna y los datos que añades se van guardando. Si se introduce en una aplicación con paginas externas, los datos se pierden al pasar de una a otra. Como se puede solucionar?.
    Muchas gracias.

  2. Tendría que revisarlo, pero creo recordar que si usas jQueryMobile y navegas entre páginas usando $.mobile.changePage(), las páginas “externas” las va a crear como div’s dentro de la página inicial, por lo que los datos seguirían siempre accesibles.

    No obstante, en una aplicación real, los datos no estarían almacenados en la propia página porque se perderían al recargarla. En ese caso, podrías usar websql (para almacenar en el lado del cliente) o almacenar directamente en un servidor y comunicar las páginas a través de él. Algo así como:

    - Cuando en la página de editar se terminan de hacer cosas, su ViewModel envía el resultado de los cambios al servidor.

    - Cuando se activa la página de la lista, lo primero que hace es refrescar los elementos de la lista con una petición al servidor.

  3. Pingback: Tutorial jQuery Mobile + Knockout (y IV): Configurando el Data Binding « Koalite's blog

  4. Pingback: Tutorial jQuery Mobile + Knockout (II): Creando las vistas con jQuery Mobile « Koalite's blog

Deja un comentario

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

*

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>