Microsoft — мировой лидер в области ПО и ИТ-услуг
531,53
рейтинг
24 декабря 2012 в 14:08

Разработка → Разбираемся с разработкой Windows 8 приложений на XAML/С#, реализуя простой RSS Reader. Ч.1 tutorial



После поста Введение в разработку WinRT-приложений на HTML/JavaScript. От шаблона к приложению с данными вы уже знаете как разрабатывать RSS читалку под Windows 8 с использованием HTML и JavaScript. Пришло время попробовать сделать примерно то же самое, но с использованием XAML/C#. Это первая часть, но мы уже в ней сделаем красиво!

Создание приложения из шаблона


Начнём разработку с также с шаблона Grid приложения, выбрав разработку на XAML/C#
В меню File выберите New Project, далее Visual C#, затем — Windows Store, в центральной части окна New Project выберите шаблон под названием Grid App (XAML). Введите в поле Name название приложения, например, MyReader, если нужно, в поле Location папку, в которой будет размещаться проект приложения и нажмите кнопку OK.
habr-templ-xaml-1
В результате мы получаем практически полнофункцинальное по навигации и отображению данных приложение, которое отображает пример данных.
Давайте посмотрим, как оно работает, нажав F5, зеленую стрелочку или выбрав Debug -> Start Debugging
habr-templ-xaml-2
Можно убедиться, что это уже  практически полнофункциональное приложение:
  • нажмите на любую отдельную серую плитку или на заголовок группы;
  • воспользуйтесь кнопкой назад на внутренних страницах приложения;
  • переведите приложение в Snapped-режим.

Вернитесь в Visual Studio и остановите отладку приложения (Shift+F5, красный квадратик или выберите в меню Debug -> Stop Debugging).
Разберёмся, с основными файлами нашего решения:
  • *.xaml — разметка страницы приложения на языке XAML.
  • *.xaml.cs — это code-behind файл на языке C# страницы.
  • App.xaml — инициализация объекта Windows Store-приложения на языке XAML, не имеет визуального представления и не является страницей приложения.
  • App.xaml.cs — это code-behind файл на языке C# для App.xaml. В данном файле обрабатываются события уровня приложения, такие как запуск, активация, в том числе активация по поисковому запросу, и деактивация приложения. Кроме того, в App.xaml.cs вы можете перехватывать необработанные исключения и отлавливать ошибки навигации между страницами.
  • папка Common, файл StandardStyles.xaml — файл ресурсов стилей для графических элементов управления,  подключается в App.xaml и его стили доступны во всех страницах приложения.
  • Package.appxmanifest — это манифест приложения, XML-файл, содержит разнообразные настройки приложения. Есть графического редактора, который открывается двойным щелчком мышью по имени файла Package.appxmanifest в окне Solution Explorer.
  • AssemblyInfo.cs —  конфигурационный файл, в котором определяются некоторые метаданные главной сборки приложения.
  • MyReader_TemporaryKey.pfx — криптографический сертификат с закрытым ключом, которым подписывается приложение.
  • папка Asset, файлы Logo.png, SmallLogo.png, StoreLogo.png — логотипы для большой и малой плиток приложения, а также иконка для списка всех приложений.
  • SplashScreen.png — картинка для экрана заставки, который отображается во время загрузки приложения.

Также в папке Common, находятся также файлы с общим кодом, который используюется в приложении, в папке DataModel нахдится файл с моделью данных приложения — SampleDataSource.cs.

Меняем данные для отображения

Итак, приложение уже работает и в нём реализован пример источника данных. Поэтому задача номер один – подменить пример источника даных на реальный источник данных.
Начнём с удаления конструктора по умолчанию класса SampleDataSource, в котором происходит инициализация статическими данными.
public SampleDataSource()

Дальше, воспользуемся встроенными в Visual Studio возможностями рефакторинга и переименуем SampleDataSource в RSSDataSource, SampleDataGroup в RSSDataGroup, SampleDataItem в RSSDataItem и SampleDataCommon в RSSDataCommon.

Для создания целостной картины мира можно файл SampleDataSource.cs переименовать в RSSDataSource.cs.

Для упрощения работы с классом данных в этом примере — упростим его. Для этого заменим следующий кусок класса RSSDataSource
private static RSSDataSource _sampleDataSource = new RSSDataSource();

private ObservableCollection<RSSDataGroup> _allGroups = new ObservableCollection<RSSDataGroup>();
public ObservableCollection<RSSDataGroup> AllGroups
{
     get { return this._allGroups; }
}


На аналогичный по смыслу код, упрощающий восприятие и дальнейшую разработку примера.
public static readonly ObservableCollection<RSSDataGroup> AllGroups = new ObservableCollection<RSSDataGroup>();

В завершение убираем ненужную локальную переменную _sampleDataSource из кода класса.

Итак, мы подготовились к добавлению реальных данных.

Допишем простой метод, который будет брать поток данных RSS или ATOM и представлять его для дальнейшего отображения в интерфейсе приложения.
 public static async Task<bool> AddGroupForFeedAsync(string feedUrl)
{
    string clearedContent = String.Empty;

    if (RSSDataSource.GetGroup(feedUrl) != null) return false;

    var feed = await new SyndicationClient().RetrieveFeedAsync(new Uri(feedUrl));

    var feedGroup = new RSSDataGroup(
        uniqueId: feedUrl,
        title: feed.Title != null ? feed.Title.Text : null,
        subtitle: feed.Subtitle != null ? feed.Subtitle.Text : null,
        imagePath: feed.ImageUri != null ? feed.ImageUri.ToString() : null,
        description: null);

    foreach (var i in feed.Items)
    {
        string imagePath = null;

        if (i.Summary != null)
            clearedContent = Windows.Data.Html.HtmlUtilities.ConvertToText(i.Summary.Text);
        else
            if (i.Content != null)
                clearedContent = Windows.Data.Html.HtmlUtilities.ConvertToText(i.Content.Text);

        if (imagePath != null && feedGroup.Image == null)
            feedGroup.SetImage(imagePath);

        if (imagePath == null) imagePath = "ms-appx:///Assets/DarkGray.png";

        feedGroup.Items.Add(new RSSDataItem(
            uniqueId: i.Id, title: i.Title.Text, subtitle: null, imagePath: imagePath,
            description: null, content: clearedContent, @group: feedGroup));
    }

    AllGroups.Add(feedGroup);
    return true;
}

Код достаточно простой. Используя SyndicationClient, получаем и разбираем данные из RSS/ATOM, после чего создаём группу и добавляем в неё записи. Попутно, очищаем содержимое от HTML тегов, просто, чтобы выглядело лучше, а также предусматриваем добавление картинки, чем займёмся позже.

Не забудте добавить в блок using следующую директивы:
using Windows.Web.Syndication;
using System.Threading.Tasks;

Чтобы сразу проверить, как это работает, двойным щелчком откроем файл GroupedItemsPage.xaml.cs и перейдём к методу LoadState, который инициализирует данные и заменим источник данных sampleDataGroups на RSSDataSource.AllGroups, а также удалим ненужную теперь инициализацию sampleDataGroups. Теперь, код LoadState состоит из одной строчки:
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
{
    this.DefaultViewModel["Groups"] = RSSDataSource.AllGroups;
}

Теперь, чтобы всё заработало, нужно добавить какой-нибудь RSS. Воспользуемся свежесозданной функцией AddGroupForFeedAsync и добавим две строчки в функцию LoadState:
RSSDataSource.AddGroupForFeedAsync("http://blogs.msdn.com/b/stasus/rss.aspx");
RSSDataSource.AddGroupForFeedAsync("http://www.spugachev.com/feed");

Собираем проект и запускаем.

Ура — работает!

Но как-то всё не очень красиво… Ну что же, давайте сделаем красиво!

Делаем красиво

Первое с чего надо начать — это картинки в посты и на плитки в сгруппированном отображении.
Перейдём обратно в RSSDataSource.cs и добавим магический метод, добывающий изображения из RSS постов:
private static string GetImageFromPostContents(SyndicationItem item)
{
    string text2search = "";

    if (item.Content != null) text2search += item.Content.Text;
    if (item.Summary != null) text2search += item.Summary.Text;

    return Regex.Matches(text2search,
            @"(?<=<img\s+[^>]*?src=(?<q>['""]))(?<url>.+?)(?=\k<q>)",
            RegexOptions.IgnoreCase)
        .Cast<Match>()
        .Where(m =>
        {
            Uri url;
            if (Uri.TryCreate(m.Groups[0].Value, UriKind.Absolute, out url))
            {
                string ext = Path.GetExtension(url.AbsolutePath).ToLower();
                if (ext == ".png" || ext == ".jpg" || ext == ".bmp") return true;
            }
            return false;
        })
        .Select(m => m.Groups[0].Value)
        .FirstOrDefault();
}

В методе, мы, используя регулярные выражения пытаемся найти картинки (с расширениями png, jpg и bmp) в тексте поста или его summary.
Не забудте добавить в блок using следующую директивы:
using System.Text.RegularExpressions;
using System.IO;

Теперь добавим вызовы этого метода в заботливо оставленные в методе AddGroupForFeedAsync заглушку. Меняем
string imagePath = null;
на
string imagePath = GetImageFromPostContents(i);

Пересобираем и запускаем приложение.

Всё стало гораздо лучше, но почему все топики отображаются плитками одного размера? Пожалуй, надо сделать, как в Windows Store!

Это несколько более сложная задача, чем всё то, что мы делали раньше. Но мы не останавливаемся перед трудностями!

Для того, чтобы реализовать подобный функционал, нам придётся создать свой класс, наследник GridView. Добавьте в проект новый файл класса с именем: VariableSizeGridView.cs. В новом файле, добавьте в блок using директиву:
using Windows.UI.Xaml.Controls;
, укажите, что класс наследуется от GridView и переопределите в нём метод класса: PrepareContainerForItemOverride. В результате должно получиться что-то вроде этого:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml.Controls;

namespace MyReader
{
    class VariableSizeGridView: GridView 
    {
        protected override void PrepareContainerForItemOverride(Windows.UI.Xaml.DependencyObject element, object item)
        {
            base.PrepareContainerForItemOverride(element, item);
        }
    }
}

Собственно метод PrepareContainerForItemOverride и будет определять логику, по которой будут отображаться плитки разного размера в сгруппированном представлении. Теперь необходимо подготовить разметку в файле GroupedItemsPage.xaml.

Сначала заменяем GridView на наш класс — local:VariableSizeGridView. Далее в ItemPanelTemplate дополняем свойства VariableSizedWrapGrid указав размер Item-а и максимальное количество срок и колонок, так, что он будет выглядеть следующим образом:
<VariableSizedWrapGrid Orientation="Vertical" Margin="0,0,80,0" ItemWidth="200" ItemHeight="125" MaximumRowsOrColumns="9" />


Добавим собственный шаблон Item в ресурсы страницы:
<DataTemplate x:Key="CustomItemTemplate">
            <Grid HorizontalAlignment="Left">
                <Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}">
                    <Image Source="{Binding Image}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/>
                </Border>
                <StackPanel Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}" VerticalAlignment="Bottom">
                    <TextBlock Text="{Binding Title}" Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}"  Height="90" Margin="15,0,15,0" FontSize="30" />
                </StackPanel>
            </Grid>
        </DataTemplate>


И укажем его в качестве шаблона Item, вместо стандартного в local:VariableSizeGridView::
<local:VariableSizeGridView
            x:Name="itemGridView"
            AutomationProperties.AutomationId="ItemGridView"
            AutomationProperties.Name="Grouped Items"
            Grid.RowSpan="2"
            Padding="116,137,40,46"
            ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"
            ItemTemplate="{StaticResource CustomItemTemplate}"
            SelectionMode="None"
            IsSwipeEnabled="false"
            IsItemClickEnabled="True"
            ItemClick="ItemView_ItemClick">

Запускам приложение.

Выглядит не очень красиво. Но это пока мы не добавим наш код с логикой в PrepareContainerForItemOverride.

Итак, добавляем код:
class VariableSizeGridView: GridView 
{
    private int rowVal;
    private int colVal;
        
    protected override void PrepareContainerForItemOverride(Windows.UI.Xaml.DependencyObject element, object item)
    {
        RSSDataItem dataItem = item as RSSDataItem;
        int index = -1;

        if (dataItem != null)
        {
            index = dataItem.Group.Items.IndexOf(dataItem);

        }

        if (index == 1)
        {
            colVal = 2;
            rowVal = 4;
        }
        else
        {
            colVal = 2;
            rowVal = 2;
        }

        if (index == 2)
        {
            colVal = 2;
            rowVal = 4;
        }

        if (index == 5)
        {
            colVal = 4;
            rowVal = 4;
        }

        VariableSizedWrapGrid.SetRowSpan(element as UIElement, rowVal);
        VariableSizedWrapGrid.SetColumnSpan(element as UIElement, colVal);

        base.PrepareContainerForItemOverride(element, item);
    }

Как видно, мы просто руками указываем какую по порядку плитку, как отображать.

Запустим ещё раз.

Выглядит гораздо лучше, а реализуя шаблоны для Item и логику работы PrepareContainerForItemOverride можем добиться практически любого представления в пределах GridView.

На этом я, пожалуй завершу первую часть. Во второй части, мы продолжим делать красиво: добавим живые плитки, контракты поиска и share, а также вынесем RSS фиды в настройки.

UPD: Да, и ещё сделаем стиль для Title, чтобы он на плитках в группированом отображении отображался по человечески, как на снимке экрана в начале статьи.

Получившееся приложение в виде проекта на SkyDrive (сокращённая ссылка): aka.ms/w8RSSp1

С наступающим Новым Годом!

UPD:
Часть 2-я цикла
Часть 3-я цикла
Автор: @stasus
Microsoft
рейтинг 531,53
Microsoft — мировой лидер в области ПО и ИТ-услуг

Комментарии (16)

  • 0
    Это тот самый пример, что есть в бесплатной книжке, которую где-то тут рядом и раздавали?
    • 0
      Не совсем.

      Сейчас будет сильно сложноподчинённое предложение :)

      В книжке пример, который сделан на основе примера, который я сделал на основе поста из блога С. Сомасегара для демонстрации на открытии Windows Camp.

      Этот пример — доработка в части грида с разными плитками, того примера, который я делал изначально + ещё то, чего здесь пока нет. У меня — уже готовое приложение-пример, а статья пошаговое восстановление того, как я его делал.
      • 0
        Действительно непростое предложение получилось, но я справился. )
        В любом случае спасибо. Больше статей по Windows Metro хороших и разных.
        А где бы почитать, как сделать движущийся фон под гридом, как в главном меню Windows 8? Я когда сам его делал, жутких костылей наворотил
        • 0
          «Движущийся фон» — это параллакс!
          *flies away*
          • 0
            Я название эффекта знаю в принципе ) Но реализовать от этого легче не становится. Я с xaml + c# еще не очень дружу видимо. Не получилось толком именно плавная прокрутка фона.
  • 0
    Подскажите пожалуйста, как поменять метод AddGroupForFeedAsync чтоб можно било добавлять в группу несколько RSS-фидов.
    • 0
      1. Передавать в метод список Uri, тем или иным способом.
      2. Создать группу не по Uri, поменяв код:
       var feedGroup = new RSSDataGroup(
                      uniqueId: <ваш_уникальный_ID_группы>,
                      title: <ваше_название_группы>,
                      subtitle: <ваше_дополнительное_название_группы>,
                      imagePath: <ваша_картинка_группы>,
                      description: <ваше_описание_группы>);
      

      3. Оберунть код:
       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));
                  }
      

      Дополнительным циклом по списку переданных RSS Uri
      • 0
        Спасибо.
      • 0
        Может кому то понадобится:
        public static async Task<bool> AddGroupForFeedAsync(string[] feedUrl,string id,string Title,
                                                                    string SubTitle)
                {
                    string clearedContent = String.Empty;
        
                    var feedGroup = new RSSDataGroup(
                             uniqueId: id,
                             title: Title,
                             subtitle: SubTitle,
                             imagePath:"" ,
                             description: null);
        
                    for (int count = 0; count < feedUrl.Length; count++)
                    {
                        if (RSSDataSource.GetGroup(feedUrl[count]) != null) return false;
        
                        var feed = await new SyndicationClient().RetrieveFeedAsync(new Uri(feedUrl[count]));
        
                        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;
                }
        
  • 0
    Подскажите как мне передать в класс содержимое заголовка страницы ItemDetailPage?
    В xaml(e) есть строка
       <TextBlock x:Name="pageTitle" Text="{Binding Title}" Style="{StaticResource PageHeaderTextStyle}" 
                           Grid.Column="1" IsHitTestVisible="false" x:FieldModifier="public"/>
    

    Как передать содержимое pageTitle в строку str?
    Пробовал так
    ItemDetailPage _itemDetailPage = new ItemDetailPage();
    string str=_itemDetailPage.pageTitle.Text;
    

    Но почему то передается пустая строка. Как сделать все правильно? Спасибо.
    • 0
      На странице ItemDetailPage это будет выглядеть так:

      string str = pageTitle.Text;
  • 0
    Ошибочку поправьте:
    f (imagePath != null && feedGroup.Image == null)
    на
    if (imagePath != null && feedGroup.Image == null)

    Не проверяли свой код, да?:)
    • 0
      Спасибо. Поправил.

      Нет, всё проверял. Внизу статьи рабочий проект с этим кодом. Просто потерялось при копировании.
  • 0
    удалил — не туда

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка