(La version finale est accessible ici: http://blogs.msdn.com/b/eternalcoding/archive/2012/06/08/comment-cuisiner-une-application-windows-8-avec-html-5-css3-et-javascript-en-une-semaine-jour-5.aspx)
Le but de cette série d’articles est de fournir une recette pragmatique pour écrire une application Windows 8 à partir de zéro.
L’application que j’utiliserai comme exemple est nommée UrzaGatherer et est utilisée pour aider les collectionneurs de cartes Magic The Gathering à gérer leur collection.

UrzaGatherer fut développé à l’origine avec WPF 4.0 (http://urzagatherer.codeplex.com/) mais j’ai préféré utiliser HTML 5, CSS3 et JavaScript pour développer la version Windows 8 car je voulais me pencher sur ce nouveau mode de développement.
Pour développer l’application, vous allez donc avoir besoin de :
La solution peut être téléchargée ici: http://www.catuhe.com/msdn/urza/day0.zip
L’intégralité de la série est disponible ici:
- Jour 0 : http://blogs.msdn.com/b/eternalcoding/archive/2012/04/07/comment-cuisiner-une-application-windows-8-avec-html-5-css3-et-javascript-en-une-semaine-jour-0.aspx (Création du projet et premiers pas)
- Jour 1 : http://blogs.msdn.com/b/eternalcoding/archive/2012/04/10/comment-cuisiner-une-application-windows-8-avec-html-5-css3-et-javascript-en-une-semaine-jour-1.aspx (Finalisation des écrans et mode déconnecté)
- Jour 2 : http://blogs.msdn.com/b/eternalcoding/archive/2012/04/16/comment-cuisiner-une-application-windows-8-avec-html-5-css3-et-javascript-en-une-semaine-jour-2.aspx (Internationalisation , Gestion de l’activation du mode offline , Intégration d’une flipView pour la page des cartes , Gestion des vues “snapped” , Adaptation aux différentes résolutions , Gestion des éléments de tailles différentes)
- Jour 3 : http://blogs.msdn.com/b/eternalcoding/archive/2012/04/19/comment-cuisiner-une-application-windows-8-avec-html-5-css3-et-javascript-en-une-semaine-jour-3.aspx (Contrat de recherche , Contrat de partage , Contrat d’ouverture de fichier , Vignette interactive, Vignettes secondaires)
- Jour 4 : http://blogs.msdn.com/b/eternalcoding/archive/2012/05/21/comment-cuisiner-une-application-windows-8-avec-html-5-css3-et-javascript-en-une-semaine-jour-4.aspx (Gestion de la collection, Live SDK)
- Jour 5 : http://blogs.msdn.com/b/eternalcoding/archive/2012/06/08/comment-cuisiner-une-application-windows-8-avec-html-5-css3-et-javascript-en-une-semaine-jour-5.aspx (Portage vers la RP)
- Version RTM : http://blogs.msdn.com/b/eternalcoding/archive/2012/08/20/how-to-cook-a-windows-8-application-with-html5-javascrip-css3-rtm-version.aspx
Créer le projet
La première chose à faire est de créer un projet vide (nous aurions bien sur pu utiliser un modèle plus complet de Visual Studio comme le ‘Grid Application"’ mais l’objectif ici est de comprendre comme tout fonctionne) en utilisant le menu Fichier/Nouveau projet :

Le projet est ainsi créé avec uniquement les fichiers nécessaires :

Créer les ressources graphiques
Le fichier package.appxmanifest permet de décrire votre application auprès de Windows 8. Il contient particulièrement la description de l’application ainsi que tous les logos et ressources graphiques nécessaires :
J’aime beaucoup ajouter ces ressources à mes applications car cela donne tout de suite un air de “vraie application”.
Le “splash screen” par exemple est très important car c’est la première chose que vos utilisateurs vont voir et comme chacun sait la première impression est toujours la plus importante :

Cette partie est souvent la plus dure au final car les développeurs ne sont que très rarement de bons designers 
Structurer le projet
La structuration va dépendre de votre manière de travailler. Personnellement j’aime travailler ainsi :

- Un répertoire pour mes ressources graphiques (/images)
- Un répertoire pour mon code JavaScript qui n’est pas en relation avec les pages (/js)
- Un répertoire pour les pages
- Un répertoire pour chaque page (/pages/xxx) ou seront positionnés les fichiers .js, .html, .css
- Une page racine default.html (avec son .css et son .js)
Connexion aux données
La connexion aux données sera faite dans le fichier /js/data.js.
Pour UrzaGatherer, les données sont composées de :
- Un fichier all.json qui décrit toutes les cartes supportées
- Les images de chaque carte (plus de 4Go!!)
- Un logo pour chaque extension (sachant qu’une carte appartient à une extension qui appartient à un bloc)
- Un logo pour chaque bloc
En partant de notre fichier data.js vide, nous allons ajouter une fonction anonyme automatique :
(function () { })();
A l’intérieur de cette fonction, nous allons récupérer les données. Pour UrzaGatherer, ce fichier est présent en ligne (all.json) mais fait une taille trop importante pour être téléchargé à chaque fois. Il faut donc gérer un cache local :
(function () {
var blocks = new WinJS.Binding.List();
var expansions = new WinJS.Binding.List();
var root = "http://urzagatherer.blob.core.windows.net";
var expansionSorter = function (i0, i1) {
if (i0.orderInBlock > i1.orderInBlock)
return 1;
else if (i0.orderInBlock < i1.orderInBlock)
return -1;
return 0;
};
var processBlocks = function (data) {
var result = JSON.parse(data);
for (var blockIndex = 0; blockIndex < result.length; blockIndex++) {
var block = result[blockIndex];
block.logo = root + "/blocks/" + block.name.replace(":", "_") + ".png";
blocks.push(block);
var sortedExpansions = block.expansions.sort(expansionSorter);
for (var expansionIndex = 0; expansionIndex < sortedExpansions.length; expansionIndex++) {
var expansion = sortedExpansions[expansionIndex];
expansion.block = block;
expansion.logo = root + "/logos/" + expansion.name.replace(":", "_") + ".png";
expansions.push(expansion);
}
}
}
var getBlocksDistant = function (onload) {
var localFolder = Windows.Storage.ApplicationData.current.localFolder;
var requestStr = root + "/cards/all.json";
WinJS.xhr({ url: requestStr }).then(function (request) {
processBlocks(request.responseText);
localFolder.createFileAsync("all.json",
Windows.Storage.CreationCollisionOption.replaceExisting).then(function (file) {
Windows.Storage.FileIO.writeTextAsync(file, request.responseText);
});
if (onload)
onload();
});
}
var getBlocks = function (onload) {
var localFolder = Windows.Storage.ApplicationData.current.localFolder;
localFolder.getFileAsync("all.json").done(function (file) {
return Windows.Storage.FileIO.readTextAsync(file).then(function (data) {
processBlocks(data);
if (onload)
onload();
});
}, function () {
getBlocksDistant(onload);
});
}
WinJS.Namespace.define("UrzaGatherer", {
Blocks: blocks,
Expansions: expansions,
Init: getBlocks
});
})();
En utilisantWinJS.Namespace.define, vous allez pouvoir déclarer un objet global (nommé UrzaGatherer) qui sera disponible partout dans votre code.
L’initialisation (via la fonctionInit ) commence donc par chercher les informations en local puis tente de le télécharger via WinJS.xhr (http://msdn.microsoft.com/en-us/library/windows/apps/br229787.aspx) en cas d’échec.
La fonction Init prend en paramètre une fonction que j’utilise pour savoir que le chargement est terminé et que je peux cacher mon anneau d’attente (une progress bar en mode “ring”).
Préparation de la page d’arrivée
Système de navigation
La page default.html est la page d’arrivée c’est à dire la page qui sera chargée après le lancement de l’application. Dans notre cas elle sera chargé de gérer le système de navigation :
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>UrzaGatherer</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.0.6/css/ui-light.css" rel="stylesheet">
<script src="//Microsoft.WinJS.0.6/js/base.js"></script>
<script src="//Microsoft.WinJS.0.6/js/ui.js"></script>
<!-- UrzaGatherer references -->
<link href="/default.css" rel="stylesheet">
<script src="/js/data.js"></script>
<script src="/js/tools.js"></script>
<script src="/js/navigator.js"></script>
<script src="/default.js"></script>
</head>
<body>
<div id="contenthost" data-win-control="UrzaGatherer.PageControlNavigator"
data-win-options="{home: '/pages/home/home.html'}"></div>
</body>
</html>
Cette page est simple: Elle commence par référencer les fichiers de WinJS puis elle charge le code JavaScript global et la feuille de style par défaut.
Au niveau DOM, elle contient un seul div qui va servir de hôte pour les pages filles. C’est un point extrêmement important à comprendre. En effet les pages ne sont pas chargées les unes sur les autres comme dans un navigateur mais sont chargées dans une page hôte (default.html).
Pour mettre en place ce principe il faut faire appel au fichier navigator.js qui va créer le contrôle de navigation (Le code de ce fichier est présent dans les templates Visual Studio 11 tel que ‘Grid Application’) :
(function () {
"use strict";
var appView = Windows.UI.ViewManagement.ApplicationView;
var displayProps = Windows.Graphics.Display.DisplayProperties;
var nav = WinJS.Navigation;
var ui = WinJS.UI;
var utils = WinJS.Utilities;
WinJS.Namespace.define("UrzaGatherer", {
PageControlNavigator: WinJS.Class.define(
// Define the constructor function for the PageControlNavigator.
function (element, options) {
this.element = element || document.createElement("div");
this.element.appendChild(this._createPageElement());
this.home = options.home;
nav.onnavigated = this._navigated.bind(this);
appView.getForCurrentView().onviewstatechanged = this._viewstatechanged.bind(this);
document.body.onkeyup = this._keyupHandler.bind(this);
document.body.onkeypress = this._keypressHandler.bind(this);
nav.navigate(this.home);
}, {
// This function creates a new container for each page.
_createPageElement: function () {
var element = document.createElement("div");
element.style.width = "100%";
element.style.height = "100%";
return element;
},
// This function responds to keypresses to only navigate when
// the backspace key is not used elsewhere.
_keypressHandler: function (eventObject) {
if (eventObject.key === "Backspace")
nav.back();
},
// This function responds to keyup to enable keyboard navigation.
_keyupHandler: function (eventObject) {
if ((eventObject.key === "Left" && eventObject.altKey)
|| (eventObject.key === "BrowserBack")) {
nav.back();
} else if ((eventObject.key === "Right" && eventObject.altKey)
|| (eventObject.key === "BrowserForward")) {
nav.forward();
}
},
// This function responds to navigation by adding new pages
// to the DOM.
_navigated: function (eventObject) {
var newElement = this._createPageElement();
var parentedComplete;
var parented = new WinJS.Promise(function (c) { parentedComplete = c; });
var that = this;
WinJS.UI.Pages.render(eventObject.detail.location,
newElement, eventObject.detail.state, parented).
then(function (control) {
that.element.appendChild(newElement);
that.element.removeChild(that.pageElement);
parentedComplete();
document.body.focus();
that.navigated();
});
},
// This function is called by _viewstatechanged in order to
// pass events to the page.
_updateLayout: {
get: function () { return (this.pageControl && this.pageControl.updateLayout)
|| function () { }; }
},
_viewstatechanged: function (eventObject) {
(this._updateLayout.bind(this.pageControl))(this.pageElement, eventObject.viewState);
},
// This function updates application controls once a navigation
// has completed.
navigated: function () {
// Do application specific on-navigated work here
var backButton = this.pageElement.querySelector("header[role=banner] .win-backbutton");
if (backButton) {
backButton.onclick = function () { nav.back(); };
if (nav.canGoBack) {
backButton.removeAttribute("disabled");
}
else {
backButton.setAttribute("disabled", "disabled");
}
}
},
// This is the PageControlNavigator object.
pageControl: {
get: function () { return this.pageElement && this.pageElement.winControl; }
},
// This is the root element of the current page.
pageElement: {
get: function () { return this.element.firstElementChild; }
}
}
),
// This function navigates to the home page which is defined when the
// control is created.
navigateHome: function () {
var home = document.querySelector("#contenthost").winControl.home;
var loc = nav.location;
if (loc !== "" && loc !== home) {
nav.navigate(home);
}
},
});
})();
Comme vous pouvez le voir, le controle PageControlNavigator ajoute la page cible en tant qu’élément enfant après avoir supprimé le précédent. Comprendre ce point est essentiel car cela implique par exemple que tous les css et les scripts restent présents même lorsque l’on navigue vers une autre page (http://msdn.microsoft.com/en-us/library/windows/apps/hh452768.aspx).
Appliquer les styles à la page
La feuille de style default.css constitue la feuille de style racine et donc comme vu précédemment sera présente dans toutes les pages filles. Elle est responsable de mettre en place la structure globale.
Elle me sert également a stocker les styles globaux que j’utiliser comme la classe .hidden (que j’utilise pour cacher des éléments) :
html {
cursor: default;
}
body {
background-image: url('images/background.jpg');
background-size: 100% 100%
}
#contenthost {
height: 100%;
width: 100%;
}
.fragment {
/* Define a grid with rows for a banner and a body */
-ms-grid-columns: 1fr;
-ms-grid-rows: 133px 1fr 0px;
display: -ms-grid;
height: 100%;
width: 100%;
}
.fragment header[role=banner] {
/* Define a grid with columns for the back button and page title. */
-ms-grid-columns: 120px 1fr;
-ms-grid-rows: 1fr;
display: -ms-grid;
}
.fragment header[role=banner] .win-backbutton {
margin-left: 39px;
margin-top: 59px;
}
.fragment header[role=banner] .titlearea {
-ms-grid-column: 2;
margin-top: 37px;
}
.fragment header[role=banner] .titlearea .pagetitle {
width: calc(100% - 20px);
}
.fragment section[role=main] {
-ms-grid-row: 2;
height: 100%;
width: 100%;
}
.hidden {
display: none;
}
Vous pouvez noter l’utilisation de display:-ms-grid connu sous le nom de CSS3 Grid Layout (http://msdn.microsoft.com/en-us/library/windows/apps/hh465327.aspx). Ce type de display est très pratique car il permet de définir la structure en utilisant des grilles.
De plus pour le fond d’écran si vous n’avez pas de graphistes sous la main, vous pouvez simplement utiliser un léger dégradé (facile à créer avec Paint.net par exemple) :
Création de la page principale
Le premier écran visible est fournit par la page “home”. Elle va servir à afficher les extensions triées par blocs.
De base, elle est constituée ainsi :
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>UrzaGatherer</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.0.6/css/ui-light.css" rel="stylesheet">
<script src="//Microsoft.WinJS.0.6/js/base.js"></script>
<script src="//Microsoft.WinJS.0.6/js/ui.js"></script>
<!-- UrzaGatherer references -->
<link href="home.css" rel="stylesheet">
<script src="home.js"></script>
</head>
<body>
<!--Content-->
<div class="home fragment">
<header aria-label="Header content" role="banner">
<button class="win-backbutton" aria-label="Back" disabled></button>
<h1 class="titlearea win-type-ellipsis"><span class="pagetitle">UrzaGatherer</span>
</h1>
</header>
<section aria-label="Main content" role="main">
</section>
</div>
</body>
</html>
On peut noter la présence de notre bannière (header) et une section pour mettre notre contenu.
Pour ce faire, j’utilise un contrôle de type WinJS.UI.ListView. Ce dernier permet d’afficher des valeurs regroupées par des clefs :
<div class="blocksList" aria-label="List of blocks" data-win-control="WinJS.UI.ListView"
data-win-options="{itemTemplate:select('.itemTemplate'), groupHeaderTemplate:select('.headerTemplate')
, selectionMode:'none', swipeBehavior:'none', tapBehavior:'invoke',
layout:{type:WinJS.UI.GridLayout}}">
</div>
Le contrôle référence deux templates (itemTemplate et headerTemplate) qui définissent la structure visuelle des éléments et des entêtes :
<div class="headerTemplate" data-win-control="WinJS.Binding.Template">
<div class="header-title" data-win-bind="innerText: name">
</div>
<img class="item-image" data-win-bind="src: logo" src="#" />
</div>
<div class="itemTemplate" data-win-control="WinJS.Binding.Template">
<img class="item-image" data-win-bind="src: logo" src="#" />
<div class="item-overlay">
<h4 class="item-title" data-win-bind="textContent: name"></h4>
</div>
</div>
Ici encore, j’utilise CSS3 Grid pour créer ma structure comme on peut le voir dans home.css :
.home .blocksList .win-item {
-ms-grid-columns: 1fr;
-ms-grid-rows: 1fr 30px;
display: -ms-grid;
height: 130px;
width: 260px;
background: white;
outline: rgba(0, 0, 0, 0.8) solid 2px;
}
.home .blocksList .win-item:hover {
outline: #5F38FF solid 2px;
}
.home .blocksList .win-item .item-image-container {
-ms-grid-columns: 1fr;
-ms-grid-rows: 1fr;
-ms-grid-row: 1;
display: -ms-grid;
padding: 4px;
-ms-transition: opacity ease-out 0.2s, -ms-transform ease-out 0.2s;
-ms-transform: scale(1.0, 1.0);
}
.home .blocksList .win-item .item-image-container:hover {
opacity: 0.9;
-ms-transform: scale(1.1, 1.1);
}
.home .blocksList .win-item .item-image {
-ms-grid-row: 1;
-ms-grid-column-align: center;
-ms-grid-row-align: center;
max-height: 90px;
}
.home .blocksList .win-item .item-overlay {
-ms-grid-row: 2;
padding: 3px 15px 2px;
background-color: rgba(0, 0, 0, 0.8);
}
Et grâce à CSS3 Transitions (
http://msdn.microsoft.com/en-us/library/windows/apps/Hh781227.aspx), c’est simplissime de faire une jolie animation pour gérer le survol (“hover”).
Par la suite, la liste (blocksList) est remplie en utilisant les données récupérées précédemment et la fonction createGrouped de la classe WinJS.Binding.List :
var groupDataSource = UrzaGatherer.Expansions.createGrouped(this.groupKeySelector,
this.groupDataSelector, this.groupCompare);
ui.setOptions(listView, {
itemDataSource: groupDataSource.dataSource,
groupDataSource: groupDataSource.groups.dataSource,
layout: new ui.GridLayout({ groupHeaderPosition: "top" })
});
Un point important ici est situé autour de la fonction groupKeySelector. Cette dernière est utilisée pour créer une clef unique pour chaque groupe. Cette clef est ensuite utilisée pour grouper les éléments et sera aussi utile lorsque l’on ajoutera le SemanticZoom :
groupKeySelector: function (item) {
return item.block.name + "*" + item.block.index;
},
Faites bien attention ici: Il faut obligatoirement retourner une chaine de caractères et pas un nombre sous peine de faire planter les contrôles.
La fonction groupCompare se charge de faire le tri en se basant sur les clefs :
groupCompare: function (i0, i1) {
var index0 = parseInt(i0.split("*")[1]);
var index1 = parseInt(i1.split("*")[1]);
return index1 - index0;
},
Ajout d’un contrôle personnalisé pour afficher les images
Un des problèmes que j’ai rencontré avec l’affichage des images est le fait que leur téléchargement peut prendre du temps et comme tout est asynchrone (ce qui est bien) la page peut s’afficher avant que toutes les images ne soient chargées. Cela donne alors un effet de clignotement désagréable quand une image remonte.
Pour contrer ce problème, je vous propose de créer notre propre contrôle pour afficher les images de manière plus gracieuse :
La déclaration d’un contrôle personnalisé se fait via la fonction WinJS.Class.define :
(function () {
var delayImageLoader = WinJS.Class.define(
function (element, options) {
this._element = element || document.createElement("div");
this.element.winControl = this;
WinJS.Utilities.addClass(this.element, "imageLoader");
WinJS.Utilities.query("img", element).forEach(function (img) {
img.addEventListener("load", function () {
WinJS.Utilities.addClass(img, "loaded");
});
});
},
{
element: {
get: function () { return this._element; }
},
});
WinJS.Namespace.define("UrzaGatherer.Tools", {
DelayImageLoader: delayImageLoader
});
})();
Comme vous pouvez le voir, le contrôle recherche toutes les enfants de type image et se branche sur leur événement “load”. Toute la magie est en fait dans le CSS puisque le contrôle ne fait que rajouter une classe CSS au début (imageLoader) et une autre à la fin du chargement (loaded).
Ces deux classes devant être globales, je les ai défini dans le fichier default.css :
.imageLoader img {
opacity: 0;
-ms-transform: scale(0.8, 0.8);
}
.imageLoader img.loaded {
opacity: 1;
-ms-transition: opacity ease-out 0.2s, -ms-transform ease-out 0.2s;
-ms-transform: scale(1, 1);
}
En utilisant les CSS3 transitions, l’image arrive avec une petite animation du meilleur effet 
Ajout du zoom sémantique
Finalement, nous allons ajouter un zoom sémantique (http://msdn.microsoft.com/en-us/library/windows/apps/hh465492.aspx) pour permettre à nos utilisateurs de sauter rapidement vers un bloc:
Pour ce faire, nous avons juste à embarquer notre ListView avec une autre ListView (celle responsable de la vue zoomée) au sein d’un contrôle de type WinJS.UI.SemanticZoom :
<div class="zoomControl" data-win-control="WinJS.UI.SemanticZoom">
<div class="blocksList" aria-label="List of blocks" data-win-control="WinJS.UI.ListView"
data-win-options="{itemTemplate:select('.itemTemplate'), groupHeaderTemplate:select('.headerTemplate'),
selectionMode:'none', swipeBehavior:'none', tapBehavior:'invoke',
layout:{type:WinJS.UI.GridLayout}}">
</div>
<div class="zoomedList" aria-label="Zoomed List of blocks" data-win-control="WinJS.UI.ListView"
data-win-options="{itemTemplate:select('.semanticZoomTemplate'), selectionMode:'none',
swipeBehavior:'none', tapBehavior:'invoke', layout:{type:WinJS.UI.GridLayout}}">
</div>
</div>
Pour synchroniser les deux listes il faut juste utiliser la même source de données pour les groupes dans la première liste et pour les éléments dans la seconde :
var groupDataSource = UrzaGatherer.Expansions.createGrouped(this.groupKeySelector,
this.groupDataSelector, this.groupCompare);
ui.setOptions(listView, {
itemDataSource: groupDataSource.dataSource,
groupDataSource: groupDataSource.groups.dataSource,
layout: new ui.GridLayout({ groupHeaderPosition: "top" })
});
ui.setOptions(zoomedListView, {
itemDataSource: groupDataSource.groups.dataSource
});
A suivre
Le prochain article introduira les fonctionnalités suivantes :
- La page des cartes
- La page des extensions
- Gestion du mode offline
- Gestion des settings