Разработка приложений для Windows 8

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

Разбить на страницы
Показывать лекцию целиком

Задание: начиная с базового шаблона, создайте свое первое приложение для Windows 8, работающее с внешним источником данных. В результате должно получиться приложение, отображающее данные с внешнего ресурса (например, через RSS или Atom) в стандартном Grid-шаблоне приложения.

Для разработчиков на XAML + C#

Обновление 1 (05.04.2013). Добавлен пример проверки наличия интернет-соединения.

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

    В меню File выберите New Project, далее Visual C#, затем — Windows Store, в центральной части окна New Project выберите шаблон под названием Grid App (XAML). Введите в поле Name название приложения, например, MyReader, если нужно, в поле Location папку, в которой будет размещаться проект приложения и нажмите кнопку OK.

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

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

    Можно убедиться, что это уже практически полнофункциональное приложение:

  • нажмите на любую отдельную серую плитку или на заголовок группы;
  • воспользуйтесь кнопкой назад на внутренних страницах приложения;
  • переведите приложение в Snapped-режим.
  • Вернитесь в Visual Studio и остановите отладку приложения (Shift+F5, красный квадратик или выберите в меню Debug -> Stop Debugging). Разберёмся, с основными файлами нашего решения:

  • *.xaml — разметка страницы приложения на языке XAML.
  • *.xaml.cs — это code-behind файл на языке C# страницы.
  • App.xaml — инициализация объекта Windows Store-приложения на языке XAML, не имеет визуального представления и не является страницей приложения.
  • App.xaml.cs — это code-behind файл на языке C# для App.xaml. В данном файле обрабатываются события уровня приложения, такие как запуск, активация, в том числе активация по поисковому запросу, и деактивация приложения. Кроме того, в App.xaml.cs вы можете перехватывать необработанные исключения и отлавливать ошибки навигации между страницами.
  • папка Common, файл StandardStyles.xaml — файл ресурсов стилей для графических элементов управления, подключается в App.xaml и его стили доступны во всех страницах приложения.
  • Package.appxmanifest — это манифест приложения, XML-файл, содержит разнообразные настройки приложения. Есть графический редактор, который открывается двойным щелчком мышью по имени файла Package.appxmanifest в окне Solution Explorer.
  • AssemblyInfo.cs — конфигурационный файл, в котором определяются некоторые метаданные главной сборки приложения.
  • MyReader_TemporaryKey.pfx — криптографический сертификат с закрытым ключом, которым подписывается приложение.
  • папка Asset, файлы Logo.png, SmallLogo.png, StoreLogo.png — логотипы для большой и малой плиток приложения, а также иконка для списка всех приложений.
  • SplashScreen.png — картинка для экрана заставки, который отображается во время загрузки приложения.
  • Также в папке Common, находятся файлы с общим кодом, который используется в приложении, в папке DataModel находится файл с моделью данных приложения — SampleDataSource.cs.

    Меняем данные для отображения

    Итак, приложение уже работает и в нём реализован пример источника данных. Поэтому задача номер один – подменить пример источника данных на реальный источник данных. Начнём с удаления конструктора по умолчанию класса SampleDataSource, в котором происходит инициализация статическими данными.

    public SampleDataSource()

    Дальше, воспользуемся встроенными в Visual Studio возможностями рефакторинга и переименуем SampleDataSource в RSSDataSource, SampleDataGroup в RSSDataGroup, SampleDataItem в RSSDataItem и SampleDataCommon в RSSDataCommon.

    Для создания целостной картины мира можно файл SampleDataSource.cs переименовать в RSSDataSource.cs.

    Для упрощения работы с классом данных в этом примере — упростим его. Для этого заменим следующий кусок класса RSSDataSource

    private static RSSDataSource _sampleDataSource = new RSSDataSource();
    
    private ObservableCollection<RSSDataGroup> _allGroups = new ObservableCollection<RSSDataGroup>();
    public ObservableCollection<RSSDataGroup> AllGroups
    {
         get { return this._allGroups; }
    }

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

    public static readonly ObservableCollection<RSSDataGroup> AllGroups = new ObservableCollection<RSSDataGroup>();

    В завершение убираем ненужную локальную переменную _sampleDataSource из кода класса.

    Итак, мы подготовились к добавлению реальных данных.

    Допишем простой метод, который будет брать поток данных RSS или ATOM и представлять его для дальнейшего отображения в интерфейсе приложения.

     public static async Task<bool> AddGroupForFeedAsync(string feedUrl)
    {
        string clearedContent = String.Empty;
    
        if (RSSDataSource.GetGroup(feedUrl) != null) return false;
    
        var feed = await new SyndicationClient().RetrieveFeedAsync(new Uri(feedUrl));
    
        var feedGroup = new RSSDataGroup(
            uniqueId: feedUrl,
            title: feed.Title != null ? feed.Title.Text : null,
            subtitle: feed.Subtitle != null ? feed.Subtitle.Text : null,
            imagePath: feed.ImageUri != null ? feed.ImageUri.ToString() : null,
            description: null);
    
        foreach (var i in feed.Items)
        {
            string imagePath = null;
    
            if (i.Summary != null)
                clearedContent = Windows.Data.Html.HtmlUtilities.ConvertToText(i.Summary.Text);
            else
                if (i.Content != null)
                    clearedContent = Windows.Data.Html.HtmlUtilities.ConvertToText(i.Content.Text);
    
            if (imagePath != null  feedGroup.Image == null)
                feedGroup.SetImage(imagePath);
    
            if (imagePath == null) imagePath = "ms-appx:///Assets/DarkGray.png";
    
            feedGroup.Items.Add(new RSSDataItem(
                uniqueId: i.Id, title: i.Title.Text, subtitle: null, imagePath: imagePath,
                description: null, content: clearedContent, @group: feedGroup));
        }
    
        AllGroups.Add(feedGroup);
        return true;
    }

    Код достаточно простой. Используя SyndicationClient, получаем и разбираем данные из RSS/ATOM, после чего создаём группу и добавляем в неё записи. Попутно, очищаем содержимое от HTML тегов, просто, чтобы выглядело лучше, а также предусматриваем добавление картинки, чем займёмся позже.

    Не забудьте добавить в блок using следующую директивы:

    using Windows.Web.Syndication;
    using System.Threading.Tasks;

    Чтобы сразу проверить, как это работает, двойным щелчком откроем файл GroupedItemsPage.xaml.cs и перейдём к методу LoadState, который инициализирует данные и заменим источник данных sampleDataGroups на RSSDataSource.AllGroups, а также удалим ненужную теперь инициализацию sampleDataGroups. Теперь, код LoadState состоит из одной строчки:

    protected override async void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
    {
        this.DefaultViewModel["Groups"] = RSSDataSource.AllGroups;
    }

    Теперь, чтобы всё заработало, нужно добавить какой-нибудь RSS. Воспользуемся свежесозданной функцией AddGroupForFeedAsync и добавим две строчки в функцию LoadState:

    RSSDataSource.AddGroupForFeedAsync("http://blogs.msdn.com/b/stasus/rss.aspx");
    RSSDataSource.AddGroupForFeedAsync("http://www.spugachev.com/feed");
    

    Собираем проект и запускаем.

    Ура — работает!

    Интернет, контракт настроек и политики конфиденциальности

    Приложение мы пишем не просто так — мы хотим опубликовать его в Windows Store. Если прибавить к этому, что наше приложение ходит в интернет, и прочитать требования к сертификации приложений, окажется, что нам необходимо реализовать контракт настроек, который бы тем или иным образом указывал на Privacy Settings (Политику конфиденциальности).

    Обратите внимание, что для большинства шаблонов по умолчанию устанавливается Capabilities (Возможность) – интернет-клиент, которая требует от нас наличия политики конфиденциальности. Нам эта возможность безусловно нужна, потому что мы ходим в интернет за новостями:

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

    Добавим контракт настроек (Settings) только для начальной страницы. Для этого переопределим методы OnNavigatedTo и OnNavigatedFrom, регистрацию и дерегистрирую обработчика для настроек:

    protected override void OnNavigatedFrom(NavigationEventArgs e)
    {
        SettingsPane.GetForCurrentView().CommandsRequested -= Settings_CommandsRequested;
        base.OnNavigatedFrom(e);
    }
    
    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        SettingsPane.GetForCurrentView().CommandsRequested += Settings_CommandsRequested;
        base.OnNavigatedTo(e);
    }

    Теперь определим обработчик Settings_CommandsRequested:

    private void Settings_CommandsRequested(SettingsPane sender, SettingsPaneCommandsRequestedEventArgs args)
    {
        var viewPrivacyPage = new SettingsCommand("", "Privacy Statement", cmd =>
        {
            Launcher.LaunchUriAsync(new Uri("http://go.microsoft.com/fwlink/?LinkId=248681", UriKind.Absolute));
        });
        args.Request.ApplicationCommands.Add(viewPrivacyPage);
    }
    

    Не забудте добавить в блок using следующую директиву:

    using Windows.System;
    using Windows.UI.ApplicationSettings;

    Код очень простой. Мы отображаем пункт настроек с названием Privacy Statement и, по нажатию на него, отправляем на Privacy Policy блогов MSDN.

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

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

    if (NetworkInformation.GetInternetConnectionProfile().GetNetworkConnectivityLevel() 
    !=  NetworkConnectivityLevel.InternetAccess)
    {
        var msg = new MessageDialog("The program need an internet connection to work.
           Please check it and restart the porgram.");
        await msg.ShowAsync();
    }

    Давайте добавим этот код в метод LoadState страницы GroupedItemsPage страницы следующим образом:

    protected override async void LoadState(Object navigationParameter, 
    Dictionary<String, Object> pageState)
    {
    if (NetworkInformation.GetInternetConnectionProfile() == null 
    || NetworkInformation.GetInternetConnectionProfile().GetNetworkConnectivityLevel() != NetworkConnectivityLevel.InternetAccess)
    {
        var msg = new MessageDialog("The program need an internet connection to work. 
          Please check it and restart the porgram.");
        await msg.ShowAsync();
     }
     else
     {
    
         this.DefaultViewModel["Groups"] = RSSDataSource.AllGroups;
         RSSDataSource.AddGroupForFeedAsync("http://blogs.msdn.com/b/stasus/rss.aspx");
         RSSDataSource.AddGroupForFeedAsync("http://www.spugachev.com/feed");
    
     }	
    }

    Обратите внимание, что метод помечен ключевым словом async, поскольку теперь внутри него используется await.

    Не забудьте добавить в блок using следующую директивы:

    using Windows.Networking.Connectivity;
    using Windows.UI.Popups;

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

    Бонус: Добавляем картинки

    Первое с чего надо начать — это картинки в посты и на плитки в сгруппированном отображении.

    Перейдём обратно в RSSDataSource.cs и добавим магический метод, добывающий изображения из RSS постов:

    private static string GetImageFromPostContents(SyndicationItem item)
    {
        string text2search = "";
    
        if (item.Content != null) text2search += item.Content.Text;
        if (item.Summary != null) text2search += item.Summary.Text;
    
        return Regex.Matches(text2search,
                @"(?<=<img\s+[^>]*?src=(?<q>['""]))(?<url>.+?)(?=\k<q>)",
                RegexOptions.IgnoreCase)
            .Cast<Match>()
            .Where(m =>
            {
                Uri url;
                if (Uri.TryCreate(m.Groups[0].Value, UriKind.Absolute, out url))
                {
                    string ext = Path.GetExtension(url.AbsolutePath).ToLower();
                    if (ext == ".png" || ext == ".jpg" || ext == ".bmp") return true;
                }
                return false;
            })
            .Select(m => m.Groups[0].Value)
            .FirstOrDefault();
    }

    В методе, мы, используя регулярные выражения пытаемся найти картинки (с расширениями png, jpg и bmp) в тексте поста или его summary.

    Не забудте добавить в блок using следующую директивы:

    using System.Text.RegularExpressions;
    using System.IO;

    Теперь добавим вызовы этого метода в заботливо оставленную в методе AddGroupForFeedAsync заглушку. Меняем

    string imagePath = null;

    на

    string imagePath = GetImageFromPostContents(i);

    Пересобираем и запускаем приложение.

    Вот теперь всё гораздо красивее!

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

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

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

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

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

  • package.appxmanifest — манифест приложения, описывающий ключевые настройки, используемые возможности, название приложения, плитки и другие параметры;
  • default.html — формальная стартовая страница приложения;
  • pages\groupedItems — папка с html-, js- и css-файлами для страницы представления групп контента (подгружается в стартовую страницу);
  • pages\groupDetail — папка с html-, js- и css-файлами для страницы отображения группы новостей (записей в rss-потоке), соответствующих одному потоку;
  • pages\itemDetail — папка с html-, js- и css-файлами для страницы отображения каждой из новостей отдельно;
  • js\data.js — js-файл, описывающий работу с данными (содержит зашитые внутрь демонстрационные данные);
  • js\default.js — описывает события, необходимые для инициализации приложения;
  • js\navigator.js — описывает логику переходов между страницами и необходимые для этого события и объекты;
  • images\logo.png — изображение, используемое для квадратной плитки;
  • images\smalllogo.png — изображение, используемое при перечислении приложения в операционной системе, например, при поиске или выборе приложений для поиска или общего доступа;
  • images\splashscreen.png — загрузочное изображение, показываемое при открытии приложения;
  • 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);
        });

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

    Готово!

    Страницы:

    Задание: начиная с базового шаблона, создайте свое первое приложение для Windows 8, работающее с внешним источником данных. В результате должно получиться приложение, отображающее данные с внешнего ресурса (например, через RSS или Atom) в стандартном Grid-шаблоне приложения.

    Для разработчиков на XAML + C#

    Обновление 1 (05.04.2013). Добавлен пример проверки наличия интернет-соединения.

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

    В меню File выберите New Project, далее Visual C#, затем — Windows Store, в центральной части окна New Project выберите шаблон под названием Grid App (XAML). Введите в поле Name название приложения, например, MyReader, если нужно, в поле Location папку, в которой будет размещаться проект приложения и нажмите кнопку OK.

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

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

    Можно убедиться, что это уже практически полнофункциональное приложение:

  • нажмите на любую отдельную серую плитку или на заголовок группы;
  • воспользуйтесь кнопкой назад на внутренних страницах приложения;
  • переведите приложение в Snapped-режим.
  • Вернитесь в Visual Studio и остановите отладку приложения (Shift+F5, красный квадратик или выберите в меню Debug -> Stop Debugging). Разберёмся, с основными файлами нашего решения:

  • *.xaml — разметка страницы приложения на языке XAML.
  • *.xaml.cs — это code-behind файл на языке C# страницы.
  • App.xaml — инициализация объекта Windows Store-приложения на языке XAML, не имеет визуального представления и не является страницей приложения.
  • App.xaml.cs — это code-behind файл на языке C# для App.xaml. В данном файле обрабатываются события уровня приложения, такие как запуск, активация, в том числе активация по поисковому запросу, и деактивация приложения. Кроме того, в App.xaml.cs вы можете перехватывать необработанные исключения и отлавливать ошибки навигации между страницами.
  • папка Common, файл StandardStyles.xaml — файл ресурсов стилей для графических элементов управления, подключается в App.xaml и его стили доступны во всех страницах приложения.
  • Package.appxmanifest — это манифест приложения, XML-файл, содержит разнообразные настройки приложения. Есть графический редактор, который открывается двойным щелчком мышью по имени файла Package.appxmanifest в окне Solution Explorer.
  • AssemblyInfo.cs — конфигурационный файл, в котором определяются некоторые метаданные главной сборки приложения.
  • MyReader_TemporaryKey.pfx — криптографический сертификат с закрытым ключом, которым подписывается приложение.
  • папка Asset, файлы Logo.png, SmallLogo.png, StoreLogo.png — логотипы для большой и малой плиток приложения, а также иконка для списка всех приложений.
  • SplashScreen.png — картинка для экрана заставки, который отображается во время загрузки приложения.
  • Также в папке Common, находятся файлы с общим кодом, который используется в приложении, в папке DataModel находится файл с моделью данных приложения — SampleDataSource.cs.

    Меняем данные для отображения

    Итак, приложение уже работает и в нём реализован пример источника данных. Поэтому задача номер один – подменить пример источника данных на реальный источник данных. Начнём с удаления конструктора по умолчанию класса SampleDataSource, в котором происходит инициализация статическими данными.

    public SampleDataSource()

    Дальше, воспользуемся встроенными в Visual Studio возможностями рефакторинга и переименуем SampleDataSource в RSSDataSource, SampleDataGroup в RSSDataGroup, SampleDataItem в RSSDataItem и SampleDataCommon в RSSDataCommon.

    Для создания целостной картины мира можно файл SampleDataSource.cs переименовать в RSSDataSource.cs.

    Для упрощения работы с классом данных в этом примере — упростим его. Для этого заменим следующий кусок класса RSSDataSource

    private static RSSDataSource _sampleDataSource = new RSSDataSource();
    
    private ObservableCollection<RSSDataGroup> _allGroups = new ObservableCollection<RSSDataGroup>();
    public ObservableCollection<RSSDataGroup> AllGroups
    {
         get { return this._allGroups; }
    }

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

    public static readonly ObservableCollection<RSSDataGroup> AllGroups = new ObservableCollection<RSSDataGroup>();

    В завершение убираем ненужную локальную переменную _sampleDataSource из кода класса.

    Итак, мы подготовились к добавлению реальных данных.

    Допишем простой метод, который будет брать поток данных RSS или ATOM и представлять его для дальнейшего отображения в интерфейсе приложения.

     public static async Task<bool> AddGroupForFeedAsync(string feedUrl)
    {
        string clearedContent = String.Empty;
    
        if (RSSDataSource.GetGroup(feedUrl) != null) return false;
    
        var feed = await new SyndicationClient().RetrieveFeedAsync(new Uri(feedUrl));
    
        var feedGroup = new RSSDataGroup(
            uniqueId: feedUrl,
            title: feed.Title != null ? feed.Title.Text : null,
            subtitle: feed.Subtitle != null ? feed.Subtitle.Text : null,
            imagePath: feed.ImageUri != null ? feed.ImageUri.ToString() : null,
            description: null);
    
        foreach (var i in feed.Items)
        {
            string imagePath = null;
    
            if (i.Summary != null)
                clearedContent = Windows.Data.Html.HtmlUtilities.ConvertToText(i.Summary.Text);
            else
                if (i.Content != null)
                    clearedContent = Windows.Data.Html.HtmlUtilities.ConvertToText(i.Content.Text);
    
            if (imagePath != null  feedGroup.Image == null)
                feedGroup.SetImage(imagePath);
    
            if (imagePath == null) imagePath = "ms-appx:///Assets/DarkGray.png";
    
            feedGroup.Items.Add(new RSSDataItem(
                uniqueId: i.Id, title: i.Title.Text, subtitle: null, imagePath: imagePath,
                description: null, content: clearedContent, @group: feedGroup));
        }
    
        AllGroups.Add(feedGroup);
        return true;
    }

    Код достаточно простой. Используя SyndicationClient, получаем и разбираем данные из RSS/ATOM, после чего создаём группу и добавляем в неё записи. Попутно, очищаем содержимое от HTML тегов, просто, чтобы выглядело лучше, а также предусматриваем добавление картинки, чем займёмся позже.

    Не забудьте добавить в блок using следующую директивы:

    using Windows.Web.Syndication;
    using System.Threading.Tasks;

    Чтобы сразу проверить, как это работает, двойным щелчком откроем файл GroupedItemsPage.xaml.cs и перейдём к методу LoadState, который инициализирует данные и заменим источник данных sampleDataGroups на RSSDataSource.AllGroups, а также удалим ненужную теперь инициализацию sampleDataGroups. Теперь, код LoadState состоит из одной строчки:

    protected override async void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
    {
        this.DefaultViewModel["Groups"] = RSSDataSource.AllGroups;
    }

    Теперь, чтобы всё заработало, нужно добавить какой-нибудь RSS. Воспользуемся свежесозданной функцией AddGroupForFeedAsync и добавим две строчки в функцию LoadState:

    RSSDataSource.AddGroupForFeedAsync("http://blogs.msdn.com/b/stasus/rss.aspx");
    RSSDataSource.AddGroupForFeedAsync("http://www.spugachev.com/feed");
    

    Собираем проект и запускаем.

    Ура — работает!

    Интернет, контракт настроек и политики конфиденциальности

    Приложение мы пишем не просто так — мы хотим опубликовать его в Windows Store. Если прибавить к этому, что наше приложение ходит в интернет, и прочитать требования к сертификации приложений, окажется, что нам необходимо реализовать контракт настроек, который бы тем или иным образом указывал на Privacy Settings (Политику конфиденциальности).

    Обратите внимание, что для большинства шаблонов по умолчанию устанавливается Capabilities (Возможность) – интернет-клиент, которая требует от нас наличия политики конфиденциальности. Нам эта возможность безусловно нужна, потому что мы ходим в интернет за новостями:

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

    Добавим контракт настроек (Settings) только для начальной страницы. Для этого переопределим методы OnNavigatedTo и OnNavigatedFrom, регистрацию и дерегистрирую обработчика для настроек:

    protected override void OnNavigatedFrom(NavigationEventArgs e)
    {
        SettingsPane.GetForCurrentView().CommandsRequested -= Settings_CommandsRequested;
        base.OnNavigatedFrom(e);
    }
    
    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        SettingsPane.GetForCurrentView().CommandsRequested += Settings_CommandsRequested;
        base.OnNavigatedTo(e);
    }

    Теперь определим обработчик Settings_CommandsRequested:

    private void Settings_CommandsRequested(SettingsPane sender, SettingsPaneCommandsRequestedEventArgs args)
    {
        var viewPrivacyPage = new SettingsCommand("", "Privacy Statement", cmd =>
        {
            Launcher.LaunchUriAsync(new Uri("http://go.microsoft.com/fwlink/?LinkId=248681", UriKind.Absolute));
        });
        args.Request.ApplicationCommands.Add(viewPrivacyPage);
    }
    

    Не забудте добавить в блок using следующую директиву:

    using Windows.System;
    using Windows.UI.ApplicationSettings;

    Код очень простой. Мы отображаем пункт настроек с названием Privacy Statement и, по нажатию на него, отправляем на Privacy Policy блогов MSDN.

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

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

    if (NetworkInformation.GetInternetConnectionProfile().GetNetworkConnectivityLevel() 
    !=  NetworkConnectivityLevel.InternetAccess)
    {
        var msg = new MessageDialog("The program need an internet connection to work.
           Please check it and restart the porgram.");
        await msg.ShowAsync();
    }

    Давайте добавим этот код в метод LoadState страницы GroupedItemsPage страницы следующим образом:

    protected override async void LoadState(Object navigationParameter, 
    Dictionary<String, Object> pageState)
    {
    if (NetworkInformation.GetInternetConnectionProfile() == null 
    || NetworkInformation.GetInternetConnectionProfile().GetNetworkConnectivityLevel() != NetworkConnectivityLevel.InternetAccess)
    {
        var msg = new MessageDialog("The program need an internet connection to work. 
          Please check it and restart the porgram.");
        await msg.ShowAsync();
     }
     else
     {
    
         this.DefaultViewModel["Groups"] = RSSDataSource.AllGroups;
         RSSDataSource.AddGroupForFeedAsync("http://blogs.msdn.com/b/stasus/rss.aspx");
         RSSDataSource.AddGroupForFeedAsync("http://www.spugachev.com/feed");
    
     }	
    }

    Обратите внимание, что метод помечен ключевым словом async, поскольку теперь внутри него используется await.

    Не забудьте добавить в блок using следующую директивы:

    using Windows.Networking.Connectivity;
    using Windows.UI.Popups;

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

    Бонус: Добавляем картинки

    Первое с чего надо начать — это картинки в посты и на плитки в сгруппированном отображении.

    Перейдём обратно в RSSDataSource.cs и добавим магический метод, добывающий изображения из RSS постов:

    private static string GetImageFromPostContents(SyndicationItem item)
    {
        string text2search = "";
    
        if (item.Content != null) text2search += item.Content.Text;
        if (item.Summary != null) text2search += item.Summary.Text;
    
        return Regex.Matches(text2search,
                @"(?<=<img\s+[^>]*?src=(?<q>['""]))(?<url>.+?)(?=\k<q>)",
                RegexOptions.IgnoreCase)
            .Cast<Match>()
            .Where(m =>
            {
                Uri url;
                if (Uri.TryCreate(m.Groups[0].Value, UriKind.Absolute, out url))
                {
                    string ext = Path.GetExtension(url.AbsolutePath).ToLower();
                    if (ext == ".png" || ext == ".jpg" || ext == ".bmp") return true;
                }
                return false;
            })
            .Select(m => m.Groups[0].Value)
            .FirstOrDefault();
    }

    В методе, мы, используя регулярные выражения пытаемся найти картинки (с расширениями png, jpg и bmp) в тексте поста или его summary.

    Не забудте добавить в блок using следующую директивы:

    using System.Text.RegularExpressions;
    using System.IO;

    Теперь добавим вызовы этого метода в заботливо оставленную в методе AddGroupForFeedAsync заглушку. Меняем

    string imagePath = null;

    на

    string imagePath = GetImageFromPostContents(i);

    Пересобираем и запускаем приложение.

    Вот теперь всё гораздо красивее!

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

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

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

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

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

  • package.appxmanifest — манифест приложения, описывающий ключевые настройки, используемые возможности, название приложения, плитки и другие параметры;
  • default.html — формальная стартовая страница приложения;
  • pages\groupedItems — папка с html-, js- и css-файлами для страницы представления групп контента (подгружается в стартовую страницу);
  • pages\groupDetail — папка с html-, js- и css-файлами для страницы отображения группы новостей (записей в rss-потоке), соответствующих одному потоку;
  • pages\itemDetail — папка с html-, js- и css-файлами для страницы отображения каждой из новостей отдельно;
  • js\data.js — js-файл, описывающий работу с данными (содержит зашитые внутрь демонстрационные данные);
  • js\default.js — описывает события, необходимые для инициализации приложения;
  • js\navigator.js — описывает логику переходов между страницами и необходимые для этого события и объекты;
  • images\logo.png — изображение, используемое для квадратной плитки;
  • images\smalllogo.png — изображение, используемое при перечислении приложения в операционной системе, например, при поиске или выборе приложений для поиска или общего доступа;
  • images\splashscreen.png — загрузочное изображение, показываемое при открытии приложения;
  • 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);
        });

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

    Готово!

    Вернуться к учебному плану