Будьте добры сообщите какой срок проверки заданий и каким способом я буду оповещен! |
Работа в офф-лайне и файл настроек
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, если вы не сделали этого раньше, используя команды контекстного меню:
Теперь нужно добавить чтение файла и разбор его содержимого.
В рамках данной лабораторной мы будем разбирать только 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, чтобы он не был насколько "лапшеобразным".