Задание: продолжая проект, полученный в результате выполнения второго практического задания, вынесите список источников новостей в отдельный файл и добавьте кеширование данных и вывод информации о наличии интернет-соединения в приложении.
ЗАМЕЧАНИЕ: напоминаем, что ваше приложение должно быть уникальным:
Начнем с достаточно простой задачи – добавление файла настроек. Для этого, щелкнув правой кнопкой мыши по проекту, выберете Add, New Folder и назовите новую папку Data.
Теперь, щелкнув правой кнопкой мыши по папке Data, выберите Add, New Item …, Visual C#, Data, XML File, назовите его config.xml и нажмите кнопку Add.
Давайте сделаем файл конфигурации с заделом на будущее, чтобы можно было из него конфигурировать как и что будет отображаться у нас в приложении:
<?xml version="1.0" encoding="utf-8" ?>
<config>
<application>
<settings>
<title>Мега-ридер</title>
</settings>
</application>
<feeds>
<feed>
<id>1</id>
<title>bash.im</title>
<url>http://bash.im/rss/</url>
<description>Это bash!</description>
<type>rss</type>
<view>variablesize</view>
<policy>http://wintheweb.ru/openprivacy.html</policy>
</feed>
<feed>
<id>2</id>
<title>Комиксы bash.im</title>
<url>http://bash.im/rss/comics.xml</url>
<description>Комиксы</description>
<type>images</type>
<view>variablesize</view>
<policy>http://wintheweb.ru/openprivacy.html</policy>
</feed>
</feeds>
</config>
Итак, у нас корневой элемент config, содержит 2 элемента: application и feeds. В элементе application будем хранить настройки приложения, а в feeds – наши источники RSS с идентификатором (id), названием (title), ссылкой на RSS (url), описанием (description), типом контента (type), вариантом отображения (view) и соответствующей политикой конфиденциальности (policy).
Теперь осталось прочитать этот файл и в соответствии с настройками провести инициализацию приложения.
Желая развивать приложение дальше, мы задумываемся о том, что надо бы иметь возможность изменять эти настройки. Поэтому, первая задача, которую мы должны решить – при первом запуске приложения скопировать файл настроек из места куда он развернется, туда, где мы можем в него писать.
Для того, чтобы файл настроек включился в сборку, пометьте его в свойствах, как Content, который будет копироваться, если он более новый.
В файл App.xaml.cs добавим новую функцию:
public static async Task<bool> CopyConfigToLocalFolder()
{
//получаем папку с именем Data в локальной папке приложения
var localFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync("Data",CreationCollisionOption.OpenIfExists);
//получаем список файлов в папке Data
var files = await localFolder.GetFilesAsync();
//получаем список всех файлов, имя которых config.xml
var config = from file in files
where file.Name.Equals("config.xml")
select file;
//нам возращается IEnumrable - а он гарантирует тольок один проход
//копируем в массив - если не беспокоитесь об этом - просто уберите эту строчку
//а в условии проверяйте config.Count()
// if (config.Count() == 0) { }
var configEntries = config as StorageFile[] ?? config.ToArray();
//то же самое, что config.Count() == 0, но гарантиует от странных ошибок
//т.е. в целом мы проверили, что файла config.xml нет в подпапке Data
//папки локальных данных приложения
if (!configEntries.Any())
{
//получаем папку Data из установленого приложения
var dataFolder = await Package.Current.InstalledLocation.GetFolderAsync("Data");
//получаем файл сonfig.xml
var configFile = await dataFolder.GetFileAsync("config.xml");
//копируем его в локальную папку данных
await configFile.CopyAsync(localFolder);
return true;
}
return false;
}
Она из места куда установлена программа, в папке Data находит файл config.xml и копирует его в подпапку Data локальной папки приложения, причем, если папка отсутствует – она создается, а если папка уже присутствует и в ней есть файл, копирования не происходит. Мы это делаем, чтобы в дальнейшем сохранять в config.xml настройки и чтобы они не перезаписывались при исполнении этой функции.
Что конкретно выполнят каждая из строчек кода – можно узнать из комментария над ней.
И, наконец, добавим необходимые using, если вы не сделали этого раньше, используя команды контекстного меню:
using Windows.Storage;
Теперь нужно добавить чтение файла и разбор его содержимого.
В рамках данной лабораторной мы будем разбирать только feeds часть XML, в качестве самостоятельной работы добавьте разбор всего XML, дополните его своими секциями и также их считайте в коде.
Итак, приступим.
Сначала добавим в наш проект класс для представления данных RSS потока Feed, аналогично тому, как мы добавляли config.xml:
В получившимся файле, изменим определение класса Feed, чтобы оно стало выглядеть так:
public class Feed
{
public string id { get; set; }
public string title{ get; set; }
public string url { get; set; }
public string description { get; set; }
public string type { get; set; }
public string view { get; set; }
public string policy { get; set; }
}
Итак, у нас теперь есть класс с полями, куда мы можем считывать нашу конфигурацию.
Осталось прочитать XML файл и разобрать его.
Добавим для этого следующую функцию в файл App.xaml.cs:
public static async Task<IEnumerable<Feed>> ReadSettings()
{
//получаем папку в которой находится наш файл конфигурации
var localFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync
("Data", CreationCollisionOption.OpenIfExists);
//получаем список файлов в папке Data
var files = await localFolder.GetFilesAsync();
//получаем список всех файлов, имя которых config.xml
var config = from file in files
where file.Name.Equals("config.xml")
select file;
//нам возращается IEnumrable - а он гарантирует тольок один проход
//копируем в массив - если не беспокоитесь об этом - просто уберите эту строчку
//а в условии проверяйте config.Count()
// if (config.Count() == 0) { }
var configEntries = config as StorageFile[] ?? config.ToArray();
//то же самое, что config.Count() == 0, но гарантиует от странных ошибок
//т.е. в целом мы проверили, что файла config.xml нет в подпапке Data
//папки локальных данных приложения
if (!configEntries.Any())
await CopyConfigToLocalFolder();
//получаем конфигурационный файл
var configFile = await localFolder.GetFileAsync("config.xml");
//считываем его как текст
var configText = await FileIO.ReadTextAsync(configFile);
//загружаем его как XML
XElement configXML = XElement.Parse(configText);
//разбираем XML инициализируя данным массив
var feeds =
from feed in configXML.Descendants("feed")
select new Feed
{
id = feed.Element("id").Value,
title= feed.Element("title").Value,
url = feed.Element("url").Value,
description = feed.Element("description").Value,
type = feed.Element("type").Value,
view = feed.Element("view").Value,
policy = feed.Element("policy").Value
};
//отдаем наружу массив с конфигурацией RSS потоков
return feeds;
}
И, наконец, добавим необходимые using, если вы не сделали этого раньше, используя команды контекстного меню:
using System.Threading.Tasks; using System.Xml.Linq;
Все что делается в этой функции – описано построчно в комментариях. Итак, у нас уже все готово, чтобы теперь у нас все инициализировалось из XML файла.
Для этого в функции LoadState в файле GroupedItemsPage.xaml.cs заменим код
RSSDataSource.AddGroupForFeedAsync("http://blogs.msdn.com/b/stasus/rss.aspx");
RSSDataSource.AddGroupForFeedAsync("http://www.spugachev.com/feed");
на код:
var feeds = await App.ReadSettings();
foreach (var feed in feeds)
{
await RSSDataSource.AddGroupForFeedAsync(feed.url);
}
Т.е. мы получаем список RSS потоков и инициализируем нашу читалку пробегая по ним всем.
Сделайте самостоятельно проверку, что файл в из поставки приложения отличается от того, который уже скопирован и, в этом случае, копируйте файл из поставки приложения поверху. Таким образом у вас появится возможность обновлениями config.xml обновлять то, что показывает приложение.
Для работы при отсутствии интернета можно закэшировать данные, которые скачиваются вашим приложением.
Для этого сначала поменяем функцию AddGroupForFeedAsync чтобы она выглядела следующим образом:
public static async Task<bool> AddGroupForFeedAsync(string feedUrl, string ID)
{
string clearedContent = String.Empty;
if (RSSDataSource.GetGroup(feedUrl) != null) return false;
var feed = await new SyndicationClient().RetrieveFeedAsync(new Uri(feedUrl));
//получаем папку с именем Data в локальной папке приложения
var localFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync
("Data",CreationCollisionOption.OpenIfExists);
//получаем/перезаписываем файл с именем "ID".rss
var fileToSave = await localFolder.CreateFileAsync(ID+".rss",CreationCollisionOption.ReplaceExisting);
//сохраняем фид в этот файл
await feed.GetXmlDocument(SyndicationFormat.Rss20).SaveToFileAsync(fileToSave);
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 = GetImageFromPostContents(i);
if (i.Summary != null)
clearedContent = i.Summary.Text;
else
if (i.Content != null)
clearedContent = 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;
}
Добавленное выделено цветом.
И, наконец, добавим необходимые using, если вы не сделали этого раньше, используя команды контекстного меню:
using Windows.Storage;
Также в функции LoadState в файле GroupedItemsPage.xaml.cs изменим код вызова функции
var feeds = await App.ReadSettings();
foreach (var feed in feeds)
{
await RSSDataSource.AddGroupForFeedAsync(feed.url,feed.id);
}
Запустите и проверьте, что все работает.
Теперь при отсутствии интернета – мы можем выводить данные сохраненных файлов.
Для начала перегрузим функцию AddGroupForFeedAsync чтобы она загружали RSS поток из сохраненного файла. Код очень простой – мы создаем новый Feed и загружаем его из XML документа:
public static async Task<bool> AddGroupForFeedAsync(StorageFile sf)
{
string clearedContent = String.Empty;
if (RSSDataSource.GetGroup(sf.DisplayName) != null) return false;
var feed = new SyndicationFeed();
feed.LoadFromXml(await XmlDocument.LoadFromFileAsync(sf));
var feedGroup = new RSSDataGroup(
uniqueId: sf.DisplayName,
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 = GetImageFromPostContents(i);
if (i.Summary != null)
clearedContent = i.Summary.Text;
else
if (i.Content != null)
clearedContent = 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;
}
Во всем остальном функция осталась такой же самой.
Теперь в в функции LoadState в файле GroupedItemsPage.xaml.cs дополним код обработки отсутствия сети. Но для начала добавим в XAML этой же страницы индикацию режима оффлайн работы. В XAML заголовка страницы необходимо добавить еще одно определение колонки, а в контент еще один TextBox:
<!-- Back button and pagetitle-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button x:Name="backButton" Click="GoBack" IsEnabled="{Binding Frame.CanGoBack,
ElementName=pageRoot}" Style="{StaticResource BackButtonStyle}"/>
<TextBlock x:Name="pageTitle" Text="{StaticResource AppName}" Grid.Column="1"
IsHitTestVisible="false" Style="{StaticResource PageHeaderTextStyle}"/>
<TextBlock x:Name="OfflineMode" Grid.Column="2" IsHitTestVisible="false"
Style="{StaticResource PageHeaderTextStyle}" Text="оффлайнн режим" FontSize="18"
Foreground="#DEF40B0B" HorizontalAlignment="Right"
VerticalAlignment="Top" Height="37" Margin="0,-16,18,0" Visibility="Collapsed"/>
</Grid>
И допишем функцию:
protected async override void LoadState(Object navigationParameter,
Dictionary<String, Object> pageState)
{
if (NetworkInformation.GetInternetConnectionProfile().GetNetworkConnectivityLevel() !=
NetworkConnectivityLevel.InternetAccess)
{
//получаем папку с именем Data в локальной папке приложения
var localFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync
("Data", CreationCollisionOption.OpenIfExists);
//получаем список файлов в папке Data
var cachedFeeds = await localFolder.GetFilesAsync();
//получаем список всех файлов, имя которых config.xml
var feedsToLoad = from feeds in cachedFeeds
where feeds.Name.EndsWith(".rss")
select feeds;
//нам возращается IEnumrable - а он гарантирует тольок один проход
//копируем в массив
var feedsEntries = feedsToLoad as StorageFile[] ?? feedsToLoad.ToArray();
if (feedsEntries.Any())
{
this.DefaultViewModel["Groups"] = RSSDataSource.AllGroups;
foreach (var feed in feedsEntries)
{
await RSSDataSource.AddGroupForFeedAsync(feed);
}
OfflineMode.Visibility = Visibility.Visible;
}
else
{
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;
OfflineMode.Visibility = Visibility.Collapsed;
var feeds = await App.ReadSettings();
foreach (var feed in feeds)
{
await RSSDataSource.AddGroupForFeedAsync(feed.url, feed.id);
}
}
}
Каждый раз, когда вы приходите на страницу LoadState – проверяется, если ли у вас подключение к сети, и если есть – загружаются новые данные. Если нет, но у нас есть закешированные данные – отображаются они и метка – оффлайн режим. Таким образом, при навигации по приложению, в случае появления интернет соединения – при переходе на главную страницу произойдет автоматическое переключение режима.
Самостоятельно выполните рефакторинг кода функции LoadState, чтобы он не был насколько "лапшеобразным".
Задание: продолжая проект, полученный в результате выполнения второго практического задания, вынесите список источников новостей в отдельный и добавьте кеширование данных и вывод информации о наличии интернет-соединения в приложении.
ЗАМЕЧАНИЕ: напоминаем, что ваше приложение должно быть уникальным:
В первом практическом упражнении мы научились подключать к нашему приложению внешние источники данных, используя для этого RSS-потоки в качестве источника и XMLHTTPRequest (в обертке WinJS.xhr) для запроса данных.
Если вы вернетесь к написанному коду (data.js), вы увидите, что список источников со всеми необходимыми атрибутами прописан непосредственно в приложении.
var blogs = [
{
key: "MyBlogKey",
url: "http://myblog.com/rss",
title: 'My Blog Title, rsstitle: 'tbd', updated: 'tbd',
dataPromise: null
},
…
]
Во многих сценариях это может быть неудобно и непрактично. Фактически, в текущем варианте данные смешаны с кодом и, что еще более важно, эти данные неудобно обновлять. Например, легко можно представить ситуацию, когда вы хотите добавить в список еще один источник или заменить ссылку на какой-либо из источников, причем сделать это необходимо удаленно, без повторного размещения приложения в Windows Store. Одним из шагов к решению такой задачи будет вынесение списка источников в отдельный файл, который потом можно будет, например, синхронизировать с облачным хранилищем или вашим веб-сервером.
Давайте попробуем вынести эти настройки в отдельный файл внутри нашего проекта. Первым делом, необходимо определиться с форматом данных. Для JavaScript одним из наиболее подходящих форматов является JSON – его легко парсить средствами самого JavaScript, более того, он в точности соответствует нашему описанию списка блогов.
Создайте внутри проекта новую папку data и внутри новый пустой файл, дав ему соответствующее название, например, "sources.js" (так как формат описания данных повторяет формат описания в самом языке JavaSсript, то расширение .js позволяет автоматически использовать подсветку синтаксиса из VS, но можно дать и другое расширение, например, .json).
Чтобы составить описание в виде JSON, который будет стабильно распознаваться при парсинге, лучше всего поручить это какому-либо автоматизированному средству. Например, можно вызвать JavaScript-консоль внутри VS во время выполнения проекта (Ctrl+Alt+V, C), либо открыть инструменты разработчика в любом современном браузере (обычно F12) и переключиться там на JavaScript-консоль.
После чего, достаточно вызвать функцию JSON.stringify(…), передав в нее в качестве параметра значение blogs. Например, в IE10 это выглядит так:
Сначала объявить переменную blogs
Далее в консоли вызвать функцию JSON.stringify:
JSON.stringify(blogs);
В результате работы функция выдаст тест в правильном формате (внутри кавычек):
[{"key":"BBlogging","url":"http://blogs.windows.com/windows/b/bloggingwindows/rss.aspx",
"title":"Blogging Windows","rsstitle":"tbd","updated":"tbd","dataPromise":null},
{"key":"AExperience","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}]
Такой текст и нужно вставить в наш файл описания источников данных. Как видите, в целом он повторяет описание коллекции данных непосредственно в JavaScript.
Замечание: не забудьте заменить этот текст-описание блогов на ваш собственный, отвечающий вашим предпочтениям.
Замечание: будьте внимательны – функция JSON.parse, которую мы будем использовать ниже, чувствительна к кодировке и связанными с ней переносам строки.
Вернитесь в файл data.js и удалите исходное описание списка блогов:
var blogs = […];
Перейдите к функции getBlogPosts, в которой мы использовали этот список для загрузки соответствующих rss-потоков:
function getBlogPosts(postsList) {
blogs.forEach(function (feed) {
…
});
return postsList;
}
Как видите, здесь мы проходимся по списку блогов внутри переменной blogs, которую мы только что удалили. Нам необходимо получить список блогов из файла и подставить его вместо переменной blogs.
Для этого создайте рядом новую функцию getBlogsList, которая будет считывать локальный файл sources.js, обрабатывать его и передавать дальше список блогов:
function getBlogsList() {
var sourcesFile = "data/" + "sources.js";
var sourceUri = new Windows.Foundation.Uri("ms-appx:///" + sourcesFile);
var listPromise = Windows.Storage.StorageFile.getFileFromApplicationUriAsync(sourceUri).then(function (file) {
return Windows.Storage.FileIO.readTextAsync(file, Windows.Storage.Streams.UnicodeEncoding.utf8);
}).then(function (text) {
return JSON.parse(text);
});
return listPromise;
}
Обратите внимание на два важных момента:
Функция getBlogsList возвращает новый Promise-объект, которые после выполнения вернет JavaScript-объект описания данных.
Замечание: попробуйте также добавить обработчик ошибки при чтении файла на случай, если у вас будет ошибка в формате JSON-файла.
Замечание: помимо использования системных API для чтения файлов, в принципе, мы также могли воспользоваться XMLHTTPRequest (WinJS.xhr) для считывания данных.
Еще раз вернитесь к функции getBlogPosts, теперь надо внутри вызвать функцию getBlogsList и после возвращения результата проделать все остальные операции. Обновленная функция должна выглядеть следующим образом:
function getBlogPosts(postsList) {
getBlogsList().then(function (list) {
list.forEach(function (feed) {
// Создание Promise
…
});
});
return postsList;
}
Обратите внимание, что фактически весь внутренний код мы обернули функцией then, доступной из Promise, который в свою очередь возвращает функция getBlogsList.
Попробуйте запустить проект и убедитесь, что он работает также, как и раньше.
В будущем вы можете синхронизировать эти настройки с внешним сервисом, например, расположенном с облаке.
Теперь давайте научимся сохранять полученные с сервера данные локально и использовать их как кеш.
Для сохранения данных в локальном хранилище, Windows предоставляет специальное API, доступное через объект Windows.Storage.ApplicationData. Подобное хранилище предоставляется для каждого приложения.
В нашем приложении мы будем реализовывать следующее поведение:
Давайте разбираться по порядку. И начнем с сохранения локальных данных. Для этого снова вернемся к функции getBlogPosts, в которой мы загружаем данные.
Найдите код обработки результата запроса к внешнему сервису:
function (response) {
if (response) {
var syndicationXML = response.responseXML || (new DOMParser()).parseFromString(response.responseText, "text/xml");
processRSSFeed(syndicationXML, feed, postsList);
}
},
Этот код вызывается в цепочке событий и, чтобы мы могли ее продолжить для локального сохранения файлов, изнутри функции должен возвращаться результат. Давайте "наружу" передадим текстовый ответ, который мы постараемся сохранить:
function (response) {
if (response) {
var syndicationXML = response.responseXML || (new DOMParser()).parseFromString(response.responseText, "text/xml");
processRSSFeed(syndicationXML, feed, postsList);
return response.responseText;
}
},
Теперь добавьте в конец цепочки Promise еще одну функцию:
feed.dataPromise = WinJS.xhr({ url: feed.url }).then(
…
).then(function(text) {
});
Внутри добавьте следующий код для сохранения локального кеша (мы записываем в настройки дату последнего сохранения и в файл сам текст):
var localSettings = Windows.Storage.ApplicationData.current.localSettings;
var localFolder = Windows.Storage.ApplicationData.current.localFolder;
if (text) {
localSettings.values[feed.key] = Date.now.toString();
localFolder.createFileAsync(feed.key + ".rss", Windows.Storage.CreationCollisionOption.replaceExisting).then(function (file){
Windows.Storage.FileIO.writeTextAsync(file, text);
});
}
Здесь мы записываем в настройки для каждого источника дату сохранения (на самом деле, нам достаточно записать любой параметр, по которому мы сможем понять, что у нас есть локальный кеш) и далее сохраняем полученный текст в локальный файл.
Теперь давайте перейдем ко второй задаче, если нет интернета, будем пытаться отобразить локальную копию данных. Для этого найдите код, в котором мы проверяем наличие интернет-соединения:
function tryUpdateData() {
if (isInternetConnection()) {
getBlogPosts(list);
} else {
showConnectionError("Please check your internet connection. ");
}
};
Давайте добавим дополнительную логику:
function tryUpdateData() {
if (isInternetConnection()) {
getBlogPosts(list);
} else {
getLocalBlogPosts(list);
}
};
Здесь мы ссылаемся на некоторую локальную функцию getLocalBlogPosts, которую еще необходимо написать. Эта функция похожа на обычную функцию загрузки постов с той лишь разницей, что данные мы будем брать локально:
function getLocalBlogPosts(postsList) {
getBlogsList().then(function (list) {
// Локальные настройки
var localSettings = Windows.Storage.ApplicationData.current.localSettings;
// Локальная папка с файлами
var localFolder = Windows.Storage.ApplicationData.current.localFolder;
list.forEach(function (feed) {
if (localSettings.values[feed.key]) {
// Доступ к файлу
localFolder.getFileAsync(feed.key + ".rss").then(function (file) {
// Чтение файла
return Windows.Storage.FileIO.readTextAsync(file);
}).then(function (responseText) {
// Обработка результата
var syndicationXML = (new DOMParser()).parseFromString(responseText, "text/xml");
processRSSFeed(syndicationXML, feed, postsList);
});
}
});
});
}
Внутри мы сначала проверяем через настройки, сохраняли ли мы файл с нужным именем (в целом это быстрее, чем проверить наличие файла). Если файл сохраняли, мы пытаемся получить к нему доступ и далее считываем информацию в текстовую переменную, после чего пытаемся обработать точно также, как мы это делали в случае удаленного запроса.
Наконец, осталось обработать ситуацию, в которой, никакого локального кеша нет. Для этого мы можем завести аккумулирующую переменную hasCache, но нам также понадобится собрать все отдельные Promise обработки файлов в общую коллецию (используя join), чтобы после этого обработать результат:
function getLocalBlogPosts(postsList) {
var hasCache = false;
getBlogsList().then(function (list) {
// Локальные настройки
var localSettings = Windows.Storage.ApplicationData.current.localSettings;
// Локальная папка с файлами
var localFolder = Windows.Storage.ApplicationData.current.localFolder;
var filePromises = [];
list.forEach(function (feed) {
if (localSettings.values[feed.key]) {
// Доступ к файлу
var filePromise = localFolder.getFileAsync(feed.key + ".rss").then(function (file) {
// Чтение файла
return Windows.Storage.FileIO.readTextAsync(file);
}).then(function (responseText) {
// Обработка результата
var syndicationXML = (new DOMParser()).parseFromString(responseText, "text/xml");
processRSSFeed(syndicationXML, feed, postsList);
hasCache = true;
return hasCache;
});
filePromises.push(filePromise);
}
});
return WinJS.Promise.join(filePromises);
}).then(function () {
if (!hasCache) showConnectionError("Please check your internet connection. ");
}, function error(e) {
showConnectionError("Please check your internet connection. ");
});
}
Последний штрих. Давайте при работе offline, сообщим пользователю, что он работает в offline-режиме. Откройте файл groupedItems.html и добавьте небольшой фрагмент html-кода (aside):
<div class="fragment groupeditemspage">
<aside class="online" id="offlineNotification">offline</aside>
…
</div>
Далее добавьте в сопутствующем файле groupedItems.css необходимые стили:
#offlineNotification {
background: red;
color:white;
position:absolute;
top:0;
right: 50px;
padding: 8px;
font-size: 11pt;
}
#offlineNotification.online {
display:none;
}
Осталось добавить дополнительную логику. Перейдите к определению глобальной переменной Data и добавьте внизу информацию о статусе подключения:
WinJS.Namespace.define("Data", {
items: groupedItems,
groups: groupedItems.groups,
getItemReference: getItemReference,
getItemsFromGroup: getItemsFromGroup,
resolveGroupReference: resolveGroupReference,
resolveItemReference: resolveItemReference,
online: isInternetConnection()
});
Наконец, откройте файл groupedItems.js и добавьте в нем переключение внешнего вида нотификации в зависимости от наличия подключения к интернету (это можно сделать внутри функции _initializeLayout):
if (Data.online) {
document.getElementById("offlineNotification").className = "online";
} else {
document.getElementById("offlineNotification").className = "";
}
Отключите интернет (например, перейдя в Airplane-режим) и попробуйте запустить приложение. Если вы до этого кешировали данные, то должен отобразиться закешированный контент:
Если вы запустили приложение впервые, то должно появиться сообщение об ошибке:
Попробуйте самостоятельно:
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.