Задание: начиная с базового шаблона, создайте свое первое приложение для Windows 8, работающее с внешним источником данных. В результате должно получиться приложение, отображающее данные с внешнего ресурса (например, через RSS или Atom) в стандартном Grid-шаблоне приложения.
Обновление 1 (05.04.2013). Добавлен пример проверки наличия интернет-соединения.
Начнём разработку с шаблона Grid приложения, выбрав разработку на XAML/C#.
В меню File выберите New Project, далее Visual C#, затем — Windows Store, в центральной части окна New Project выберите шаблон под названием Grid App (XAML). Введите в поле Name название приложения, например, MyReader, если нужно, в поле Location папку, в которой будет размещаться проект приложения и нажмите кнопку OK.
В результате мы получаем практически полнофункцинальное по навигации и отображению данных приложение, которое отображает пример данных.
Давайте посмотрим, как оно работает, нажав F5, зеленую стрелочку или выбрав Debug -> Start Debugging
Можно убедиться, что это уже практически полнофункциональное приложение:
Вернитесь в Visual Studio и остановите отладку приложения (Shift+F5, красный квадратик или выберите в меню Debug -> Stop Debugging). Разберёмся, с основными файлами нашего решения:
Также в папке 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);
Пересобираем и запускаем приложение.
Вот теперь всё гораздо красивее!
Откройте Visual Studio 2012, выберите создание нового проекта (File -> New -> Project…). Далее в шаблонах выберите проект на JavaScript -> Windows Store. Укажите, что будете использовать шаблон Grid App.
Укажите любое название проекта, например, Reader App.
Изучите структуру проекта:
Также по умолчанию к проекту подключена библиотека WinJS, содержащая наборы стилей для темной и светлой тем и вспомогательных функций и объектов на JavaScript.
Попробуйте запустить приложение, нажав F5, зеленую стрелочку или выбрав Debug -> Start Debugging.
Изучите работу приложения:
Откройте файл 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
}];
Для описания каждого блога (группы контента) мы указываем:
Чтобы превратить ссылки в данные на компьютере, информацию по ним необходимо загрузить. Для этого после строчки 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-шаблоне приложения.
Обновление 1 (05.04.2013). Добавлен пример проверки наличия интернет-соединения.
Начнём разработку с шаблона Grid приложения, выбрав разработку на XAML/C#.
В меню File выберите New Project, далее Visual C#, затем — Windows Store, в центральной части окна New Project выберите шаблон под названием Grid App (XAML). Введите в поле Name название приложения, например, MyReader, если нужно, в поле Location папку, в которой будет размещаться проект приложения и нажмите кнопку OK.
В результате мы получаем практически полнофункцинальное по навигации и отображению данных приложение, которое отображает пример данных.
Давайте посмотрим, как оно работает, нажав F5, зеленую стрелочку или выбрав Debug -> Start Debugging
Можно убедиться, что это уже практически полнофункциональное приложение:
Вернитесь в Visual Studio и остановите отладку приложения (Shift+F5, красный квадратик или выберите в меню Debug -> Stop Debugging). Разберёмся, с основными файлами нашего решения:
Также в папке 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);
Пересобираем и запускаем приложение.
Вот теперь всё гораздо красивее!
Откройте Visual Studio 2012, выберите создание нового проекта (File -> New -> Project…). Далее в шаблонах выберите проект на JavaScript -> Windows Store. Укажите, что будете использовать шаблон Grid App.
Укажите любое название проекта, например, Reader App.
Изучите структуру проекта:
Также по умолчанию к проекту подключена библиотека WinJS, содержащая наборы стилей для темной и светлой тем и вспомогательных функций и объектов на JavaScript.
Попробуйте запустить приложение, нажав F5, зеленую стрелочку или выбрав Debug -> Start Debugging.
Изучите работу приложения:
Откройте файл 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
}];
Для описания каждого блога (группы контента) мы указываем:
Чтобы превратить ссылки в данные на компьютере, информацию по ним необходимо загрузить. Для этого после строчки 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);
});
Теперь в приложении доступна панель с описанием Политики конфиденциальности нашего приложения:
Готово!
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.