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

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

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

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;
}

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

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

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

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

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

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