[Windows 8] Navegando en una aplicación HTML/JavaScript

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í.

Material de la charla de Computación paralela en Windows de la CodeMotion

Como viene siendo habitual aquí tenéis el material de la charla sobre computación paralela del pasado sábado día 24 de marzo.

clip_image002

El código de ejemplo lo podéis descargar de aquí: http://bit.ly/TPLCodeMotion

Y ya sabéis nada de dejar los try/catch vacíos.
¡Espero que disfrutéis de todos los cores del mundo!

Saludos. Luis.

San Valentín se Baila

Este domingo en la plaza de Callao de Madrid habrá un evento de El Corte Inglés, Microsoft y Xbox 360 para celebrar el día de los enamorados. En este evento Plain Concepts presentará dos aplicaciones para Windows Phone 7 y Surface 2 para que las parejas de enamorados puedan hacerse fotografías con el teléfono móvil (un Nokia Lumia) y después componer una tarjeta de felicitación en un Surface 2.

Os invitamos a todos a que os paséis este domingo por la mañana por Callao con vuestras parejas, para celebrar San Valentín y disfrutar de estas aplicaciones.

Windows Phone 7

Las imágenes se suben a Azure y luego desde la aplicación de Surface 2 se pueden componer.

Surface 2

12 Horas de Visual Studio – Calidad de Software y patrones de diseño en Windows Phone 7.5

Hoy es el evento de 12 Horas de Visual Studio de Microsoft y Globbtv, podeis ver el evento en directo aquí http://www.globbtv.com/vstudio12horas/

Este es el material que voy a utilizar sobre mi charla sobre “Calidad de Software y patrones de diseño de Windows Phone 7.5”

clip_image002

El código de ejemplo os lo podéis descargar de aquí, http://bit.ly/12HorasVSWindowsPhone

Hack-a-thon, repaso a las novedades de networking de Windows Phone 7

Este fin de semana en Málaga se ha celebrado el Hack-a-thon un evento de Microsoft para incentivar el desarrollo de aplicaciones de Windows Phone 7 en entornos universitarios. Es todo un fin de semana de programación de apps y entre medias los desarrolladores pueden elegir las charlas que quieres escuchar. Así que es un agenda dinámica que se decide en base a las valoraciones.

Yo es la segunda vez que participo en un evento de este tipo y en mi caso la charla que me ha tocado es la de Networking. Así que estando ahora mismo en el evento aprovecho para hacer un repaso de las novedades de Windows Phone 7.1 (Mango) en el apartado de comunicaciones.

clip_image002

Peticiones HTTP

Dentro de apartado de peticiones HTTP de toda la vida, WP7 tiene dos clases para realizar este trabajo: WebClient y HttpWebRequest (+Response). Las dos API se distinguen una de la otra por la simplicidad y opciones que ofrecen.

Hay que recordad que todas las comunicaciones en WP7 son asíncronas.

WebClient

WebClient es la API más sencilla para hacer peticiones HTTP, simplemente hay que crear una instancia de esta clase, suscribirse al evento deseado, por ejemplo, DownloadStringCompleted y llamar al método DownloadStringAsync(Uri) para descargarse el contenido de una URI como un string.

public partial class MainPage : PhoneApplicationPage
{
    WebClient client;

    // Constructor
    public MainPage()
    {
        InitializeComponent();

        client = new WebClient();
        client.DownloadStringCompleted +=
                     new DownloadStringCompletedEventHandler(
                         client_DownloadStringCompleted);
    }

    void client_DownloadStringCompleted(object sender,
                                DownloadStringCompletedEventArgs e)
    {
        if (e.Error == null)
        {
            XElement twitterElements = XElement.Parse(e.Result);

            var postList =
                from tweet in twitterElements.Descendants("status")
                select new TwitterPost
                {
                    UserImage = tweet.Element("user").Element("profile_image_url").Value,
                    PostText = tweet.Element("text").Value,
                    DatePosted = tweet.Element("created_at").Value
                };

            tweetsListBox.ItemsSource = postList;
        }
    }

    private void loadButton_Click(object sender, RoutedEventArgs e)
    {
        string url = "http://twitter.com/statuses/user_timeline/" +
                     nameTextBox.Text + ".xml";
        client.DownloadStringAsync(new Uri(url));
    }
}

 

En este ejemplo podemos ver como en los argumentos DownloadStringCompletedEventArgs podemos obtener una propiedad llamada Result que contiene el string con el contenido de la petición.

En este tipo de peticiones no podemos añadir cookies ni configurar ningún otro tipo de propiedad para la petición.

Las opciones de personalización son:

  • Añadir cabeceras en la petición y leer las cabeceras de la respuesta.
  • Configurar credenciales para autenticación de usuarios.
  • Permitir la lectura buffereada del contenido de la lectura y de la escritura.
  • Codificación usada para lectura y escritura.

HttpWebRequest

HttpWebRequest es la clase de bajo nivel que permite hacer peticiones HTTP configurando todas las opciones que queramos, es mucho más flexible, pero más complejo de consumir. Estas API utiliza el APM (Asychonous Programming Model) de .NET lo que significa que utiliza para las notificaciones asíncronas IAsyncResult.

Estas son las características:

  • Acceso a todas las cabeceras.
  • Podemos agregar cookies en las peticiones y leer las cookies de respuesta.
  • Podemos especificar el método de la petición (GET o POST)
  • Podemos escribir en el cuerpo de la petición.

Así tenemos un ejemplo completo de peticiones usando HttpWebRequest:

public class ComplexRestRequest : BaseRequestProcessor
{
    public override void ProcessRequest(Uri uri, string body)
    {
        content = body;

        request = HttpWebRequest.Create(uri);
        request.Method = "POST";

        request.Headers["Authorization"] = AuthorizationService.AuthorizationToken.Token;
        request.Headers["IsComplex"] = "true";
        request.BeginGetRequestStream(new AsyncCallback(OnBeginGetRequestStream), null);

    }

    private void OnBeginGetRequestStream(IAsyncResult result)
    {
        Stream stream = request.EndGetRequestStream(result);
        byte[] buff = System.Text.Encoding.UTF8.GetBytes(content);
        stream.Write(buff, 0, buff.Length);
        buff = null;

        request.BeginGetResponse(OnBeginGetResponse, null);
    }

    private void OnBeginGetResponse(IAsyncResult result)
    {
        try
        {
            response = request.EndGetResponse(result);
            string authorizationHeader = response.Headers["Authorization"];
            if (!string.IsNullOrEmpty(authorizationHeader))
            {
                AuthorizationService.UpdateAuthorizationToken(authorizationHeader);
            }
            string content = null;
            if (response.ContentLength > 0L)
            {
                using (Stream stream = response.GetResponseStream())
                {
                    using (StreamReader reader = new StreamReader(stream, Encoding.UTF8, true))
                    {
                        content = reader.ReadToEnd();
                    }
                }
            }

            FireEndRequestCompleted(new HttpResult(content, false, null));
        }
        catch (Exception ex)
        {
            Trace.WriteLine(ex.ToString());
            FireEndRequestCompleted(new HttpResult(null, true, ex));
        }
    }

    private WebRequest request;
    private WebResponse response;
    private string content;
}

 

Socket

La siguiente gran funcionalidad de comunicaciones, muy esperada, en Windows Phone 7 son los sockets. Los sockets permiten una comunicación más directa en un canal de comunicación orientado a conexión (TCP) o no orientado a conexión (UDP y multicasting).

clip_image004

Windows Phone 7 soporta direcciones IPv4 pero no soporta IPv6. Toda la API es asíncrona.

Background file transfers

Si tenemos que descargar o subir ficheros al isolated storage de nuestra aplicación, pero queremos que esa descarga se haga cuando el usuario no esté usando la conexión a internet de su dispositivo móvil, podemos usar Background file transfers.

Esta API permite programar la descarga de un fichero al almacenamiento aislado de Windows Phone 7 incluso si nuestra aplicación no está ejecutándose. Soporta HTTP y HTTPS pero no FTP. Alguna de las cabeceras HTTP están reservadas, principalmente las de control de cache.

Estos son los valores de cuota:

  • Tamaño máximo de subida: 5MB
  • Tamaño máximo de descarga sobre 2G/3G: 20 MB
  • Tamaño máximo de descarga sobre WiFi: 100MB

API de información de comunicaciones

Todas las aplicaciones que utilicen recursos online deberán de ser tolerantes a faltas de conectividad por parte del usuario en su dispositivo. Si el usuario está modo avión, no tiene cobertura ni Wifi, la aplicación no debería de fallar y cerrarse, sino que debería de ofrecer la posibilidad de reconectarse de nuevo.

Para eso necesitamos saber cuál es el estado de las comunicaciones del dispositivo.

  • Consulta del operador móvil
    • DeviceNetworkInformation.CellularMobileOperator
  • Consulta si hay red disponible
    • DeviceNetworkInformation.IsNetworkAvailable
  • Consulta si hay red celular 2G/3G
    • DeviceNetworkInformation.IsCellularDataEnabled
  • Consulta si el romaing está habilitado
    • DeviceNetworkInformation.IsCellularDataRoamingEnabled
  • Consulta si el WiFi está habilitado
    • DeviceNetworkInformation.IsWiFiEnabled

Eligiendo la mejor serialización para aplicaciones móviles

Cuando desarrollamos aplicaciones móviles tenemos que tener en cuenta el tamaño de los datos que enviamos al cliente. Por eso tenemos que elegir la serialización que permite utilizar el menor tamaño para enviar los datos. Aquí tenemos una comparativa de los diferentes formatos para los mismos datos envíados.

Wire Serialization Format Size in Bytes
ASMX SOAP – DataSet (XML) 39670
ODATA XML 73786
ODATA JSON 34030
REST + JSON 15540
REST + JSON GZip 8680

Luis Guerrero.

Como tener más de un emulador de Windows Phone 7 en la misma máquina

Si en alguna ocasión habéis tenido la necesidad de tener más de un emulador ejecutándose en la misma máquina, para por ejemplo, depurar vuestro código de Socket para Mango, ahora podéis hacerlo.

Esto son los pasos.

Ve a la carpeta C:\ProgramData\Microsoft\Phone Tools\CoreCon\10.0\addons

En esa carpeta hay un fichero llamado ImageConfig.en-us.xsl copiadlo por otro fichero llamado por ejemplo ImageConfig2.en-us.xsl.

clip_image002

Una vez hecho esto abridlo con un bloc de notas elevador (como administrador)

clip_image004

En el nodo PLATFORM hay una propiedad llamada ID, teneis que generar una nueva GUID. Podéis usar la herramienta que viene en Visual Studio para generar GUID’s. Una vez generada la nueva GUID, cambiar el atributo del nodo xml.

Podéis cambiar el nombre del emulador cambiando el atributo name del mismo nodo.

clip_image006

Buscad una propiedad llamada VMID en el valor tenéis que poner la misma GUID.

Ahora cerrad el Visual Studio y abridlo de nuevo, ahora teneis dos valores en el combo de Device de Windows Phone 7.

clip_image007

Aquí tenéis una captura de dos emuladores ejecutando ByeByeBrain.

clip_image009

Espero que sea de utilidad.

Saludos. Luis.

Material de la charla de HTML5 en el CIIN

El lunes 3 de septiembre estuve con la gente del CIIN dando una charla sobre desarrollo de aplicaciones en HTML5 y cuáles son las novedades en este sentido.

Aquí os dejo la presentación, las demos y algunos enlaces que comenté durante la charla.

image

Agradecer a Alejandro Hidalgo (MVP de Internet Explorer 9) por la presentación en HTML5.

Enlaces de interés:

Decir que en PlainConcepts utilizamos Sproutcore para nuestros desarrollos y además formamos parte del soporte oficial de SproutCore para Microsoft y Strobe.

Saludos. Luis.

Autorización de usuarios para una API web en WCF o como hacer una gestión light de sesión en WCF

En uno de los proyectos en los que estoy trabajando ahora mismo tenemos que hacer una API para que se consuma desde Javascript puro, es un proyecto en HTML5, así que tenemos que maximizar la productividad para este tipo de cliente.

Nuestra API tiene un login de usuarios, un registro y partir de ahí los servicios debería de ser securizados, es decir, solamente para el usuario actual. Así que me surge la necesidad de autenticar estas peticiones para asegurarme de que es un usuario válido para acceder al servicio.

Viendo un poco como los demás servicios, Twitter, Facebook y compañía lo hacen, decidí que cuando el usuario hacer login se le devuelva un token de autorización temporal (que tienen este aspecto VvTnZEpvrYBDZfF1hCIR8kZR0yW7jKrA) obligar a que cada petición se añada una cabera más de Autrorization para que yo desde el servidor puede leerla y comprobar que es un usuario válido.

Ahora bien yo estoy desarrollando mi solución con WCF utilizando JSON como formato de cliente, para que así sea más cómodo consumirlo con el cliente, así que tenía dos maneras de solucionar esta manera de autorización, habilitar la compatibilidad de ASP.NET en WCF y hacerlo a través del objeto de HttpContext.Request o directamente utilizar la infraestructura de WCF.

Decidí usar únicamente WCF.

Autorizar al usuario

Lo primero de todo es que tengo que comprobar las credenciales del usuario en el login, podéis elegir el mejor mecanismo para eso. Una vez que sabemos que el usuario es un usuario válido tenemos que devolver el token de autorización para que pueda usarlo en sucesivas peticiones al servicio. ¿Cómo generamos esa autorización?

Yo he preferido hacerlo de la manera más sencilla y mantenerlo lo más sencillo posible. Yo genero un string formado por la id del usuario logeado y la fecha del login en ticks, así que me queda algo como esto: 1345-634475405148831292.

Evidentemente enviar ese string directamente al cliente es un grave problema de seguridad así que lo que tenemos que hacer es encriptar y añadir un hash a esa cadena.

private string CreateAuthorizationString(User user)
{
    string result = null;

    if (user != null)
    {
        string key = "{0}-{1}";
        key = string.Format(key, user.UserId, DateTime.Now.Ticks);

        ICryptoTransform transform = new TripleDESCryptoServiceProvider().CreateEncryptor(this.key, this.iv);
        byte[] input = Encoding.Default.GetBytes(key);
        byte[] buff = new byte[input.Length];
        buff = transform.TransformFinalBlock(input, 0, input.Length);
        
        result = Convert.ToBase64String(buff);
    }

    return result;
}

Yo para ese caso utilizo TripleDES como algoritmo simétrico y luego el string generado lo convierto a Base64 para tenerlo en un cómo string.

Comprobar la autorización en WCF

Una vez que tenemos generado el token de autorización tenemos que implementar un mecanismo para poder comprobar esa autorización en el servicio, teniendo un caso especial, uno cuando el usuario se quiere autorizar (hay que permitir la petición) y cualquier otra petición.

Yo en la definición de mi servicio tengo un webHttpBinding y tengo aplicado un endPointConfiguration y un serviceBehaviorConfiguration.

<behaviors>
  <endpointBehaviors>
    <behavior name="JsonEndpointBehavior">
      <webHttp defaultBodyStyle="Bare" defaultOutgoingResponseFormat="Json"
        automaticFormatSelectionEnabled="true" faultExceptionEnabled="true" />
    </behavior>
  </endpointBehaviors>
  <serviceBehaviors>
    <behavior name="DefaultServiceBehavior">
      <serviceMetadata httpGetEnabled="true" />
      <serviceDebug includeExceptionDetailInFaults="true" />
      <serviceAuthorization principalPermissionMode="Custom" serviceAuthorizationManagerType="Microsoft.Magazine.Foundation.MagazineServiceAuthorizationManager, Microsoft.Magazine.Foundation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    </behavior>
  </serviceBehaviors>
</behaviors>

En el serviceBehavior tengo aplicado un ServiceAuthorization en el que especifico que el manager de objetos principal (identidades) será personalizado y especifico el tipo que se encargará de gestionar la autorización de las identidades.

Así que lo que tenemos que hacer es implementar los dos casos, cuando el usuario está intentado hacer login, tenemos que permitir la autorización y cuando el usuario hacer cualquier otra petición tenemos que asegurarnos de que es un usuario válido.

protected override bool CheckAccessCore(OperationContext operationContext)
{
    bool result = false;

    Message message = operationContext.RequestContext.RequestMessage;
    object value;
    if (message.Properties.TryGetValue("HttpOperationName", out value))
    {
        if ((string)value == "LoginUser")
        {
            result = true;
        }
    }

    if (!result)
    {
        HttpRequestMessageProperty httpRequestMessage;
        object httpRequestMessageObject;
        if (message.Properties.TryGetValue(HttpRequestMessageProperty.Name, out httpRequestMessageObject))
        {
            httpRequestMessage = httpRequestMessageObject as HttpRequestMessageProperty;
            if (!string.IsNullOrEmpty(httpRequestMessage.Headers["Authorization"]))
            {
                string authorization = httpRequestMessage.Headers["Authorization"];
                result = new Login().IsValidAuthorization(authorization);
            }
        }
    }

    if (result)
    {
        operationContext.ServiceSecurityContext.AuthorizationContext.Properties["Principal"] = Thread.CurrentPrincipal;
    }

    return result;
}

La manera que tengo de comprobar que el usuario quiere hacer login es comprobando la operación de http que está invocando, que es, justamente la operación del servicio que invoca. Así que si está invocando LoginUser significa que está haciendo login así que result lo establezco en true.

En caso de que result no sea true, tengo que asegurarme de que la petición tiene la cabecera authorization, para ello tenemos que extraer de las propiedades del mensaje el objeto del tipo HttpRequestMessageProperty que contiene las propiedades de la petición http asociada a este mensaje. Acordaros que nosotros usábamos webHttpBinding con WebGet.

Dentro de ese objeto tenemos acceso a las cabeceras de http normales, buscamos Authorization y entonces intentamos validar ese token.

Validar el token de autorización

Una vez que ya tenemos el string que representa el token de autorización tenemos que desencriptar el contenido y parsear el formato para verificar la id del usuario y la fecha del login.

public bool IsValidAuthorization(string value)
{
    bool result = false;

    value.EnsureIsNotNullOrEmpty();

    ICryptoTransform transform = new TripleDESCryptoServiceProvider().CreateDecryptor(this.key, this.iv);
    byte[] buff = Convert.FromBase64String(value);

    buff = transform.TransformFinalBlock(buff, 0, buff.Length);
    string ticket = Encoding.Default.GetString(buff);

    string[] values = ticket.Split('-');
    if (values != null && values.Length == 2)
    {
        int userId;
        long ticks;
        if (int.TryParse(values[0], out userId) && long.TryParse(values[1], out ticks))
        {
            if (IsValidUser(userId) && Math.Abs((new DateTime(ticks) - DateTime.Now).Hours) < 1)
            {
                result = true;
            }
        }
    }

    return result;
}

Así que dentro de mi infraestructura validar el usuario es comprobar que es un usuario válido (está en la base de datos) y que el tiempo de la última vez que el usuario hizo login fue una hora.

Conclusiones

Con estos pasos tengo un sistema centralizado de autorización, utilizo en todo momento la infraestructura de WCF, sin habilitar la compatibilidad con ASP.NET, que penaliza el rendimiento, y no necesito en cada petición obtener la referencia al usuario actual sino que seré capaz de obtenerlo a través del objeto principal del thread que procesa la petición.

Es importante resaltar la importancia de encriptar el token de autorización para evitar problemas de robo de sesiones y generar sesiones automáticamente, ya que la clave y el vector de inicialización del algoritmo TricpleDES está seguro en la parte de servidor.

Luis Guerrero.

Como generamos las releases en Plain Concepts, el caso de las aplicaciones de Windows Phone de Prisa.

Como generamos las releases en Plain Concepts, el caso de las aplicaciones de Windows Phone de Prisa.

Como dije anteriormente en mi artículo, ya están disponibles las aplicaciones de El País, As.com y CincoDías.com en el Marketplace de Windows Phone 7. Estas aplicaciones han sido desarrolladas y diseñadas íntegramente por Plain Concepts, en este artículo explicaré cuales son los procesos de desarrollo que seguidos dentro de PlainConcepts para asegurar la calidad de la aplicación y las técnicas empleadas.

Las aplicaciones están desarrolladas en C# usando Visual Studio 2010 y la metodología usada es Scrum, adaptándola para el desarrollo con un equipo tan pequeño y a las particularidades del proyecto en cuestion. Durante su desarrollo han participado un equipo de dos programadores, un diseñador y un scrum master. Se han empleado 7 iteraciones a aproximadamente 2 semanas cada una para completar el desarrollo, más una iteración final para estabilizar código. Durante la penúltima y última iteración una persona fuera del equipo se encargó del testing en la aplicación en dispositivos físicos.

Aquí os muestro la gráfica de Burndown and Burn Rate del proyecto:

clip_image002

Como se puede ver, conforme se iban generando trabajo se iba completando y aunque hay muchos picos, que indican que se ha ido bajando las horas de las tareas adecuadamente, en general la progresión de la aplicación ha sido buena. Conforme se iba cerrando trabajo se iba generando nuevo. La banda de color azul de fondo indica tareas que se quedaron fuera de la funcionalidad final de la aplicación, pero que no se eliminaron del TFS.

La gráfica del estado de todas las iteraciones, muestra cómo se fueron cerrando las tareas de cada iteración de manera correcta. Además podemos observar como al principio el equipo era demasiado optimista en cuanto a las estimaciones que se hacían, pero conforme las iteraciones fueron avanzando la confianza, la experiencia y el mayor conocimiento del proyecto hacen que las estimaciones y el reparto de tareas sean cada vez más cercanas a la realidad. Aunque como he dicho se iban generando tareas fuera de la iteración con trabajo de fondo que no se fueron cerrando en el TFS.

clip_image004

Como está el proyecto organizado

Al desarrollar una aplicación en Silverlight para Windows Phone 7, el patrón principal a la hora de desarrollar todas la aplicación ha sido MVVM, este patrón permite tener una vista hecha en xaml sin apenas nada de código y llevarse toda la lógica de la aplicación al ViewModel donde se realizaba todo el trabajo, en este desarrollo no se ha usado ningún framework de terceros para MVVM ya nosotros mismo hemos generado las utilidades que necesitábamos para el proyecto en un proyecto común a las 3 aplicaciones. Así el diagrama de arquitectura general se queda así:

clip_image006

Donde PlainConcepts.Common está la funcionalidad común a los tres proyectos; convertes, controles, tipos de datos comunes, parseadores de xml, navegación, serialización, comandos y viewmodels.

En cada uno de los proyectos están sus respectivas vistas, controles de usuario, páginas del teléfono y demás artefactos específicos, imágenes, iconos, etc.

Configuración del TFS, políticas de check-in, ramas y builds.

Para manejar el ciclo de vida del desarrollo y la integración continua durante el proyecto se han usado diferentes artefactos para asegurar que el desarrollo era incremental.

Políticas de check-in

El proyecto tenia habilitado el multiple chech-out y como políticas de check-in teníamos, requerir asocial un work ítem en cada check-in y requerir agregar un comentario en cada check-in. Eso nos permite tener trazabilidad de como el proyecto se ha desarrollado y que changeset del servidor está asociado a cada work ítem. Por no decir que también de esta manera sabemos que work ítems están asociados a cada compilación.

Políticas de ramas (branching)

Durante el desarrollo inicial de la aplicación no había ninguna configuración especial en el servidor sobre ramas, es decir únicamente teníamos una rama dev (Developer) en la que se trabajaba. Pero conforme el cliente fue pidiendo versiones estables para ver el progreso del desarrollo, se optó por esta configuración:

clip_image007

La carpeta dev se pasó a llamar Main. A partir de esta rama Main se generaron, dev y todas las releases de las diferentes aplicaciones.´

Esto nos permitía tener en la rama Main versiones estables de las tres aplicaciones entregables al cliente, libre de bugs, estabilizadas y fuera del ciclo de desarrollo. El tener una rama Main te permite que el equipo de desarrollo siga trabajando en dev, pero conforme la funcionalidad vaya generándose que se vayan mezclándose (merge) la funcionalidad de dev a Main y así hacer un entregable al cliente.

Las ramas de reléase de cada aplicación tienes las versiones reléase publicadas en el Marketplace y las sucesivas actualizaciones que se vayan generando. Así si encontramos un bug en la versión del servidor, tenemos el changeset etiquetado podemos generar un fix en la rama reléase de esa aplicación, sin tener que impactar en las demás aplicaciones y cuando el bug este corregido y testeado, propagar ese cambio de la rama reléase de la aplicación, a la rama Main y de ahí a dev y el resto de releases (en caso de que el bug sea de algo común).

Compilaciones (Builds)

Otra requisito importante para seguir integración continua durante el desarrollo son las compilaciones, en el proyecto hay 5 compilaciones creadas y todas ellas activas.

  • As Release 1.0: gated checking en la rama de release (modo release).
  • CincoDias Release 1.0: gated checkin en la rama de release (modo release).
  • ElPais Release 1.0: gated checkin en la rama de reléase (modo release).
  • Prisa.WP7: continuous integration en la rama de dev (modo debug y release).
  • Prisa.WP7 Main: gated checkin en la rama de main (modo debug y release).

Con esta lista de compilaciones en el equipo nos podíamos asegurar de que cada checkin era incremental, porque podíamos verificar que la compilación era correcta por lo menos en Main y en las ramas de release. Esto nos permitía saber que el código que teníamos en Main y Release compilaba sin problemas para que en cualquier momento se pudiera hacer un cambio, además de eliminar el molesto “Works on my machine” ayudando así a los tester a tener versiones incrementales de la funcionalidad compilada por el servidor y poder ir generando bugs sobre las versiones generadas.

Aquí podemos ver el estado de las compilaciones correctas a través del tiempo:

clip_image009

Otra de las cosas interesantes de tener un servidor de compilaciones es que conforme la funcionalidad va generándose se van generando binarios de la aplicación que se pueden consumir, así que el equipo de Plain Concepts decidió dar acceso al cliente, a través de http a la carpeta de salida de todos los binarios de las aplicaciones. De esta manera el cliente puede tener acceso ubicuo a su producto y desplegarse la última versión siempre que lo desee.

Testing

Aunque no es posible integrar el testing de Visual Studio en proyecto de Silverlight para Windows Phone 7, el testing de la aplicación se definió como tesing manual que se hacía por personas independientes del proyecto en terminales físicos y nunca sobre el emulador, con conexiones 2G (GPRS), 3G, 3.5G y WiFi, sobre varios terminales diferentes.

Como se publica una versión en el Marketplace

El proceso para publicar una versión en el Marketplace implica tener los cambios solicitados por el cliente en la rama reléase de la aplicación, que la compilación en modo reléase, desplegar esa fichero generado por el servidor de compilación en el teléfono y testear que todos los cambios que el cliente había solicitado están integrados y que de la lista de test previos que todo este correcto, para que se no se no se hayan introducido nuevos bugs.

Una vez hecho esto tenemos que subir el número de versión de la aplicación y hacer checkin, este último check-in con el cambio de versión, es el xap que se sube al Marketplace. Se etiqueta la build con una etiqueta de calidad “Published in WP7 Marketplace” y se retiene la build.

clip_image011

Además de eso se aplica una etiqueta en la rama de la aplicación con la versión de la aplicación para en caso de que haya un bug saber el changeset asociado.

Conclusiones

El desarrollo de software es una disciplina muy complicada y que no se debe infravalorar, en Plain Concepts nos gusta hacer las cosas bien y sabemos lo complicado que es montar un proyecto, mantenerlo y sobre todo desarrollar funcionalidad. Así que con este post queremos mostrar un poco cual es el proceso que seguimos internamente para desarrollar. Eso no significa que todos los proyectos se montan de la misma manera, sino que cada proyecto es único a la hora de crearse y desarrollarse, y no existen reglas fijas de metodologías y artefactos que utilizar. Estas deben ser adecuadas al proyecto en cuestión haciendo que sean útiles para el equipo y para el proceso en sí. Lo difícil de todo esto es saber que metodologías usar, que tecnologías usar y cómo gestionar todo ese flujo. Como dice Rodrigo Corral, “los proyectos no fracasan por la solución tecnológica, sino por la gestión en sí”, es decir, que confiar en que el proyecto va a ser un éxito simplemente por usar todas las últimas tecnologías y herramientas, es simplemente un error, porque no te elimina la necesidad de gestionar personas y recursos.

Si tenéis alguna pregunta, o queréis comentar algo sobre el proceso podéis hacerlo en los comentarios.

Saludos.

Luis Guerrero.