Comment “cuisiner” une application Windows 8 avec HTML 5, CSS3 et JavaScript en une semaine–Jour 1 - Eternal Coding - HTML5 / Windows / Kinect / 3D development - Site Home - MSDN Blogs

Comment “cuisiner” une application Windows 8 avec HTML 5, CSS3 et JavaScript en une semaine–Jour 1


 

Comment “cuisiner” une application Windows 8 avec HTML 5, CSS3 et JavaScript en une semaine–Jour 1

  • Comments 3

(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 premier jour de notre recette était dédicacé à la création de la page d’accueil et à la mise en place des données.

Aujourd’hui nous allons nous concentrer sur les écrans manquants et le support du mode déconnecté.

Le design global des écrans est le suivant :

image_thumb2

La solution associée à cet article est disponible ici : http://www.catuhe.com/msdn/urza/day1.zip

L’intégralité de la série est disponible ici:

L’écran des extensions

L’écran des extensions est construit grâce à une ListView qui sera utilisée pour afficher toutes les cartes appartenant à l’extension en cours.

Une première ligne contiendra les filtres et une seconde ligne servira à présenter la ListView :

<!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="expansion.css" rel="stylesheet">
    <script src="expansion.js"></script>
</head>
<body>
    <!--Templates-->
    <div class="itemTemplate" data-win-control="WinJS.Binding.Template">
        <div class="item-image-container" data-win-control="UrzaGatherer.Tools.DelayImageLoader" 
data-win-options="{root: 'cards'}"> <img class="item-image" data-win-bind="src: logo; alt: name" src="#" /> </div> <div class="item-overlay"> <h4 class="item-title" data-win-bind="textContent: name"></h4> </div> </div> <!--Content--> <div class="expansion fragment"> <header aria-label="Header content" role="banner"> <button class="win-backbutton" aria-label="Back"></button> <h1 class="titlearea win-type-ellipsis"><span class="pagetitle" id="pageTitle"></span> </h1> </header> <section aria-label="Main content" role="main"> <div class="filters"> <select id="orderSelect"> <option>By number</option> <option>By name</option> </select> <select id="colorFilter"> </select> <select id="authorFilter"> </select> <select id="rarityFilter"> </select> <select id="typeFilter"> </select> <select id="checkFilter"> <option>All</option> <option>Only missing</option> <option>All except missing</option> </select> <input type="search" id="textFilter" /> </div> <div class="cardsList" aria-label="List of cards" data-win-control="WinJS.UI.ListView" data-win-options="{itemTemplate:select('.itemTemplate'), selectionMode:'none',
swipeBehavior:'none', tapBehavior:'invoke', layout:{type:WinJS.UI.GridLayout}}"> </
div> </section> </div> </body> </html>

Comme toujours, le fichier html sert à positionner le squelette de la page et le fichier css sert à positionner les éléments. Par exemple, la disposition des filtres est construite en utilisant CSS3 Grid (http://msdn.microsoft.com/en-us/library/windows/apps/hh465327.aspx#css3_grid_alignment):

.expansion .filters {
    margin-left: 120px;
    -ms-grid-row: 1;
    -ms-grid-columns: auto auto auto auto auto auto 1fr;
    display: -ms-grid;
}

    .expansion .filters #orderSelect {
        -ms-grid-column: 1;
    }

    .expansion .filters #colorFilter {
        -ms-grid-column: 2;
        margin-left: 10px;
    }

    .expansion .filters #authorFilter {
        -ms-grid-column: 3;
        margin-left: 10px;
    }

    .expansion .filters #rarityFilter {
        -ms-grid-column: 4;
        margin-left: 10px;
    }

    .expansion .filters #typeFilter {
        -ms-grid-column: 5;
        margin-left: 10px;
    }

    .expansion .filters #checkFilter {
        -ms-grid-column: 6;
        margin-left: 10px;
    }

    .expansion .filters #textFilter {
        -ms-grid-column: 7;
        -ms-grid-column-align: end;
        margin-right: 10px;
    }

Gérer le niveau de zoom

UrzaGatherer doit pouvoir gérer plusieurs tailles d’affichage pour les cartes :image_thumb6

Pour le moment la taille des éléments est contrôlé par une classe CSS :

.expansion .cardsList .win-item {
    height: 340px;
    width: 240px;
    color: white;
    background-color: white;
    -ms-grid-columns: 1fr;
    -ms-grid-rows: 1fr;
    display: -ms-grid;
    outline: rgba(0, 0, 0, 0.8) solid 2px;
}

Pour changer la taille des éléments, il suffit donc de mettre à jour cette classe. Pour ce faire, il faut trouver l’objet css représentant le fichier css, trouver la règle css qui nous intéresse (".expansion .cardsList .win-item") et mettre à jour les règles width et height:

var updateSize = function (forceUpdate) {
    var zoomLevel = Windows.Storage.ApplicationData.current.roamingSettings.values["zoomLevel"];

    if (!zoomLevel)
        return;

    var css = UrzaGatherer.Tools.FindCSS("expansion.css");
    var level = zoomLevel / 100.0;

    for (var index = 0; index < css.cssRules.length; index++) {
        if (css.cssRules[index].selectorText == ".expansion .cardsList .win-item") {
            css.cssRules[index].style.width = (480 * level) + "px";
            css.cssRules[index].style.height = (680 * level) + "px";
        }
    }

    if (forceUpdate) {
        var listView = document.querySelector(".cardsList").winControl;
        listView.forceLayout();
    }
}

Le niveau de zoom courant est un réglage itinérant (http://msdn.microsoft.com/en-us/library/windows/apps/hh465094.aspx) que l’on récupère avec Windows.Storage.ApplicationData.current.roamingSettings.values["zoomLevel"].

Pour trouver le fichier css qui nous intéresse dans le DOM, le code est le suivant :

var findCSS = function (name) {
    for (var index = 0; index < document.styleSheets.length; index++) {
        if (document.styleSheets[index].href.indexOf(name) != -1)
            return document.styleSheets[index];
    }
}

WinJS.Namespace.define("UrzaGatherer.Tools", {
    FindCSS: findCSS
});

La fonction updateSize sera appelée par Windows à chaque fois que la fonction suivante sera appelée:

Windows.Storage.ApplicationData.current.signalDataChanged();

 

Ajout des réglages de l’application

Pour configurer le niveau de zoom, l’écran de réglages fournit par Windows est bien évidemment le meilleur endroit. En effet à chaque fois que vous avez des réglages généraux pour votre application, il faut les mettre dans cet écran (http://msdn.microsoft.com/en-us/library/windows/apps/Hh780611.aspx):

 

image_thumb12

Pour créer cet écran, il faut tout d’abord en définir la structure dans un fichier html (j’ai pour ma part utilisé le fichier default.html) :

<!--Settings-->
<div id="settingsDiv" data-win-control="WinJS.UI.SettingsFlyout" data-win-options="{width:'narrow'}">
    <div class="win-header">
        <button type="button" onclick="WinJS.UI.SettingsFlyout.show()" class="win-backbutton">
        </button>
        <div class="win-label">Settings</div>
    </div>
    <div class="win-content">
        <h4>Cards zoom level:</h4>
        <input type="range" id="zoomRange" min="20" max="80" value="50" />
    </div>
</div>

Comme vous pouvez le voir, j’utilise le controle WinJS.UI.SettingsFlyout avec une taille minimale (“narrow”).

Dans le fichier default.js, il faut ajouter un gestionnaire d’événements sur le controle zoomRange :

// Zoom range
var zoomRange = document.getElementById("zoomRange");

var zoomLevel = Windows.Storage.ApplicationData.current.roamingSettings.values["zoomLevel"];
if (zoomLevel)
    zoomRange.value = zoomLevel;

zoomRange.addEventListener("change", function () {
    Windows.Storage.ApplicationData.current.roamingSettings.values["zoomLevel"] = zoomRange.value;
    Windows.Storage.ApplicationData.current.signalDataChanged();
});

Ainsi à chaque fois que la valeur du contrôle changera, le réglage itinérant sera mis à jour et le reste de l’application sera notifié grâce à signalDataChanged.

Mode déconnecté

Toutes les cartes font en moyenne 150 Ko. De ce fait, il est important de fournir un moyen de ne les télécharger qu’une fois et de pouvoir les sauver en local.

Pour ce faire, nous allons changer la manière dont les images sont référencées. En effet, au lieu de passer par une référence en http://” nous allons donner une référence locale via le moniker ms-appdata:// et dans le cas ou cette référence ne marche pas (le fichier n’étant pas encore téléchargé par exemple), il nous suffira de le télécharger et de le sauver en local.

Comme nous avons utiliser le contrôle DelayImageLoader pour afficher nos images, il va être simple d’en modifier le comportement pour intégrer le support du mode déconnecté :

var delayImageLoader = WinJS.Class.define(
        function (element, options) {
            this._element = element || document.createElement("div");
            this.element.winControl = this;

            var downloadImage = function (source, filename, img) {
                var url = options ? UrzaGatherer.Root + "/" + options.root + "/" + source : 
UrzaGatherer.Root +
"/" + source; var xmlRequest = new XMLHttpRequest(); xmlRequest.open("GET", url, true); xmlRequest.responseType = "blob"; xmlRequest.onreadystatechange = function () { if (xmlRequest.readyState === 4) { UrzaGatherer.Stats.DecrementDownloads(); if (xmlRequest.status == 200) { var blob = xmlRequest.response; var input = blob.msDetachStream(); Windows.Storage.ApplicationData.current.localFolder.createFileAsync(filename, Windows.Storage.CreationCollisionOption.replaceExisting).then(
function (file) { file.openAsync(Windows.Storage.FileAccessMode.readWrite).then(
function (output) { Windows.Storage.Streams.RandomAccessStream.copyAsync(input, output).then(
function () { output.flushAsync().then(function () { input.close(); output.close(); img.src = "ms-appdata:///local/" + source; }); }); }); }); } } }; xmlRequest.send(null); } WinJS.Utilities.addClass(this.element, "imageLoader"); WinJS.Utilities.query("img", element).forEach(function (img) { img.addEventListener("load", function () { WinJS.Utilities.addClass(img, "loaded"); }); img.addEventListener("error", function (err) { if (img.src.substring(0, 5) == "http:") { } else { var source = img.src.replace("ms-appdata:///local/", ""); var filename = source.replace("/", "\\"); Windows.Storage.ApplicationData.current.localFolder.getFileAsync(filename).then(
function (file) { var url = options ? UrzaGatherer.Root + "/" + options.root + "/" + source :
UrzaGatherer.Root +
"/" + source; img.src = url; }, function () { // Not found UrzaGatherer.Stats.IncrementDownloads(); downloadImage(source, filename, img); }); } }); }); }, { element: { get: function () { return this._element; } }, }); WinJS.Namespace.define("UrzaGatherer.Tools", { DelayImageLoader: delayImageLoader });

En gérant l’évènement “error” pour chaque image, il est simple de détecter les erreurs de chargement et le cas-échéant de lancer le téléchargement en utilisant un XmlHttpRequest.

Ajout des filtres

Finalement, nous allons changer la manière de lier les données à la ListView pour prendre en compte les filtres. Pour ce faire, nous allons utiliser la fonction createFiltered de la WinJS.Binding.List :

updateLayout: function (element, viewState) {
    updateSize();

    var listView = element.querySelector(".cardsList").winControl;

    var sorted = expansion.cardsList.createSorted(sortFunction);
    var filtered = sorted.createFiltered(filterFunction);

    if (viewState === appViewState.snapped) {
    } else {
        ui.setOptions(listView, {
            itemDataSource: filtered.dataSource,
            layout: new ui.GridLayout({ groupHeaderPosition: "top" })
        });
    }
}

Ce code utilise la fonction filterFunction :

 var filterFunction = function (item) {
     var result = true;

     //Filters
     for (var index = 0; index < filters.length; index++) {
         var filter = document.getElementById(filters[index][0] + "Filter").value;

         if (filter.substring(0, 3) != "All") {
             result &= (item[filters[index][0]] == filter);
         }
     }

     // Text
     var textFilter = document.getElementById("textFilter").value.toLowerCase();

     if (textFilter != "") {
         result &= (item.name.toLowerCase().indexOf(textFilter) != -1
             || item.text.toLowerCase().indexOf(textFilter) != -1
             || item.flavor.toLowerCase().indexOf(textFilter) != -1);
     }

     return result;
 }

Au lieu de référencer directement les propriétés, notre fonction utilise un tableau filters :

var filters = [["color", "colors"], ["author", "authors"], ["rarity", "rarities"], ["type", "types"]];

Avec ce tableau, il est très simple d’ajouter de nouveaux filtres et seul le filtre sur le texte est traité à part car il travaille sur plusieurs propriétés.

Le tableau est aussi utilisé pour remplir les listes de filtres :

// Filters
for (var index = 0; index < filters.length; index++) {
    prepareFilter(filters[index][0], filters[index][1], that);
}
var prepareFilter = function (property, plural, that) {
    var filter = document.getElementById(property + "Filter");
    var results = [];

    for (var cardIndex = 0; cardIndex < expansion.cards.length; cardIndex++) {
        var value = expansion.cards[cardIndex][property];

        if (results.indexOf(value) == -1)
            results.push(value);
    }

    results.push("All " + plural);
    var sortedResults = results.sort(function (i0, i1) {
        if (i0 == i1)
            return 0;

        if (i0.substring(0, 3) == "All")
            return -1;

        if (i1.substring(0, 3) == "All")
            return 1;

        if (i0 > i1)
            return 1;

        return -1;
    });

    for (var index = 0; index < sortedResults.length; index++) {
        filter.options[index] = new Option(sortedResults[index]);
    }

    filter.addEventListener("change", function () {
        that.updateLayout(document, appView.value);
    });
};

 

L’écran de présentation des cartes

Cet écran est relativement simple :

image_thumb16

La carte est liée directement à l’interface simplement :

(function () {
    "use strict";

    var appView = Windows.UI.ViewManagement.ApplicationView;
    var appViewState = Windows.UI.ViewManagement.ApplicationViewState;
    var nav = WinJS.Navigation;
    var ui = WinJS.UI;
    var utils = WinJS.Utilities;

    var card;

    ui.Pages.define("/pages/card/card.html", {
        ready: function (element, options) {
            card = options.card;

            document.getElementById("pageTitle").innerText = card.name;

            document.querySelector("#picture").src = card.logo;
            document.querySelector(".item-number").innerText = card.number + " / " + 
card.expansion.cards.length; document.querySelector(
".item-type").innerText = card.type; document.querySelector(".item-color").innerText = card.color; document.querySelector(".item-power").innerText = card.power; document.querySelector(".item-text").innerText = card.text; document.querySelector(".item-flavor").innerText = card.flavor; document.querySelector(".item-author").innerText = card.author; document.querySelector("#expansion-picture").src = card.expansion.banner; this.updateLayout(element, appView.value); }, updateLayout: function (element, viewState) { if (viewState === appViewState.snapped) { } else { } } }); })();

Simple non ? Sourire

 

A suivre

Le prochain article introduira les points suivants:

  • Localisation
  • Vues “Snapped”
  • Plus de réglages
  • Adaptation aux différentes résolutions
Leave a Comment
  • Please add 2 and 5 and type the answer here:
  • Post
  • Great Post David!

  • Au lendemain de ma conférence "Windows 8 et HTML5", voilà que je me suis mis à chercher du contenu concernant la création d'appli pour Windows 8... je tombe sur un article fort intéressant... et lorsque je regarde le nom de l'auteur et la photo de l'auteur.... ah ah ! Je suis tombé vraiment par hasard (dont le nom du hasard est Google) sur ton blog et j'en profite pour te remercier pour la conférence d'hier à l'IESA. Une très bonne présentation, animée et vraiment agréable ! Je vais suivre tes articles de très près ! ;-)

  • Merci bcp :) C'est avec plaisir que j'ai animé cette session en tout cas!

Page 1 of 1 (3 items)