Con la aparición de Windows 8 Consumer Preview Microsoft ha presentado Visual Studio 2012 para desarrollar aplicaciones Metro. En este artículo hablaremos de cómo se produce la navegación de contenido en una aplicación de Metro hecha en HTML / JavaScript.
Navegando en HTML
Cuando creamos una aplicación en HTML tradicional, el método de navegación es incluir un enlace <a> para poder empezar a navegar. Lo malo que tiene este método es que durante un breve periodo de tiempo la ventana del navegador permanecerá en blanco a la espera del contenido del nuevo HTML al que se está navegando. Esto puede resultar muy molesto en algunas ocasiones y aunque se tenga una conexión a Internet muy rápida puede parecer que la página parpadea por un instante.
Para este tipo de problemas se invento AJAX, que permite modificar selectivamente una sección de una página sin que se refresque la página entera. Esta funcionalidad es ideal para bajar datos de internet y luego “conectar” esos datos con el árbol de objetos en HTML.
Aplicaciones Metro con HTML
Sabiendo estas dos cosas tenemos que hacer que nuestras aplicaciones Metro no naveguen hasta un HTML nuevo, sino que tengamos un mecanismo para poder cargar y descargar contenido del DOM para simular la navegación. Gracias a las nuevas API’s que Microsoft ha incluido en WinJS (la parte JavaScript de WinRT) podemos hacer esto de manera muy sencilla.
Empezaremos echando un vistazo a la plantilla de nuevo proyecto de aplicación Metro para HMTL / JavaScript.
Como podemos ver en la captura del menú de nuevo proyecto de Visual Studio 11, tenemos varias plantillas de diferentes tipos de proyecto. Ahora mismo nos centraremos en el tipo de proyecto “Navigation Application”. Llamaremos a nuestra aplicación NavigationDemo.
Como se puede ver en esta captura de pantalla, estos son los ficheros que se crean por defecto en esta plantilla. Ahora vamos a proceder a ver como se produce la navegación.
Primera aplicación Metro
La aplicación que vamos a desarrollar es muy sencilla. Simplemente navega de una pieza de contenido a otra, pero utilizando las APIs que tenemos en WinRT para hacer la navegación.
La primera página Default.html
La primera página que se ejecuta en una aplicación Metro es default.html, así que ese es nuestro punto de entrada, para poder empezar a entender la navegación en Metro.
Si nos fijamos en el código fuente de default.html, tenemos lo siguiente:
1: <!DOCTYPE html>
2: <html>
3: <head>
4: <meta charset="utf-8">
5: <title>NavigationDemo</title>
6:
7: <!-- WinJS references -->
8: <link href="//Microsoft.WinJS.0.6/css/ui-dark.css" rel="stylesheet">
9: <script src="//Microsoft.WinJS.0.6/js/base.js"></script> 1:
2: <script src="//Microsoft.WinJS.0.6/js/ui.js">
1: </script>
2:
3: <!-- NavigationDemo references -->
4: <link href="/css/default.css" rel="stylesheet">
5: <script src="/js/default.js">
1: </script>
2: <script src="/js/navigator.js">
</script>
10: </head>
11: <body>
12: <div id="contenthost"
13: data-win-control="NavigationDemo.PageControlNavigator"
14: data-win-options="{home: '/html/homePage.html'}"></div>
15: <!-- <div id="appbar" data-win-control="WinJS.UI.AppBar">
16: <button data-win-control="WinJS.UI.AppBarCommand" data-win-options="{id:'cmd', label:'Command', icon:'placeholder'}"></button>
17: </div> -->
18: </body>
19: </html>
Vemos que es HTML normal, de toda la vida, solamente que en la cabecera del documento tenemos unas referencias de unos ficheros JavaScript y hojas de estilos un poco especiales. Me refiero a esto:
1: <script src="//Microsoft.WinJS.0.6/js/base.js"></script>
Si nos fijamos, con esta acción estamos referenciando la API de WinJS, que es la librería de JavaScript para el desarrollo de aplicaciones Metro.
Un poco más abajo tenemos dos ficheros JS y un fichero CSS referenciados. Estos ficheros forman parte de la lógica de la aplicación.
La cosa cambia cuando nos vamos al cuerpo de nuestra página html, donde vemos que tenemos únicamente un div, pero con unos atributos que no habíamos visto antes:
1: <div id="contenthost"
2: data-win-control="NavigationDemo.PageControlNavigator"
3: data-win-options="{home: '/html/homePage.html'}"></div>
Tenemos por un lado el identificador del control contenthost que según el nombre podemos adivinar que será el hueco donde después pondremos el contenido por el que estemos navegando.
También vemos que el siguiente atributo, data-win-control, tiene como valor algo que parece una clase. Si recordáis el nombre del proyecto (NavigationDemo) vemos el valor del atributo es NavigationDemo.PageControlNavigator, lo que me indica que la clase se llama PageControlNavigator. La primera reacción a este atributo es pensar que, al estar en JS no tenemos clases, pero podemos saber por XAML que esto se parece mucho a nuestra MainPage.
El último atributo que nos queda por investigar es, data-win-options. Si nos fijamos en el contenido del mismo, vemos que es un objeto de JS que tiene una propiedad llamada home con un valor que es una ruta html relativa del proyecto. Todo parece indicar que es la primera página que se va a cargar, como efectivamente podemos comprobar si abrimos el fichero homePage.html, que mostrará justamente la primera captura de pantalla que hemos visto antes.
Pero, ¿Cómo se ha realizado esta navegación? Es el momento de entrar a hablar sobre JavaScript.
JavaScript
El primer fichero que vamos a abrir es el js/default.js, que es el punto de entrada de la aplicación.
Un detalle a destacar sobre los ficheros de JavaScript es que todos están envueltos en una función sin nombre, que se ejecuta tan pronto como se define.
1: (function () {
2: })();
El motivo de proceder de esta forma es aislar el ámbito global de JavaScript y así evitar problemas de colisión de nombres de objetos y funciones. No definir todas las variables en el espacio de nombre global es una buena práctica.
Lo siguiente que nos sorprende es que tenemos una sentencia que no habíamos visto antes (“use strict”), que indica al compilador y al runtime que hagan una verificación estricta de tipos, como si se tratase de un lenguaje fuertemente tipado. Esto, por supuesto, se realiza cuando se ejecuta la aplicación, pues no hay proceso de compilación como tal.
Después de esto el fichero default.js simplemente establece dos callbacks para el evento “onactivated” y “oncheckpoint” y luego llama a WinJS.Application.start();
Navegación
La verdadera magia del código no reside en este fichero default.js, sino en el fichero navigator.js. Veamos cómo está definido.
Lo primero que nos encontramos en la definición del fichero es la función sin nombre que envuelve todo el código. A partir de ahora la obviaremos para no ser redundantes.
En el cuerpo de la definición de la función vemos como se definen una serie de variables que son accesos directos a propiedades del código, como por ejemplo:
- Windows.UI.ViewManagement.ApplicationView
- Windows.Graphics.Display.DisplayProperties
- WinJS.Navigation
- WinJS.UI
- WinJS.Utilities
Todo lo que empiece por WinJS, está definido en el fichero base.js, que forma parte de la referencia de “Microsoft Windows Library for JavaScript SDK” que acompaña al proyecto. El fichero base.js está perfectamente formateado y documentado, así que podremos consultar como está hecho pero no podremos modificarlo desde esta localización.
Las variables que empiezan por Windows hacen referencia a la API de WinRT, a la que podemos acceder desde JavaScript. De este modo, cuando nos referimos a Windows.Graphics.Display.DisplayProperties estamos hablando de una clase que podremos utilizar con C# y C++ (los otros dos lenguajes permitidos en WinRT).
A continuación, nos encontramos con dos de las funcionalidades que mejorarán la calidad del código que generemos en JavaScript: espacios de nombres y Classes. Veamos de qué se trata antes de continuar.
WinJS.Namespace
Este objeto, como su nombre indica, nos permite definir espacios de nombres para su uso dentro de una aplicación Metro. ¿A qué nos referimos con espacios de nombre?¿A un espacio de nombres tradicional? Los programadores de C# podemos pensar que estamos definiendo un espacio de nombre de la misma manera que lo definimos en este lenguaje, pero desde luego no estamos en un entorno de .NET. Lo que realmente estamos haciendo es definir una serie de objetos que después vamos a poder utilizar con una nomenclatura de espacio de nombres. Es decir, que vamos a poder hacer definiciones de objetos de forma parecida a como hace Microsoft con, por ejemplo, WinJS.Navigation, que también forma parte de espacio de nombres de WinJS.
A la hora de definir un espacio de nombres en JavaScript, utilizando WinJS emplearemos una de las dos funciones que vienen en WinJS: define o defineWithParent.
Define
Si queremos definir un segmento del espacio de nombres de nuestra aplicación, en nuestro caso NavigationDemo.PageControlNavigator, tendremos que declarar la base del espacio de nombres como todos los segmentos menos el último, en este caso, NavigationDemo, pero podríamos tener espacios de nombres más grandes.
Una vez definido el string del espacio de nombres base como primer parámetro, el segundo parámetro de la función ha de ser un objeto que contenga el último segmento del espacio de nombres correspondiente a la clase en cuestión.
El ejemplo quedaría así:
1: WinJS.Namespace.define("DemoNamespace", {
2: Class1: {},
3: Class2: {}
4: });
De esta manera podemos tener organizado nuestro código dentro de nuestra aplicación por espacio de nombres. Realmente no son espacios de nombres en el sentido tradicional del lenguaje C# (al que más estamos acostumbrados), sino que son objetos definidos de esa manera para dar sensación de jerarquía cuando se utilizan.
Otra función que podemos utilizar es defineWithParent que nos permite extender un espacio de nombres ya existente.
Una vez comprendido esto, el siguiente paso es definir clases para empezar a escribir la funcionalidad de nuestro código.
WinJS.Class
La definición de clases es otro aspecto importante de la programación en JavaScript. Como bien es sabido, no hay clases como tales en JavaScript, pero en WinJS podemos hacer que una función tenga el aspecto de una clase.
Según Microsoft las clases en WinJS tienen tres características:
-
Constructor: es una función que nos permite inicializar la clase en cuestión, nosotros no somos responsables de devolver this en la definición porque WinJS lo hace automáticamente por nosotros.
-
Métodos de instancia: es un objeto que contiene los métodos de instancia que vamos a utilizar en la definición de la clase. No hay descriptores de visibilidad en JavaScript así que todos lo métodos son públicos.
-
Métodos estáticos: son métodos que se pueden utilizar sin necesidad de crear una instancia de la clase directamente escribiendo el nombre de la clase.
Así es como quedaría la definición de una clase con WinJS.Class.
1: WinJS.Class.define(
2: function (argum1) { },
3: {},
4: {}
5: );
Herencia y mixing
A la hora de definir clases también es posible definir herencia de clases. Como hasta ahora esto no es herencia tradicional como la entendemos en C#, sino que simplemente se define como una mezcla de los métodos que han definido en las clase base más los métodos de la clase hija. Para ello utilizaremos el método WinJS.Class.derive.
La última de las opciones es una mezcla (mix), que consiste en coger dos objetos que no tienen ninguna relación y hacer una unión de los dos en una nueva definición de clase.
Navigator.js
Ahora ha llegado el momento de hablar sobre la clase más importante de todo el proyecto, navigator.js. Como vimos anteriormente, esta clase se utilizaba en el fichero default.html para hacer la navegación de esa página hasta homePage.html. Veamos ahora cómo se realiza esa navegación.
Definición de los objetos más usados
Al principio del fichero podemos ver que se definen una serie de propiedades con objetos que vamos a utilizar durante el desarrollo.
1: var appView = Windows.UI.ViewManagement.ApplicationView;
2: var displayProps = Windows.Graphics.Display.DisplayProperties;
3: var nav = WinJS.Navigation;
4: var ui = WinJS.UI;
5: var utils = WinJS.Utilities;
Una vez definidos estos objetos lo siguiente que nos encontramos es directamente la definición de clase.
1: (function () {
2: "use strict";
3:
4: var appView = Windows.UI.ViewManagement.ApplicationView;
5: var displayProps = Windows.Graphics.Display.DisplayProperties;
6: var nav = WinJS.Navigation;
7: var ui = WinJS.UI;
8: var utils = WinJS.Utilities;
9:
10: WinJS.Namespace.define("NavigationDemo", {
11: PageControlNavigator: WinJS.Class.define(
12: // Define the constructor function for the PageControlNavigator.
13: function (element, options) {
14: this.element = element || document.createElement("div");
15: this.element.appendChild(this._createPageElement());
16:
17: this.home = options.home;
18:
19: nav.onnavigated = this._navigated.bind(this);
20: appView.getForCurrentView().onviewstatechanged = this._viewstatechanged.bind(this);
21:
22: document.body.onkeyup = this._keyupHandler.bind(this);
23: document.body.onkeypress = this._keypressHandler.bind(this);
24: nav.navigate(this.home);
25: }, {
26: // This function creates a new container for each page.
27: _createPageElement: function () {
28: var element = document.createElement("div");
29: element.style.width = "100%";
30: element.style.height = "100%";
31: return element;
32: },
33:
34: // This function responds to keypresses to only navigate when
35: // the backspace key is not used elsewhere.
36: _keypressHandler: function (eventObject) {
37: if (eventObject.key === "Backspace")
38: nav.back();
39: },
40:
41: // This function responds to keyup to enable keyboard navigation.
42: _keyupHandler: function (eventObject) {
43: if ((eventObject.key === "Left" && eventObject.altKey) || (eventObject.key === "BrowserBack")) {
44: nav.back();
45: } else if ((eventObject.key === "Right" && eventObject.altKey) || (eventObject.key === "BrowserForward")) {
46: nav.forward();
47: }
48: },
49:
50: // This function responds to navigation by adding new pages
51: // to the DOM.
52: _navigated: function (eventObject) {
53: var newElement = this._createPageElement();
54: var parentedComplete;
55: var parented = new WinJS.Promise(function (c) { parentedComplete = c; });
56:
57: var that = this;
58: WinJS.UI.Pages.render(eventObject.detail.location, newElement, eventObject.detail.state, parented).
59: then(function (control) {
60: that.element.appendChild(newElement);
61: that.element.removeChild(that.pageElement);
62: parentedComplete();
63: document.body.focus();
64: that.navigated();
65: });
66: },
67:
68: // This function is called by _viewstatechanged in order to
69: // pass events to the page.
70: _updateLayout: {
71: get: function () { return (this.pageControl && this.pageControl.updateLayout) || function () { }; }
72: },
73:
74: _viewstatechanged: function (eventObject) {
75: (this._updateLayout.bind(this.pageControl))(this.pageElement, eventObject.viewState);
76: },
77:
78: // This function updates application controls once a navigation
79: // has completed.
80: navigated: function () {
81: // Do application specific on-navigated work here
82: var backButton = this.pageElement.querySelector("header[role=banner] .win-backbutton");
83: if (backButton) {
84: backButton.onclick = function () { nav.back(); };
85:
86: if (nav.canGoBack) {
87: backButton.removeAttribute("disabled");
88: }
89: else {
90: backButton.setAttribute("disabled", "disabled");
91: }
92: }
93: },
94:
95: // This is the PageControlNavigator object.
96: pageControl: {
97: get: function () { return this.pageElement && this.pageElement.winControl; }
98: },
99:
100: // This is the root element of the current page.
101: pageElement: {
102: get: function () { return this.element.firstElementChild; }
103: }
104: }
105: ),
106:
107: // This function navigates to the home page which is defined when the
108: // control is created.
109: navigateHome: function () {
110: var home = document.querySelector("#contenthost").winControl.home;
111: var loc = nav.location;
112: if (loc !== "" && loc !== home) {
113: nav.navigate(home);
114: }
115: },
116: });
117: })();
Constructor
En el constructor de la clase se realizan varias tareas para definir la navegación.
1: function (element, options) {
2: this.element = element || document.createElement("div");
3: this.element.appendChild(this._createPageElement());
4:
5: this.home = options.home;
6:
7: nav.onnavigated = this._navigated.bind(this);
8: appView.getForCurrentView().onviewstatechanged = this._viewstatechanged.bind(this);
9:
10: document.body.onkeyup = this._keyupHandler.bind(this);
11: document.body.onkeypress = this._keypressHandler.bind(this);
12: nav.navigate(this.home);
13: }
Vemos que la función tiene dos parámetros, element y options, que son justamente el elemento host de la navegación, en este caso un div, y un objeto con las opciones, respectivamente. Si recordamos la definición del html, había un atributo que se llamaba data-win-options, cuyo valor es “{home: ‘/html/homePage.html’}” que justamente es el objeto en el que, por convención, se especifica la página de inicio.
Una vez que se tiene la referencia del elemento host, en caso de que element sea undefined, se crea un div nuevo. A continuación guardaremos el valor de options.home en una propiedad llamada home en nuestra clase. El siguiente paso es suscribirse al evento onnavigated del objeto nav que, si recordamos de la definición de variables del principio, es WinJS.Navigation.
No obstante, si nos fijamos en esa línea de código, “nav.onnavigated = this._navigated.bind(this);” veremos que no se asigna directamente el valor de “this._navigated” a “nav.onnavigated”, sino que se obtiene la referencia de la función this._navigated y se llama al método bind pasándole como parámetro this. Esto se hace así porque tenemos que recordar que en JavaScript el contexto de this no se guarda en la llamada así que cuando se ejecute el método _navigated como resultado de la navegación, this en ese momento no será el mismo this que estamos usando en el constructor del objeto. Por tanto, con esta línea de código lo que pretendemos es guardar el contexto de this y luego utilizarlo cuando se llame a la función _navigated. Este comportamiento (bind) está definido en WinJS.
Un poco más abajo en la definición del constructor podemos ver como se llama al método nav.navigate(this.home), que ejecuta la navegación en sí.
Método de instancia
Ahora viene la parte donde se hace el trabajo de la navegación en sí: obtener el html de la página de destino y adjuntarlo al DOM de la página principal. Todo ello se hace en el método _navigated.
1: _navigated: function (eventObject) {
2: var newElement = this._createPageElement();
3: var parentedComplete;
4: var parented = new WinJS.Promise(function (c) { parentedComplete = c; });
5:
6: var that = this;
7: WinJS.UI.Pages.render(eventObject.detail.location, newElement, eventObject.detail.state, parented).
8: then(function (control) {
9: that.element.appendChild(newElement);
10: that.element.removeChild(that.pageElement);
11: parentedComplete();
12: document.body.focus();
13: that.navigated();
14: });
15: }
Aquí se llama al método WinJS.UI.Pages.render que se encarga de capturar el html definido en la página de destino y cargar los ficheros que se han definido en la cabecera, JavaScript y CSS, quedándose únicamente con el contenido de la etiqueta <body>, normalmente un div. Una vez que se ha completado el proceso y usando otra funcionalidad principal de WinJS, las promesas (promises), se ejecutará el método que hay definido después del then. A grosso modo. Las promesas se utilizan en WinJS para enlazar métodos asíncronos para su ejecución, aunque su definición es mucho más extensa y queda fuera del ámbito de este artículo.
Cuando el código html ya está limpio y listo para ser procesado se llama a ese método definido en el then. Sin embargo, antes de esto debemos observar que en la línea anterior se hace algo un poco raro a primera vista: var that = this, que es definir una variable que se llama that asignándole el contenido de this, lo que está relacionado con lo que hemos comentado antes de que en JavaScript los contextos no se guardan entre llamadas. De esta forma, como en la función anónima definida en el then vamos a usar esta referencia tenemos que guardarla antes.
Ya tenemos todo lo que necesitamos para poder añadir al DOM el nuevo control y quitar el anterior, hacer foco en el body recién creado y llamar al método that.navigated() que comprueba si se tiene que mostrar el botón de atrás en la interfaz de usuario de la aplicación.
Navegación
Como hemos visto, la navegación de la aplicación se define íntegramente en este método _navigated, es que el responsable de añadir el control (que WinJS formatea por nosotros) y añadirlo al DOM, quitando previamente el anterior que pudiera existir. WinJS es el framework en el que definimos las clasesy los espacios de nombres, así como el encargado de cargar las páginas html, los scripts de JavaScript y las hojas de estilos.
¿Cómo se genera una página en WinJS?
Otro aspecto importante del desarrollo de aplicaciones de Windows 8 son las propias páginas en sí. Hasta ahora hemos visto solamente cómo se realiza la navegación en una aplicación JavaScript de Windows 8, pero no hemos visto como se hacen las páginas a las cuales se navega. Para eso Visual Studio tiene una plantilla en el menú de nuevo elemento:
Ese elemento es Page Control, que nos genera una página HTML con las referencias de WinJS y un fichero JavaScript y CSS.
HTML
El HTML que genera la plantilla no tiene nada especial, teniendo en cuenta que hemos visto ya como referenciar WinJS en HTML.
1: <!DOCTYPE html>
2: <html>
3: <head>
4: <meta charset="utf-8">
5: <title>pagecontrol</title>
6:
7: <!-- WinJS references -->
8: <link href="//Microsoft.WinJS.0.6/css/ui-dark.css" rel="stylesheet">
9: <script src="//Microsoft.WinJS.0.6/js/base.js"></script> 1:
2: <script src="//Microsoft.WinJS.0.6/js/ui.js">
1: </script>
2:
3: <link href="pagecontrol.css" rel="stylesheet">
4: <script src="pagecontrol.js">
</script>
10: </head>
11: <body>
12: <div class="pagecontrol fragment">
13: <header aria-label="Header content" role="banner">
14: <button class="win-backbutton" aria-label="Back" disabled></button>
15: <h1 class="titlearea win-type-ellipsis">
16: <span class="pagetitle">Welcome to Second Page!</span>
17: </h1>
18: </header>
19: <section aria-label="Main content" role="main">
20: <p>Content goes here.</p>
21: </section>
22: </div>
23: </body>
24: </html>
El contenido del Body es un div con la clase pagecontrol que identifica que ese es el contenido de la página a navegar.
JavaScript
1: (function () {
2: "use strict";
3:
4: // This function is called whenever a user navigates to this page. It
5: // populates the page elements with the app's data.
6: function ready(element, options) {
7: // TODO: Initialize the fragment here.
8: var div = element;
9: }
10:
11: function updateLayout(element, viewState) {
12: // TODO: Respond to changes in viewState.
13: }
14:
15: WinJS.UI.Pages.define("/SecondPage/pagecontrol.html", {
16: ready: ready,
17: updateLayout: updateLayout
18: });
19: })();
El fichero JavaScript sigue el estándar de envolver todo el código en una función anónima para así aislar el contexto global, definiendo a continuación la página internamente. Para ello se llama a la función WinJS.UI.Pages.define, a la que pasamos como primer parámetro un string con la ruta de la página actual y luego un objeto con los eventos a los que nos queremos suscribir, en el caso que nos ocupa ready y updateLayout. La función ready tiene los mismos parámetros que tenía la clase que definimos en navigator.js, siendo element el elemento div del contenido que queremos mostrar y options las opciones de la navegación, que es el segundo parámetro de WinJS.Navigator.navigate.
Conclusiones
Los desarrolladores de Silverlight estamos acostumbrados a una navegación basada en Páginas XAML, que el NavigationService se encarga de cargar y descargar conforme vamos interactuando con nuestra aplicación. En HTML tenemos el mismo concepto de navegación de página, pero vimos al principio que el inconveniente que tiene este modo de trabajar es que, en algún momento, la página se dejará de renderizar, haciendo que nuestra aplicación no tenga el aspecto de una aplicación tradicional de escritorio.
Para solucionar este problema se ha optado por una navegación basada en añadir y quitar contenido al DOM de la aplicación. El único problema que tiene esta metodología, es que, por ejemplo, los ficheros CSS que se carguen en memoria se podrán descargar, con lo que podemos encontrarnos con comportamientos no deseados. Por ejemplo, si definimos una clase de CSS con el mismo nombre en dos ficheros, al cargar el primero todo se verá correctamente. Si luego navegamos a una página, y en esa otra página cargamos otro CSS que sobreescribe la clase de la que estamos hablando, todo se seguirá mostrando correctamente. Pero si ahora volvemos hacia atrás el elemento que originalmente utilizaba esta misma clase de CSS, ahora se ve de manera diferente por este segundo fichero CSS. Por tanto, es muy importante hacer nombres de clases únicos para CSS, y si se repiten tener claro el porqué.
El desarrollo de HTML tiene un hándicap muy grande que es la falta de controles como en XAML. Esto hace que casi todo el contenido se tenga que repetir y no se pueda encapsular aspecto y funcionalidad de manera cómoda para el desarrollador. De todos modos Microsoft ha hecho un esfuerzo muy grande con WinJS para que el desarrollo de nuestras aplicaciones Metro con HTML sea lo más cómodo del mundo.
Él código de ejemplo lo puedes descargar de aquí.