Статьи

Интеграция вашего приложения в Windows 8 Metro


Сегодня меню посвящено интеграции вашего приложения в Windows 8 Metro.

Первая часть интеграции была выполнена с привязанными представлениями и настройками, но теперь пришло время завершить работу с:

  • Поиск контракта
  • Поделиться контрактом
  • Контракт на подбор файла
  • Живая плитка
  • Вторичные плитки

Эти пять предметов действительно важны для симбиоза между Windows 8 Metro и вашим приложением.

 

образ

 

Очевидно, вы можете найти предыдущие эпизоды там:

И, как обычно, полное решение доступно здесь: http://www.catuhe.com/msdn/urza/day3.zip

Вы сказали «контракты»?

Контракт ( http://msdn.microsoft.com/en-us/library/windows/apps/hh464906.aspx ) — это определение технического интерфейса между вашим приложением и Windows 8 Metro. Это явно важный вопрос, поскольку он позволяет вашему приложению определять новые точки входа за пределы своей собственной плитки.

Затем вашему приложению разрешается принимать участие в некоторых важных службах Windows 8 Metro, таких как поиск, обмен данными или выбор файла, например.

Вы должны подумать о поддержке контрактов при создании приложения, потому что если вы их опустите, пользователи могут быть разочарованы.

Случайно (!!) UrzaGatherer может легко поддержать 3 основных контракта.

Поиск контракта

Договор поиска позволяет пользователю выполнять поиск в вашем приложении, но с помощью глобального общего интерфейса:

образ

Все приложения, поддерживающие поисковый контракт (и разрешенные пользователем), перечислены здесь с использованием сортировки «наиболее часто используемые первыми». Вы также можете видеть, что поиск приложений, настроек или файлов доступен там же.

Чтобы быть признанным Windows в качестве поискового поставщика, вам просто нужно объявить его с помощью манифеста приложения:

образ

Можно определить специальную целевую страницу для поиска, но для UrzaGatherer, так как мне нужно сначала загрузить данные, и я не хочу менять всю свою архитектуру, мне не нужно предоставлять специальную страницу.

Затем важно обновить файл default.js, чтобы определить факт запуска приложения из области поиска:

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

Кстати, если на панели поиска запущено приложение, нужно сохранить текст запроса:

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

Поэтому при загрузке страницы home.html , если текст запроса определен, вы можете перейти на страницу поиска ( в этом случае home.html использовался только для загрузки данных):

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

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

Кроме того, вам нужно учитывать тот факт, что поиск может быть запущен, когда ваше приложение уже запущено. Для этого вам необходимо прослушать специальное событие WinRT:

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

Как видите, когда отправляется запрос, вам просто нужно перейти на страницу поиска (передав ей текст запроса).

Страница поиска является клоном страницы расширения, но вместо отображения карт расширения она отображает карты, содержащие текст запроса:

образ

Обратите внимание, что страница поиска также содержит фильтры для уточнения поиска при необходимости.

Ваше приложение может также улучшить возможности поиска, предоставляя подсказки для поиска, когда пользователь вводит свой запрос:

 

образ

Для этого вам необходимо прослушать другое событие:

// 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);

Поделиться контрактом

Контракт на совместное использование — это действительно полезный контракт, так как он избавляет от боли внедрения клиента для всех служб обмена данными, которые вы хотите поддерживать (Facebook, Twitter, почта,…).

С контрактом на совместное использование приложения могут быть определены как источник общего ресурса или как цель общего ресурса:

образ

Например, на этом снимке экрана я могу поделиться своей карточкой в ​​Facebook или Twitter с помощью FlipToast или отправить ее по почте с помощью Courrier .

Ваше приложение теперь может сосредоточиться на собственном бизнесе!

Для UrzaGatherer каждая карта может предоставить свое изображение службе обмена данными. Все цели общего доступа могут затем решить, что они хотят делать (опубликовать в Twitter, отправить по почте и т. Д.), Когда они выбраны пользователем.

Для этого вам просто нужно прослушать событие datarequested из DataTransferManager :

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

Используемая функция следующая:

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();
    });
};

Функция просто должна заполнить запрос информацией с карты. Обратите внимание на использование getDeferral, потому что функция асинхронная (это означает, что процесс заполнения не завершается, когда функция возвращается)

Контракт FileOpenPicker

Контракт на выборку файлов позволяет приложению предоставлять файлы другим приложениям (например, Skydrive позволяет пользователям выбирать файл в облаке как локальный). Существует также контракт FileSavePicker для получения файлов из других приложений (например, Skydrive позволяет пользователям сохранять файл в облаке).

Для UrzaGatherer вы можете предоставить изображение для каждой карты. Например, начиная с почтового приложения:

образ

Вы можете выбрать изображение внутри UrzaGatherer, чтобы добавить его в виде прикрепленного файла:

образ

Поскольку UrzaGatherer является провайдером файлов, вы можете увидеть его в списке зарегистрированных провайдеров (на том же уровне, что и SkyDrive). Выбрав его, UrzaGatherer запускается в интерфейсе Windows picker, и пользователь может выбрать карту, чтобы получить файл, содержащий ее изображение:

образ

Чтобы зарегистрировать ваше приложение в качестве поставщика файлов, вы можете использовать процесс, аналогичный поисковому контракту:

image_thumb6

С точки зрения вашего кода это легко! Прежде всего, вы должны принять во внимание, что приложение может быть запущено средством выбора файлов:

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.fileOpenPicker:
        UrzaGatherer.FileOpenPickerUI = eventObject.detail.fileOpenPickerUI;
        break;
    default:

Затем, когда вы попадаете на страницу расширения, когда пользователь нажимает на карточку, вместо того, чтобы перейти на страницу карточки, вам просто нужно получить файл:

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 });
},

 Следующая функция используется для получения файла с карты:

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);
}

Живая плитка

Windows 8 Metro теперь основана на плитках для запуска приложений. Плитка похожа на супер-иконку ( http://msdn.microsoft.com/en-us/library/windows/apps/hh779724.aspx ), которая динамична и может обновляться многими способами (приложение, фоновая задача, служба уведомлений):

образ

Отправить обновление на плитку

Основная плитка UrzaGatherer обновляется каждый раз, когда вы переходите на страницу карты. Используемый код является следующим:

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);

    });
}

Прежде всего вы должны выбрать конфигурацию вашей плитки с помощью функции Notifications.TileUpdateManager.getTemplateContent ( http://msdn.microsoft.com/en-us/library/windows/apps/hh761491.aspx ).

Эта функция возвращает объект XML, который необходимо заполнить, чтобы обновить плитку.

Создать новую картинку для плитки

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):

image

 

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:

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

END