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

Работа в офф-лайне и файл настроек

Показывать лекцию целиком

XAML+C#. Практическое занятие №3

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

  • В качестве подтверждения выполнения лабораторной работы от вас потребуется предоставить скриншот главной страницы приложения, отображающей данные при отсутствии интернет-соединения.
  • ЗАМЕЧАНИЕ: напоминаем, что ваше приложение должно быть уникальным:

  • Использовать в качестве источников данных источники, отличные от тех, которые приводятся в инструкциям к практическим занятиям
  • Внешний вид приложения должен соответствовать выбранной вами тематике приложения
  • Добавление файла настроек

    Начнем с достаточно простой задачи – добавление файла настроек. Для этого, щелкнув правой кнопкой мыши по проекту, выберете 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;
    }
    

    Добавленное выделено цветом.

  • Мы добавили параметр в вызов функции – ID RSS в нашем конфигурационном файле
  • Мы добавили вызовы нескольких функций для сохранения результата в локальную папку Data
  • И, наконец, добавим необходимые 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, чтобы он не был насколько "лапшеобразным".

    HTML + JS: Практическое занятие №3

    Файлы и работа offline

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

  • В качестве подтверждения выполнения лабораторной работы от вас потребуется предоставить скриншот первой страницы приложения, отображающей данные при отсутствии интернет-соединения.
  • ЗАМЕЧАНИЕ: напоминаем, что ваше приложение должно быть уникальным:

  • Использовать в качестве источников данных источники, отличные от тех, которые приводятся в инструкциям к практическим занятиям
  • Внешний вид приложения должен соответствовать выбранной вами тематике приложения
  • Настройки и файлы

    В первом практическом упражнении мы научились подключать к нашему приложению внешние источники данных, используя для этого 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;
    }

    Обратите внимание на два важных момента:

  • Адресацию локальных файлов проекта с использованием протокола ms-appx
  • Постоянное использование асинхронных операций (через Promise) для получения файла и чтения данных из него.
  • Функция 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-режим) и попробуйте запустить приложение. Если вы до этого кешировали данные, то должен отобразиться закешированный контент:

    Если вы запустили приложение впервые, то должно появиться сообщение об ошибке:

    Попробуйте самостоятельно:

  • добавить сообщение об отсутствии интернет-соединения (при закешированном контенте) на других страницах;
  • добавить обновление контента при подключении интернета (это можно отследить через событие Windows.Networking.Connectivity.NetworkInformation.onnetworkstatuschanged)
  • Вернуться к учебному плану