How to cook a complete Windows 8 application with HTML5, CSS3 and JavaScript in a week – Day 3

Today the menu is about integrating your application into Windows 8 Metro.

The first part of the integration was done with the snapped views and settings but it is now time to finish the job with:

  • Search contract
  • Share contract
  • File picking contract
  • Live tile
  • Secondary tiles

These five subjects are really important for the symbiosis between Windows 8 Metro and your application.

 

  

And as usual the complete solution is available here: https://www.catuhe.com/msdn/urza/day3.zip

The complete series can be found here:

You said “contracts” ?

A contract (https://msdn.microsoft.com/en-us/library/windows/apps/hh464906.aspx) is the definition of a technical interface between your application and Windows 8 Metro. It is a clearly important subject because it allows your application to define new entry points beyond its own tile.

Your application is then allowed to take part of some important services of Windows 8 Metro like the search, the data sharing or the file selection for instance.

You must think of supporting contracts during the creation of your application because if you omit them users can be disappointed.

By chance (!!) UrzaGatherer can easily supports the 3 main contracts.

Search contract

The search contract allows the user to search within your application but from a global common interface:

All the applications supporting the search contract (and allowed by the user) are listed here using a “most used first” sort. You can also see that searching apps, settings or files is available in the same place.

To be recognized by Windows as a search provider, you just have to declare it using the application manifest:

It is possible to define a special landing page for the search but for UrzaGatherer as I need to load data first and I don’t want to change all my architecture, I don’t need to provide a special page.

It is then important to update the default.js file to detect the fact that the application was launched from the search pane:

    app.onactivated = function (eventObject) {
        if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch ||
            eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.search ||

By the way, if the search pane launched the application, you have to save the query text:

switch (eventObject.detail.kind) {
    case Windows.ApplicationModel.Activation.ActivationKind.search:
        UrzaGatherer.QueryText = eventObject.detail.queryText;
        break;

So when the home.html page is loaded, if the query text is defined, you can jump to the search page (home.html was in this case only used to load data):

if (UrzaGatherer.QueryText) {
    var queryText = UrzaGatherer.QueryText;
    delete UrzaGatherer.QueryText;

    nav.navigate("/pages/search/search.html", { queryText: queryText });
}

Furthermore, you need to handle the fact that the search can be launched when your application is already launched. To do so, you have to listen for a special WinRT event:

// Search
var searchPane = Windows.ApplicationModel.Search.SearchPane.getForCurrentView();
searchPane.addEventListener("querysubmitted", function (e) {
    nav.navigate("/pages/search/search.html", { queryText: e.queryText });
}, false);

As you can see, when a request is emitted, you just have to jump to the search page (passing it the query text).

The search page is a clone of the expansion page but instead of displaying expansion’s cards it displays the cards containing the query text:

Please note that the search page also contains filters to refine the search if required.

Your application can also improve the search experience by providing search suggestions as user enters his query:

 

To do so, you have to listen another event:

// Register to Handle Suggestion Request
searchPane.addEventListener("suggestionsrequested", function (e) {
    var query = e.queryText;
    var maxNumberOfSuggestions = 5;
    var suggestionRequest = e.request;

    for (var i = 0, len = UrzaGatherer.Cards.length; i < len; i++) {
        if (UrzaGatherer.Cards[i].name.substr(0, query.length).toLowerCase() === query) {
            suggestionRequest.searchSuggestionCollection.appendQuerySuggestion(UrzaGatherer.Cards[i].name);
            if (suggestionRequest.searchSuggestionCollection.size === maxNumberOfSuggestions) {
                break;
            }
        }
    }
}, false);

 

Share contract

The share contract is a really useful contract as it gets rid of the pain of implementing a client for every data sharing services you want to support (Facebook, Twitter, mail, …).

With the share contract, applications can be defined as a share source or as a share target:

For example in this screen capture, I can share my card to Facebook or Twitter using FlipToast or I can send it by mail using Courrier.

Your application can now focus on its own business!

For UrzaGatherer, each card can exposed its image to the data sharing service. All share targets can then decide what they want to do with (publish on Twitter, send by mail, etc.) when they are selected by the user.

To do so, you just have to listen for the datarequested event from the DataTransferManager :

// Share
var dataTransferManager = Windows.ApplicationModel.DataTransfer.DataTransferManager.getForCurrentView();
dataTransferManager.addEventListener("datarequested", shareFunction);

The used function is the following:

var shareFunction = function (e) {
    var request = e.request;
    var deferral = request.getDeferral();

    request.data.properties.title = card.name + UrzaGatherer.Tools.GetString("ShareTitle") 
+ card.expansion.name; request.data.properties.description = UrzaGatherer.Tools.GetString(
"ShareDescription"); UrzaGatherer.Tools.GetCardFile(card).then(function (file) { var streamReference = Windows.Storage.Streams.RandomAccessStreamReference.createFromFile(file); request.data.properties.thumbnail = streamReference; request.data.setStorageItems([file]); request.data.setBitmap(streamReference); deferral.complete(); }); };

The function just have to fill in the request with information from the card. Please note the use of getDeferral because the function is asynchronous (meaning that the fill process is not finished when the function returns)

FileOpenPicker contract

The file picking contract allows application to provide files to others application (for instance, Skydrive allows users to pick a file in the cloud as it was local). There is also a FileSavePicker contract to get files from others application (for instance, Skydrive allows users to save a file in the cloud).

For UrzaGatherer, you can provide an image for each card. For instance, starting from the mail application:

You can decide to pick a picture inside UrzaGatherer to add it as an attached file:

As UrzaGatherer is a file provider, you can see it in the list of registered providers (at the same level as SkyDrive). By selecting it, UrzaGatherer is launched inside the Windows picker UI and the user can choose a card to get a file containing its picture:

To register your application as a file provider you can use a similar process as the search contract:

image_thumb6

From the point of view of your code, it is easy! First of all, you have to take in account that the application can be launched by the file picker:

app.onactivated = function (eventObject) {
        if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch ||
            eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.search ||
            eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.fileOpenPicker){

 

Then, you have to save a link to the file picker UI to be able to transmit files afterward:

switch (eventObject.detail.kind) {
    case Windows.ApplicationModel.Activation.ActivationKind.fileOpenPicker:
        UrzaGatherer.FileOpenPickerUI = eventObject.detail.fileOpenPickerUI;
        break;
    default:

Then, once you are in the expansion page, when the user clicks on a card, instead of jumping to the card page you just have to retrieve a file:

itemInvoked: function (eventObject) {
    if (UrzaGatherer.FileOpenPickerUI) {
        var item = eventObject.detail.itemPromise.then(function (invokedItem) {
            var card = invokedItem.data;

            UrzaGatherer.Tools.GetCardFile(card).then(function (file) {
                UrzaGatherer.FileOpenPickerUI.addFile(card.name, file);
            });
        });
        return;
    }
    nav.navigate("/pages/card/card.html", { cardIndex: eventObject.detail.itemIndex, cards: filtered });
},

The following function is used to get a file from a card :

var getCardFile = function (card) {
    // HTTP ?
    if (card.logo.substring(0, 5).toLowerCase() == "http:") {
        var uri = new Windows.Foundation.Uri(card.logo);
        var thumbnail = Windows.Storage.Streams.RandomAccessStreamReference.createFromUri(uri);

        return Windows.Storage.StorageFile.createStreamedFileFromUriAsync(card.name + ".jpg", 
uri, thumbnail); }
// Local ? return Windows.Storage.ApplicationData.current.localFolder.getFileAsync(card.alt); }

Live tile

Windows 8 Metro is now based on tiles to launch applications. A tile is like a super icon (https://msdn.microsoft.com/en-us/library/windows/apps/hh779724.aspx) which is dynamic and updatable by many ways (application, background task, notification service):

Send an update to the tile

The UrzaGatherer main tile is updated every time you go to the card page. The code used is the following:

var updateTile = function (card) {
    var Notifications = Windows.UI.Notifications;
    var Imaging = Windows.Graphics.Imaging;

    var tileXml = Notifications.TileUpdateManager.getTemplateContent(
Notifications.TileTemplateType.tileWideSmallImageAndText02);
var tileTextAttributes = tileXml.getElementsByTagName("text"); tileTextAttributes[0].appendChild(tileXml.createTextNode("UrzaGatherer")); tileTextAttributes[1].appendChild(tileXml.createTextNode(card.name)); tileTextAttributes[2].appendChild(tileXml.createTextNode(card.expansion.name)); tileTextAttributes[3].appendChild(tileXml.createTextNode(card.expansion.block.name)); var filename = card.alt.replace(".jpg", "_small.png"); rescaleImage(card.logo, 150, 150, filename, true, function (appDatafilename) { // Image var tileImageAttributes = tileXml.getElementsByTagName("image"); tileImageAttributes[0].setAttribute("src", appDatafilename); // Square var squareTileXml = Notifications.TileUpdateManager.getTemplateContent(
Notifications.TileTemplateType.tileSquareImage);
var squareTileImageAttributes = squareTileXml.getElementsByTagName("image"); squareTileImageAttributes[0].setAttribute("src", appDatafilename); var node = tileXml.importNode(squareTileXml.getElementsByTagName("binding").item(0), true); tileXml.getElementsByTagName("visual").item(0).appendChild(node); // Update var tileNotification = new Notifications.TileNotification(tileXml); tileNotification.tag = card.id; var tileUpdater = Windows.UI.Notifications.TileUpdateManager.createTileUpdaterForApplication(); tileUpdater.enableNotificationQueue(true); tileUpdater.update(tileNotification); }); }

First of all you have to choose the configuration of your tile with the Notifications.TileUpdateManager.getTemplateContent function(https://msdn.microsoft.com/en-us/library/windows/apps/hh761491.aspx).

This function returns a XML object that you have to fill in to update your tile.

Generate a new picture for the tile

One of the problem I faced when I created the code to update the tile was the size of the awaited pictures. I had to rescale my image to fit in the tile format. I used the following function to do so:

var rescaleImage = function (src, destinationWidth, destinationHeight, localfilename, fillAlpha, then) {
    var Imaging = Windows.Graphics.Imaging;
    var image = new Image();

    // lors du chargement
    image.addEventListener('load', function () {
        var canvas = document.createElement('canvas');

        canvas.width = destinationWidth;
        canvas.height = destinationHeight;

        var targetWidth;
        var targetHeight;

        if (this.width > this.height) {
            var ratio = destinationWidth / this.width;
            targetWidth = destinationWidth;
            targetHeight = this.height * ratio;
        }
        else {
            var ratio = destinationHeight / this.height;
            targetWidth = this.width * ratio;
            targetHeight = destinationHeight;
        }

        var context = canvas.getContext('2d');
        if (fillAlpha)
            context.clearRect(0, 0, canvas.width, canvas.height);
        else {
            context.fillStyle = "#fff";
            context.fillRect(0, 0, canvas.width, canvas.height);
        }
        context.drawImage(this, (canvas.width - targetWidth) / 2, (
canvas.height - targetHeight) / 2, targetWidth, targetHeight); Windows.Storage.ApplicationData.current.localFolder.createFileAsync(localfilename, Windows.Storage.CreationCollisionOption.replaceExisting).then(
function (file) { file.openAsync(Windows.Storage.FileAccessMode.readWrite).then(function (stream) { Imaging.BitmapEncoder.createAsync(Imaging.BitmapEncoder.pngEncoderId, stream).then(
function (encoder) { encoder.setPixelData(Imaging.BitmapPixelFormat.rgba8, Imaging.BitmapAlphaMode.straight, canvas.width, canvas.height, 96, 96, new Uint8Array(context.getImageData(0, 0, canvas.width, canvas.height).data)); encoder.flushAsync().then(function () { stream.flushAsync().then(function () { stream.close(); if (then) then("ms-appdata:///local/" + localfilename.replace("\", "/")); }); }); }); }); }); }, false); // Chargement image.src = src; }

Its operation is as follows:

  • Create a HTML image and listen for the load event
  • Set the source of the image to the URL of the source picture
  • After loading the image, draw it on a well sized canvas
  • Get the pixels of the canvas using getImageData
  • Create a BitmapEncoder to generate a bitmap file
  • Using a typed array (Uint8Array ) copy (without any conversion) the canvas pixels to the BitmapEncoder using setPixelData function
  • Save it to a file and voila !

Cycling on many updates

Using a tileUpdater (through Windows.UI.Notifications.TileUpdateManager.createTileUpdaterForApplication function), you can register an update of your tile.

You can even define a queue of notifications to create a cool cycling effect on your tile (like a FIFO of updates).

And to reduce the disk footprint of this system, you have to save the path of every generated picture in order to remove them afterwards:

// Store and clean
var previousTilesValue = Windows.Storage.ApplicationData.current.localSettings.values["previousTiles"];
var previousTiles = [];

if (previousTilesValue)
    previousTiles = JSON.parse(previousTilesValue);

previousTiles.push(filename);

if (previousTiles.length > 5) {
    var toRemove = previousTiles.shift();

    Windows.Storage.ApplicationData.current.localFolder.getFileAsync(toRemove).then(function (file) {
        file.deleteAsync().done();
    });
}

Windows.Storage.ApplicationData.current.localSettings.values["previousTiles"] = 
JSON.stringify(previousTiles);

 

Secondary tiles

The secondary tiles work the way as the main tile but they give users the opportunity to create a deep link into your application. Indeed, instead of pointing to the root page (default.html), they can provide arguments to point to any part of the application.

For UrzaGatherer, the expansion page can create secondary tiles via its application bar (the bottom bar):

 

So first of all you have to create the appbar:

<!--App bar-->
<div data-win-control="WinJS.UI.AppBar" data-win-options="">
    <button data-win-control="WinJS.UI.AppBarCommand" data-win-options="{id:'pinButton', 
icon:'pin',section:'global'}"> </
button> </div>

Then you have to register to the click on your appbar button:

var pinByElementAsync = function (then) {
    var tileID = expansion.id;
    var shortName = expansion.name;
    var displayName = expansion.name;
    var tileOptions = Windows.UI.StartScreen.TileOptions.showNameOnWideLogo;
    var tileActivationArguments = expansion.id;

    UrzaGatherer.Tools.RescaleImage(expansion.logo, 150, 150, 
expansion.logoPath.replace(
".png", "_uri.png"), false, function (uriLogo) { var tile = new Windows.UI.StartScreen.SecondaryTile(tileID, shortName, displayName,
tileActivationArguments, tileOptions,
new Windows.Foundation.Uri(uriLogo)); tile.foregroundText = Windows.UI.StartScreen.ForegroundText.dark; UrzaGatherer.Tools.RescaleImage(expansion.logo, 310, 150,
expansion.logoPath.replace(
".png", "_wide.png"), false, function (wideLogo) { tile.wideLogo = new Windows.Foundation.Uri(wideLogo); var element = document.getElementById("pinButton"); var selectionRect = element.getBoundingClientRect(); tile.requestCreateAsync({ x: selectionRect.left, y: selectionRect.top }).then(then); }); }); };

You can notice the reuse of the RescaleImage function in order to scale the logos to an appropriate size.

The tile is attached with an argument (tileActivationArguments) which will be transmitted to the application when the users will click on the tile.

This argument can be retrieved with the following code:

 app.onactivated = function (eventObject) {
     if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch ||
         eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.search ||
         eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.fileOpenPicker) {

switch (eventObject.detail.kind) { case Windows.ApplicationModel.Activation.ActivationKind.search: UrzaGatherer.QueryText = eventObject.detail.queryText; break; case Windows.ApplicationModel.Activation.ActivationKind.fileOpenPicker: UrzaGatherer.FileOpenPickerUI = eventObject.detail.fileOpenPickerUI; break; default: UrzaGatherer.Arguments = eventObject.detail.arguments; break; }

So when home.html is loaded, you can check if UrzaGatherer.Arguments is not null in order to directly jump to the expansion page:

if (UrzaGatherer.Arguments) {
    var expansionID = parseInt(UrzaGatherer.Arguments);
    delete UrzaGatherer.Arguments;

    var expansion;

    for (var index = 0; index < UrzaGatherer.Expansions.length; index++) {
        var exp = UrzaGatherer.Expansions.getAt(index);

        if (exp.id == expansionID) {
            expansion = exp;
            break;
        }
    }

    nav.navigate("/pages/expansion/expansion.html", { expansion: expansion });
}

Your application can so have many associated tiles on the home screen:

image_thumb2

Obviously, in the same way as the main tile, secondary tiles are updatable.

 

To be continued

The next stop will be about adding Live SDK and Skydrive support in order to handle your own collection.