Спонсор: Microsoft
Опубликован: 21.03.2013 | Доступ: свободный | Студентов: 6315 / 126 | Длительность: 06:49:00
Лабораторная работа 1:

Создание приложения из шаблона

< Лекция 1 || Лабораторная работа 1: 12 || Онлайн-консультация 1 >

Для разработчиков на HTML + JS

Версия для для разработчиков на XAML + C# - здесь "Создание приложения из шаблона"
  • Не забудьте убедиться, что ваше приложение корректно работает при отключенном интернет-соединении, а также в соответствиями с требованиями Windows Store информирует пользователя о том, как оно работает (или не работает) с его персональными данными.
  • В качестве подтверждения выполнения лабораторной работы от вас потребуется предоставить скриншот работающего приложения с подключенным внешним источником данных.

Создание проекта

Откройте Visual Studio 2012, выберите создание нового проекта (File -> New -> Project…). Далее в шаблонах выберите проект на JavaScript -> Windows Store. Укажите, что будете использовать шаблон Grid App.


Укажите любое название проекта, например, Reader App.

Изучите структуру проекта:

  1. package.appxmanifest — манифест приложения, описывающий ключевые настройки, используемые возможности, название приложения, плитки и другие параметры;
  2. default.html — формальная стартовая страница приложения;
  3. pages\groupedItems — папка с html-, js- и css-файлами для страницы представления групп контента (подгружается в стартовую страницу);
  4. pages\groupDetail — папка с html-, js- и css-файлами для страницы отображения группы новостей (записей в rss-потоке), соответствующих одному потоку;
  5. pages\itemDetail — папка с html-, js- и css-файлами для страницы отображения каждой из новостей отдельно;
  6. js\data.js — js-файл, описывающий работу с данными (содержит зашитые внутрь демонстрационные данные);
  7. js\default.js — описывает события, необходимые для инициализации приложения;
  8. js\navigator.js — описывает логику переходов между страницами и необходимые для этого события и объекты;
  9. images\logo.png — изображение, используемое для квадратной плитки;
  10. images\smalllogo.png — изображение, используемое при перечислении приложения в операционной системе, например, при поиске или выборе приложений для поиска или общего доступа;
  11. images\splashscreen.png — загрузочное изображение, показываемое при открытии приложения;
  12. images\storelogo.png — изображение, используемое в интерфейсе магазина приложений (Windows Store).

Также по умолчанию к проекту подключена библиотека WinJS, содержащая наборы стилей для темной и светлой тем и вспомогательных функций и объектов на JavaScript.

Попробуйте запустить приложение, нажав F5, зеленую стрелочку или выбрав Debug -> Start Debugging.


Изучите работу приложения:

  • Попробуйте нажать на отдельную серую плитку или на заголовок группы.
  • Попробуйте нажать на кнопку назад во внутренних страницах.
  • Попробуйте перевести приложение в Snapped-режим.
Замена источников данных

Откройте файл js\data.js. В нем объявлено несколько важных функций и объектов, которые мы также будем использовать.

Первым делом необходимо избавиться от строчек кода, генерирующих примеры данных. Для этого удалите следующие строчки:

Вставка данных в список:

// You can add data from asynchronous sources whenever it becomes available.
  generateSampleData().forEach(function (item) {
      list.push(item);
  });

Генерация примеров данных:

// Returns an array of sample data that can be added to the application's
    // data list. 
    function generateSampleData() {
        var itemContent = "Curabitur class … ";
        var itemDescription = "Item Description: Pellente…";
        var groupDescription = "Group Description: Lorem …";
        …
        return sampleItems;
    }

Если вы запустите приложение, оно продолжит работать, только в нем будут отсутствовать какие-либо данные. Остановите отладку и вернитесь к файлу data.js. Давайте вкратце пройдемся по его структуре, чтобы были понятны дальнейшие действия:

var list = new WinJS.Binding.List();

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

var groupedItems = list.createGrouped(
    function groupKeySelector(item) { return item.group.key; },
    function groupDataSelector(item) { return item.group; }
 );

На основании списка создается группированная коллекция, при создании которой с помощью специальных функций указывается, как элементы коллекции разделять на отдельные группы.

WinJS.Namespace.define("Data", {
    items: groupedItems,
    groups: groupedItems.groups,
    getItemReference: getItemReference,
    getItemsFromGroup: getItemsFromGroup,
    resolveGroupReference: resolveGroupReference,
    resolveItemReference: resolveItemReference
 });

Через функцию define в библиотеке WinJS (Namespace) прописывается глобальный объект Data, который будет доступен из других частей программы для работы с нашими данными. Внутри объекта прописываются ссылки на группированную коллекцию, список групп внутри нее и ряд функций, описанных в файле data.js и используемых для работы с коллекцией и извлечения данных.

Оставшиеся 4 функции (getItemReference, getItemsFromGroup, resolveGroupReference и resolveItemReference) используются для сравнения объектов, извлечения подмножеств элементов, принадлежащих одной группе, определения группы по ключу и элемента по набору уникальных идентификаторов.

Теперь самое время приступить к добавлению наших собственных данных. В качестве источников мы будет использовать внешние RSS-потоки.

Замечание: в данной лабораторной работе описывается работа с RSS-источниками, вы также можете добавить поддержку Atom-потоков, так как они имеют схожую структуру и ключевая разница будет в адресации искомых полей данных.

Вернитесь к началу файла и после строчки "use strict" опишите блоги, информацию из которых вы будете выводить (в вашем приложении это должны быть другие источники, с которыми вам интересно работать):

var blogs = [
    {
        key: "ABlogging",
        url: "http://blogs.windows.com/windows/b/bloggingwindows/rss.aspx",
        title: 'Blogging Windows', rsstitle: 'tbd', updated: 'tbd',
        dataPromise: null
    },
    {
        key: "BExperience",
        url: 'http://blogs.windows.com/windows/b/windowsexperience/rss.aspx',
        title: 'Windows Experience', rsstitle: 'tbd', updated: 'tbd',
        dataPromise: null
    },
    {
        key: "CExtreme",
        url: 'http://blogs.windows.com/windows/b/extremewindows/rss.aspx',
        title: 'Extreme Windows', rsstitle: 'tbd', updated: 'tbd',
        dataPromise: null
    }];

Для описания каждого блога (группы контента) мы указываем:

  • ключ — key (так как сортировка групп будет по ключу, мы также добавили в начале группы латинские буквы для явного задания сортировки — в реальном проекте это можно сделать более элегантным способом),
  • ссылку на RSS-поток — url,
  • название потока — title,
  • несколько заглушек:
    • реальное название блога — rsstitle,
    • дата обновления (updated),
    • указатель на dataPromise — "обещание" загрузить этот поток и его обработать.

Чтобы превратить ссылки в данные на компьютере, информацию по ним необходимо загрузить. Для этого после строчки var list = new WinJS.Binding.List(); добавьте новую функцию getBlogPosts, которая как раз будет заниматься загрузкой:

function getBlogPosts(postsList) {
        blogs.forEach(function (feed) {
            // Создание Promise
            feed.dataPromise = WinJS.xhr( { url: feed.url } ).then(
                function (response) {
                    if (response) {
                        var syndicationXML = response.responseXML || 
                           (new DOMParser()).parseFromString(response.responseText, "text/xml");
                        processRSSFeed(syndicationXML, feed, postsList);
                    }
                }
            );
        });

        return postsList;
    }

В данной функции мы в цикле проходимся по всем блогам, для каждой ссылки через функцию WinJS.xhr создаем асинхронную Promise-обертку вокруг XMLHttpRequest-запроса и после получения результата (then) передаем полученный ответ на обработку в функцию processRSSFeed, которую мы опишем ниже.

Замечание: для используемых нами блогов нет необходимости в дополнительной проверке, что мы получили ответ в виде XML (наличие responseXML), однако, в общем случае это неверно: некоторые блоги из-за неверных настроек сервера/движка отдают RSS-поток как текстовое содержимое, которое необходимо дополнительно обрабатывать, если мы хотим работать с ним как с XML-файлом.

Для обработки потока добавьте ниже еще одну функцию — processRSSFeed:

function processRSSFeed(articleSyndication, feed, postsList) {
        // Название блога
        feed.rsstitle = articleSyndication.querySelector("rss > channel > title").textContent;
        // Используем дату публикации последнего поста как дату обновления
        var published = articleSyndication.querySelector("rss > channel > item > pubDate").textContent;

        // Преобразуем дату в удобрый формат
        var date = new Date(published);
        var dateFmt = new Windows.Globalization.DateTimeFormatting.DateTimeFormatter(
           "day month.abbreviated year.full");
        var blogDate = dateFmt.format(date);
        feed.updated = "Обновление: " + blogDate;

        // Обработка постов
        getItemsFromRSSFeed(articleSyndication, feed, postsList);
    }

В данной функции мы пользуемся тем фактом, что на входе имеем XML-файл с известной структурой (RSS 2.0), по которому можно перемещаться используя DOM-модель, в частности, функцию querySelector, используя которую, можно вытаскивать из полученного документа необходимые данные.

Полученное текстовое значение последней даты обновления мы преобразуем в нужный формат, используя функции глобализации, доступные через API WinRT.

В конце мы передаем документ на дальнейшую обработку в функцию getItemsFromRSSFeed, которая выделяет отдельные посты и заносит их в коллекцию posts.

Добавьте ниже следующую функцию:

function getItemsFromRSSFeed(articleSyndication, feed, postsList) {
        var posts = articleSyndication.querySelectorAll("item");

        // Цикл по каждому посту в потоке
        var length = posts.length;
        for (var postIndex = 0; postIndex < length; postIndex++) {
            var post = posts[postIndex];
            // форматирование даты
            var postPublished = post.querySelector("pubDate").textContent;
            var postDate = new Date(postPublished);
            var monthFmt = new Windows.Globalization.DateTimeFormatting.DateTimeFormatter("month.abbreviated");
            var dayFmt = new Windows.Globalization.DateTimeFormatting.DateTimeFormatter("day");
            var yearFmt = new Windows.Globalization.DateTimeFormatting.DateTimeFormatter("year.full");
            var timeFmt = new Windows.Globalization.DateTimeFormatting.DateTimeFormatter("shorttime");
            
            var postContent = toStaticHTML(post.querySelector("description").textContent);
            
            var postItem = {
                index: postIndex,
                group: feed,
                // заголовок поста
                title: post.querySelector("title").textContent,
                // дата и отдельные компоненты
                postDate: postDate,
                month: monthFmt.format(postDate).toUpperCase(),
                day: dayFmt.format(postDate),
                year: yearFmt.format(postDate),
                time: timeFmt.format(postDate),
                // содержимое поста
                content: postContent,
                // ссылка на пост
                link: post.querySelector("link").textContent
            };

            postsList.push(postItem);
        }
    }

В данной функции мы проходимся в цикле по всем постам в полученном потоке, выбирая из XML-описания нужные поля (заголовок, дату публикации, контент и т.п.), после чего собираем нужную информацию в один объект (postItem), который добавляем в список постов.

Обратите внимание на форматирование дат и привязку каждого поста к соответствующей группе. Также заметьте, что для повышения безопасности полученное содержимое поста мы приводим к статическому виду с помощью функции toStaticHTML.

Замечание: в общем случае работы с RSS-потоками из неконтролируемых источников нужно внимательно контролировать (и исследовать) получаемые данные. На практике часть данных по искомым полям может отсутствовать, поэтому прежде, чем извлекать текстовое содержимое (textContent) необходимо убеждаться, что предыдущая операция вернула не null-значение. В некоторых случаях полное содержимое поста может скрываться за элементами encoded или full-text или и вовсе отсутствовать. Также известны случаи, когда сервер отдает дату в неправильном формате, в результате чего стандартный парсер выдает исключение.

Добавьте ниже следующую строчку, чтобы запустить считывание блогов:

list = getBlogPosts(list);

Запустите приложение на отладку:

Проверка интернет-соединения

Следующий важный шаг: убедиться, что приложение корректно себя ведет при отсутствии интернет-соединения. Если вы попробуете запустить текущую версию приложения с отключенной сетью, например, включив Airplane-режим, оно должно вызвать ошибку из-за невозможности скачать нужные поток. (Возможно, они успели закешироваться, поэтому для надежности эксперимента можно подключить какие-нибудь другие потоки.)

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

Чтобы проверить наличие интернета, откройте файл js\data.js и добавьте в него следующую функцию:

function isInternetConnection() {
        var connectionProfile = Windows.Networking.Connectivity.NetworkInformation.getInternetConnectionProfile();
        return (connectionProfile != null);
    }

Здесь мы обращаемся к WinRT, чтобы узнать текущее состояние сети. Это уже хорошая информация, хотя она и не дает 100% гарантии, что доступ к интернету и нужному потоку действительно есть.

Теперь давайте добавим функцию, которая будет выводить сообщение об ошибке при наличии проблем:

function showConnectionError(msg, donotexit) {
        msg = (msg != undefined) ? msg : "";

        var messageDialog = new Windows.UI.Popups.MessageDialog(msg, "Can not connect");
        
        messageDialog.commands.append(new Windows.UI.Popups.UICommand("Ok", null, 1));

        messageDialog.showAsync().done(function (command) {

            if (!donotexit && command.id == 1) {
                MSApp.terminateApp({
                    number: 0,
                    stack: "",
                    description: "No internet connection"
                });
            }
        });
    }

Данная функция, используя WinRT API, выводит заданное сообщение об ошибке и, по умолчанию, если не выставлен флаг donotexit (или он равен false), завершает приложение.

Замечание: в реальном приложении логика поведения может отличаться. Например, приложение может намеренно кешировать данные или предлагать пользователю попробовать еще раз скачать потоки с сервера, если была временная потеря соединения.

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

list = getBlogPosts(list);

на следующие:

function tryUpdateData() {
        if (isInternetConnection()) {
            list = getBlogPosts(list);            
        } else {
            showConnectionError("Please check your internet connection. ");
        }
    };

    tryUpdateData();

Фактически, мы обернули обращение данных в проверку наличия интернет-соединения. Если его нет, приложение выдает сообщение об ошибке и закрывается:


Теперь давайте вернемся к функции getBlogPosts. Здесь тоже есть вероятность получения ошибки, например, если какой-то RSS-поток перестал работать, в этом случае наша обертка над XHR вызовет исключение, которое нужно перехватить.

Попробуйте заменить одну из ссылок на RSS на неправильную и запустить приложение (здесь мы предполагаем, что ответ сервера будет соответствующим неправильной ссылке).

Чтобы обработать исключительную ситуацию, в описании Promise нужно добавить функцию, вызываемую при возникновении ошибки. Сразу после внутренней анонимной функции function (response) {…} добавьте через запятую:

function (error) {
                    showConnectionError("Can't get rss updates for " + feed.title + ". Used source: " + feed.url, true);
                }

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

Попробуйте сделать одну из ссылок неправильной и запустить приложение:

Теперь пользователь может догадаться, в чем можем быть проблема и попробовать ее исправить самостоятельно (включив интернет-соединение), либо обратившись к разработчику для исправления адреса ресурса (для это вам будет необходимо указать контакты поддержки при размещении приложения, либо встроить такую возможность в само приложение).

Также стоит отметить, что подобное информирование пользователя — это всего лишь полумера. В идеале нужно кешировать данные и информировать пользователя о наличии проблем с соединением менее разрушительным способом. См. например, как работает приложение Bing News в Windows 8.

Манифест и политика конфиденциальности

Для того, чтобы WinRT-приложение могло работать с внешними данными, получаемыми через интернет, необходимо указать такую возможность в манифесте приложения (вкладка Capabilities):

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

Замечание. Если вы делаете приложение, которому не требуется соединение с интернетом, вам следует эту опцию выключить. В целом, для любых возможностей, потенциально затрагивающих приватность пользователя, необходимо декларировать, необходимость включения соответствующей функциональности.

Следующий важный момент, вытекающий из описанного выше и требований сертификации приложения в Windows Store, заключается в том, что для работы с пользовательскими данными, а также для использования возможностей, которые такие данные могут затрагивать, приложение должно иметь внутри себя Privacy Policy (Политика конфиденциальности).

В нашем случае это интернет-соединение, которое потенциально может позволять отслеживать IP-адреса пользователей, передавать пользовательские данные и предпочтения на сервер и т.п. – все это необходимо сообщить пользователю. Даже если это простое приложение для чтения RSS-потоков, никак не подвергающее опасности персональные данные, вам все равно надо сообщить пользователю, что вы этого не делаете, но используете внешние ресурсы, за которые не отвечаете.

Чтобы это сделать, вам необходимо иметь Политику конфиденциальности (примеры можно посмотреть в доступных в Windows Store приложениях). Эта политика может быть "вшита" в приложение или размещена на внешнем интернет-сайте и доступна по ссылке. Доступ к тексту данного соглашения должен быть обеспечен как внутри приложения через настройки, так и при скачивании приложения в Windows Store.

Первый пункт мы рассмотрим ниже, а для второго при размещении приложения в магазине есть соответствующее поле в описании.

Добавьте в проект новый HTML-документ. Например, внутри папки pages можно сделать еще одну папку settings, внутри которой в свою очередь создать документ privacy.html.

Приведите содержание страницы к следующему виду:

<!doctype HTML>
<html>
    <head>
        <title>Privacy Policy flyout</title>
        <link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />
        <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
        <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
    </head>
    <body>
        <div id="privacy" data-win-control="WinJS.UI.SettingsFlyout" aria-label="Help settings flyout"
                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">Настройки</div>
                <img src="/images/smalllogo.png" style="position: absolute; right: 40px;"/>
            </div>
            <div class="win-content">
                <h2>Политика конфиденциальности</h2>
                <p> [Текст политики]</p>                
            </div>
        </div>
    </body>
</html>

Здесь мы указываем, что страница представляет собой всплывающую панель SettingsFlyout, в опциях (data-win-option) говорим, что панель должна быть узкой. Внутри указываем заголовок, кнопку назад, иконку и текст политики или ссылку на нее.

Откройте файл js\default.js, в нем нам необходимо зарегистрировать нашу панель, добавив ее в настройки приложения. Для этого добавьте обработчик события вызова настроек:

app.addEventListener("settings", function (args) {
        args.detail.applicationcommands = {
            "privacy": { href: "/pages/settings/privacy.html", title: "Политика конфиденциальности" }
        };
        WinJS.UI.SettingsFlyout.populateSettings(args);
    });

Теперь в приложении доступна панель с описанием Политики конфиденциальности нашего приложения:

Готово!

< Лекция 1 || Лабораторная работа 1: 12 || Онлайн-консультация 1 >
Андрей Милютин
Андрей Милютин

Будьте добры сообщите какой срок проверки заданий и каким способом я буду оповещен!

Данила Слупский
Данила Слупский

К сожалению, я не могу выполнить данную практическую работу в VS 2013 на WIndows 8.1. Код описанных файлов отличается от кода в моем проекте. Как мне быть?