Создание приложения универсальной платформы Windows для чтения блогов (C++)

В данном разделе подробно рассказывается, как использовать C++ и XAML для разработки приложения универсальной платформы Windows, которое можно развертывать в Windows 10. Приложение служит для чтения блогов из каналов RSS 2.0 или Atom 1.0.

В данном учебнике предполагается, что вы уже знакомы с понятиями, рассмотренными в разделе Создание первого приложения Магазина Windows на C++.

Чтобы изучить готовую версию этого приложения, скачайте ее с веб-сайта коллекции примеров кода MSDN.

В данном учебнике используется среда Visual Studio Community 2015 или более поздней версии. В других выпусках Visual Studio команды меню могут немного отличаться.

Учебники для других языков программирования см. в указанных ниже разделах.

Цели

Из этого учебника вы узнаете, как создавать многостраничные приложения Магазина Windows, как и когда использовать расширения компонентов Visual C++ (C++ и CX), чтобы упростить работу по написанию кода (по сравнению с кодом для среды выполнения Windows). Кроме того, вы научитесь применять класс concurrency::task для использования асинхронных API среды выполнения Windows.

Приложение SimpleBlogReader обладает указанными ниже возможностями.

  • Получение доступа к данным по каналам RSS и Atom через Интернет.
  • Отображение списка веб-каналов и заголовков каналов.
  • Предоставление двух способов чтения публикации в виде простого текста или веб-страницы.
  • Поддержка управления жизненным циклом процесса (PLM) и корректное сохранение и перезагрузка состояния, если система завершает работу приложения во время выполнения другой задачи.
  • Адаптация к различным размерам окна и к разным ориентациям устройства (альбомная или книжная).
  • Предоставление пользователю возможности добавлять и удалять каналы.

Часть 1. Настройка проекта

Для начала создадим проект на основе шаблона пустого приложения на C++ (универсальное приложение Windows).

Hh465045.wedge(ru-ru,WIN.10).gifСоздание проекта

  • В Visual Studio выберите пункты Файл > Создать > Проект, затем выберите Установленные > Visual C++ > Windows > Универсальные приложения. В средней области выберите шаблон Пустое приложение (универсальное приложение Windows). Назовите решение SimpleBlogReader. Более подробные инструкции см. в разделе Создание приложения "Здравствуй, мир!" (C++).

Начнем работу с добавления всех страниц. Проще добавить все страницы сразу, поскольку каждая страница должна #включать страницу, к которой от нее осуществляется переход.

Hh465045.wedge(ru-ru,WIN.10).gifДобавление страниц приложения для Windows

  1. Фактически, мы начнем с разрушения. Щелкните правой кнопкой мыши файл MainPage.xaml и выберите пункт Удалить, а затем щелкните Удалить, чтобы окончательно удалить этот файл и файлы кода программной части. Это тип пустой страницы, который нуждается в поддержке навигации. Щелкните правой кнопкой мыши узел проекта и выберите Добавить > Новый элемент. Добавление нового элемента в Visual C++
  2. На левой панели выберите «XAML», а на центральной панели выберите Страница элементов. Введите имя «MainPage.xaml» и щелкните ОК. Вы увидите окно сообщения с вопросом о добавлении новых файлов в проект. Щелкните Да. В коде запуска следует сослаться на классы SuspensionManager и NavigationHelper, определенные в файлах, которые Visual Studio размещает в новой папке «Общее».
  3. Добавьте SplitPage и примите имя по умолчанию.
  4. Добавьте BasicPage и введите имя «WebViewerPage».

Мы добавим элементы пользовательского интерфейса на эти страницы позже.

Hh465045.wedge(ru-ru,WIN.10).gifДобавление страниц приложения для телефона

  1. В обозревателе решений откройте проект для Windows Phone 8.1. Правой кнопкой мыши щелкните файл MainPage.xaml, выберите Удалить > Окончательно удалить,
  2. Добавьте новую Простую страницу XAML и назовите файл «MainPage.xaml». Щелкните Да так же, как и для проекта Windows.
  3. Вы можете заметить, что множество шаблонов страниц более ограничено в проекте для телефона; в этом приложении мы используем только простые страницы. Добавьте еще три простые страницы и назовите их «FeedPage», «TextViewerPage» и «WebViewerPage».

Часть 2. Создание модели данных

Приложения Магазина, основанные на шаблонах Visual Studio, в общих чертах олицетворяют архитектуру MVVM. В нашем приложении модель состоит из классов, которые инкапсулируют каналы блогов. Каждая страница XAML в приложении образовывает определенное представление этих данных, и каждый класс страницы имеет собственную модель представления, которая является свойством с названием «DefaultViewModel» и типом «Map<String^,Object^>». Эта карта сохраняет данные, к которым привязаны элементы управления XAML на странице и служит контекстом данных страницы.

Наша модель состоит из трех классов. Класс FeedData представляет универсальный код ресурса (URI) верхнего уровня и метаданные для канала блога. Канал на странице https://blogs.windows.com/windows/b/buildingapps/rss.aspx является примером того, что инкапсулирует FeedData. Канал имеет список публикаций блога, который мы представляем как объекты FeedItem. Каждый FeedItem представляет одну публикацию и содержит заголовок, содержимое, URI и другие метаданные. Публикации на странице https://blogs.windows.com/windows/b/buildingapps/archive/2014/05/28/using-the-windows-phone-emulator-for-testing-apps-with-geofencing.aspx являются примером FeedItem. Первая страница в нашем приложении — это представление каналов, вторая страница — представление FeedItems для единого канала и последние две страницы предоставляют различные представления одной публикации: в виде простого текста или в виде веб-страницы.

Класс FeedDataSource содержит коллекцию элементов FeedData и методы их загрузки.

Подведем итоги.

  • FeedData содержит информацию о веб-канале RSS или Atom.

  • FeedItem содержит информацию об отдельных записях блога в канале.

  • FeedDataSource содержит методы для скачивания веб-каналов и инициализации наших классов данных.

Классы определяются как открытые ссылочные классы для возможности использования привязки данных; элементы управления XAML не могут взаимодействовать со стандартными классами C++. Чтобы указать компилятору XAML, что выполняется динамическая привязка к экземплярам этих типов, используется атрибут Bindable. В открытых ссылочных классах открытые элементы данных отображаются как свойства. Свойства, не имеющие особой логики, не требуют определяемых пользователем методов getter и setter — их предоставляет компилятор. В классе FeedData отметьте, как используется Windows::Foundation::Collections::IVector для предоставления открытого типа коллекции. Класс Platform::Collections::Vector используется как конкретный тип, реализующий IVector.

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

Hh465045.wedge(ru-ru,WIN.10).gifСоздание пользовательских классов данных

  1. В обозревателе решений откройте контекстное меню узла проекта SimpleBlogReader.Shared и выберите пункты Добавить > Создать элемент. Выберите параметр Файл заголовка (.h) и дайте ему имя «FeedData.h».

  2. Откройте файл FeedData.h и вставьте в него следующий код. Обратите внимание на директиву #include для pch.h — это наш скомпилированный заголовок, а также место размещения системных заголовков, которые не сильно изменяются или не изменяются вовсе. По умолчанию pch.h включает collection.h, который необходим для типа Platform::Collections::Vector, и ppltasks.h, необходимый для concurrency::task и связанных типов. Эти заголовки включают и <string>, и <vector>, необходимые для нашего приложения, поэтому мы не должны явно включать их.

    //feeddata.h
    
    #pragma once
    #include "pch.h"
    
    namespace SimpleBlogReader
    {
    
        namespace WFC = Windows::Foundation::Collections;
        namespace WF = Windows::Foundation;
        namespace WUIXD = Windows::UI::Xaml::Documents;
        namespace WWS = Windows::Web::Syndication;
    
    
        /// <summary>
        /// To be bindable, a class must be defined within a namespace
        /// and a bindable attribute needs to be applied.
        /// A FeedItem represents a single blog post.
        /// </summary>
        [Windows::UI::Xaml::Data::Bindable]
        public ref class FeedItem sealed
        {
        public:
            property Platform::String^ Title;
            property Platform::String^ Author;
            property Platform::String^ Content;
            property Windows::Foundation::DateTime PubDate;
            property Windows::Foundation::Uri^ Link;
    
        private:
            ~FeedItem(void){}
        };
    
        /// <summary>
        /// A FeedData object represents a feed that contains 
        /// one or more FeedItems. 
        /// </summary>
        [Windows::UI::Xaml::Data::Bindable]
        public ref class FeedData sealed
        {
        public:
            FeedData(void)
            {
                m_items = ref new Platform::Collections::Vector<FeedItem^>();
            }
    
            // The public members must be Windows Runtime types so that
            // the XAML controls can bind to them from a separate .winmd.
            property Platform::String^ Title;
            property WFC::IVector<FeedItem^>^ Items
            {
                WFC::IVector<FeedItem^>^ get() { return m_items; }
            }
    
            property Platform::String^ Description;
            property Windows::Foundation::DateTime PubDate;
            property Platform::String^ Uri;
    
        private:
            ~FeedData(void){}
            Platform::Collections::Vector<FeedItem^>^ m_items;
        };
    }
    

    Эти классы являются ссылочными, поскольку классы XAML среды выполнения Windows должны взаимодействовать с ними, чтобы обеспечить привязку данных к пользовательскому интерфейсу. Атрибут [Bindable] (Привязываемый) также требуется в этих классах для привязки данных. Механизм привязки не сможет обнаружить их без этого атрибута.

Часть 3. Скачивание данных

Класс FeedDataSource содержит методы, которые скачивают веб-каналы, и некоторые другие вспомогательные методы. Он также содержит коллекцию скачанных каналов, которая добавляется в значение «Элементы» в DefaultViewModel главной страницы приложения. FeedDataSource использует класс Windows::Web::Syndication::SyndicationClient, чтобы выполнить скачивание. Поскольку сетевые операции могут занимать некоторое время, эти операции являются асинхронными. По завершении скачивания канала объект FeedData инициализируется и добавляется в коллекцию FeedDataSource::Feeds. Это IObservable<T>, что означает, что в пользовательском интерфейсе на главной странице будет отображено уведомление о добавлении элемента. Для асинхронных операций мы используем класс concurrency::task и связанные классы и методы из файла ppltasks.h. Функция create_task используется для создания программы-оболочки IAsyncOperation, а функция IAsyncAction вызывает API Windows. Функция-член task::then используется для исполнения кода, который ожидает завершения задачи.

Хорошей особенностью приложения является то, что пользователь не должен ожидать завершения скачивания всех каналов. Пользователь может нажать на канал сразу после его отображения и перейти на новую страницу, где отображаются все элементы этого канала. Это пример «быстрого и плавного» пользовательского интерфейса, который стал возможным, благодаря большому объему проделанной работы с фоновыми потоками. Мы увидим его в действии, когда добавим главную страницу XAML.

Однако асинхронные операции добавляют некоторые сложности — за «скорость и плавность» нужно «платить». Если вы читали предыдущие учебники, вы знаете, что приложение, которое на данный момент не активно, может быть закрыто системой, чтобы освободить память, и его работа восстанавливается, когда пользователь снова переключается к нему. В нашем приложении мы не сохраняем все данные канала при завершении работы, поскольку это бы заняло много места в хранилище и, возможно, мы бы получили устаревшие данные. Мы всегда скачиваем каналы при запуске. Но это означает, что необходимо определить сценарий, по которому приложение возобновляет работу после завершения и немедленно пытается отобразить объект FeedData, скачивание которого еще не завершено. Необходимо исключить попытки отображения данных, которые еще недоступны. В этом случае мы не можем использовать метод then, но можем использовать task_completed_event. Это событие не позволит любому коду получать доступ к объекту FeedData, пока загрузка этого объекта не будет завершена.

Hh465045.wedge(ru-ru,WIN.10).gif

  1. Добавьте класс FeedDataSource в файл FeedData.h в пространство имен SimpleBlogReader.

        /// <summary>
        /// A FeedDataSource represents a collection of FeedData objects
        /// and provides the methods to retrieve the stores URLs and download 
        /// the source data from which FeedData and FeedItem objects are constructed.
        /// This class is instantiated at startup by this declaration in the 
        /// ResourceDictionary in app.xaml: <local:FeedDataSource x:Key="feedDataSource" /> 
        /// </summary>
        [Windows::UI::Xaml::Data::Bindable]
        public ref class FeedDataSource sealed
        {
        private:
            Platform::Collections::Vector<FeedData^>^ m_feeds;
            FeedData^ GetFeedData(Platform::String^ feedUri, WWS::SyndicationFeed^ feed);
            concurrency::task<WFC::IVector<Platform::String^>^> GetUserURLsAsync();
            void DeleteBadFeedHandler(Windows::UI::Popups::UICommand^ command);
    
        public:
            FeedDataSource();
            property Windows::Foundation::Collections::IObservableVector<FeedData^>^ Feeds
            {
                Windows::Foundation::Collections::IObservableVector<FeedData^>^ get()
                {
                    return this->m_feeds;
                }
            }
            property Platform::String^ CurrentFeedUri;
            void InitDataSource();        
    
        internal:
            // This is used to prevent SplitPage from prematurely loading the last viewed page on resume.
            concurrency::task_completion_event<FeedData^> m_LastViewedFeedEvent;
            concurrency::task<void> RetrieveFeedAndInitData(Platform::String^ url, WWS::SyndicationClient^ client);
        };
    
  2. Теперь в общем проекте создайте файл FeedData.cpp и вставьте в него указанный ниже код.

    #include "pch.h"
    #include "FeedData.h"
    
    using namespace std;
    using namespace concurrency;
    using namespace SimpleBlogReader;
    using namespace Platform;
    using namespace Platform::Collections;
    using namespace Windows::Foundation;
    using namespace Windows::Foundation::Collections;
    using namespace Windows::Web::Syndication;
    using namespace Windows::Storage;
    using namespace Windows::Storage::Streams;
    
    FeedDataSource::FeedDataSource()
    {
           m_feeds = ref new Vector<FeedData^>();
           CurrentFeedUri = "";
    }
    
    ///<summary>
    /// Uses SyndicationClient to get the top-level feed object, then initializes 
    /// the app's data structures. In the case of a bad feed URL, the exception is 
    /// caught and the user can permanently delete the feed.
    ///</summary>
    task<void> FeedDataSource::RetrieveFeedAndInitData(String^ url, SyndicationClient^ client)
    {
           // Create the async operation. feedOp is an 
           // IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^
           auto feedUri = ref new Uri(url);
           auto feedOp = client->RetrieveFeedAsync(feedUri);
    
           // Create the task object and pass it the async operation.
           // SyndicationFeed^ is the type of the return value that the feedOp 
           // operation will pass to the continuation. The continuation can run
           // on any thread.
           return create_task(feedOp).then([this, url](SyndicationFeed^ feed) -> FeedData^
           {
                  return GetFeedData(url, feed);
           }, concurrency::task_continuation_context::use_arbitrary())
    
                  // Append the initialized FeedData object to the items collection.
                  // This has to happen on the UI thread. By default, a .then
                  // continuation runs in the same apartment that it was called on.
                  // We can append safely to the Vector from multiple threads
                  // without taking an explicit lock.
                  .then([this, url](FeedData^ fd)
           {
                  if (fd->Uri == CurrentFeedUri)
                  {
                         // By setting the event we tell the resuming SplitPage the data
                         // is ready to be consumed.
                         m_LastViewedFeedEvent.set(fd);
                  }
    
                  m_feeds->Append(fd);
    
           })
    
                  // The last continuation serves as an error handler.
                  // get() will surface any unhandled exceptions in this task chain.
                  .then([this, url](task<void> t)
           {
                  try
                  {
                         t.get();
                  }
    
                  catch (Platform::Exception^ e)
                  {
                         // Sometimes a feed URL changes(I'm talking to you, Windows blogs!)
                         // When that happens, or when the users pastes in an invalid URL or a 
                         // URL is valid but the content is malformed somehow, an exception is 
                         // thrown in the task chain before the feed is added to the Feeds 
                         // collection. The only recourse is to stop trying to read the feed.
                         // That means deleting it from the feeds.txt file in local settings.
                         SyndicationErrorStatus status = SyndicationError::GetStatus(e->HResult);
                         String^ msgString;
    
                         // Define the action that will occur when the user presses the popup button.
                         auto handler = ref new Windows::UI::Popups::UICommandInvokedHandler(
                               [this, url](Windows::UI::Popups::IUICommand^ command)
                         {
                               auto app = safe_cast<App^>(App::Current);
                               app->DeleteUrlFromFeedFile(url);
                         });
    
                         // Display a message that hopefully is helpful.
                         if (status == SyndicationErrorStatus::InvalidXml)
                         {
                               msgString = "There seems to be a problem with the formatting in this feed: ";
                         }
    
                         if (status == SyndicationErrorStatus::Unknown)
                         {
                               msgString = "I can't load this feed (is the URL correct?): ";
                         }
    
                         // Show the popup.
                         auto msg = ref new Windows::UI::Popups::MessageDialog(
                               msgString + url);
                         auto cmd = ref new Windows::UI::Popups::UICommand(
                               ref new String(L"Forget this feed."), handler, 1);
                         msg->Commands->Append(cmd);
                         msg->ShowAsync();
                  }
           }); //end task chain
    }
    
    ///<summary>
    /// Retrieve the data for each atom or rss feed and put it into our custom data structures.
    ///</summary>
    void FeedDataSource::InitDataSource()
    {
           // Hard code some feeds for now. Later in the tutorial we'll improve this.
           auto urls = ref new Vector<String^>();
           urls->Append(L"http://sxp.microsoft.com/feeds/3.0/devblogs");
           urls->Append(L"https://blogs.windows.com/windows/b/bloggingwindows/rss.aspx");
           urls->Append(L"https://azure.microsoft.com/blog/feed");
    
           // Populate the list of feeds.
           SyndicationClient^ client = ref new SyndicationClient();
           for (auto url : urls)
           {
                  RetrieveFeedAndInitData(url, client);
           }
    }
    
    ///<summary>
    /// Creates our app-specific representation of a FeedData.
    ///</summary>
    FeedData^ FeedDataSource::GetFeedData(String^ feedUri, SyndicationFeed^ feed)
    {
           FeedData^ feedData = ref new FeedData();
    
           // Store the Uri now in order to map completion_events 
           // when resuming from termination.
           feedData->Uri = feedUri;
    
           // Get the title of the feed (not the individual posts).
           // auto app = safe_cast<App^>(App::Current);
           TextHelper^ helper = ref new TextHelper();
    
           feedData->Title = helper->UnescapeText(feed->Title->Text);
           if (feed->Subtitle != nullptr)
           {
                  feedData->Description = helper->UnescapeText(feed->Subtitle->Text);
           }
    
           // Occasionally a feed might have no posts, so we guard against that here.
           if (feed->Items->Size > 0)
           {
                  // Use the date of the latest post as the last updated date.
                  feedData->PubDate = feed->Items->GetAt(0)->PublishedDate;
    
                  for (auto item : feed->Items)
                  {
                         FeedItem^ feedItem;
                         feedItem = ref new FeedItem();
                         feedItem->Title = helper->UnescapeText(item->Title->Text);
                         feedItem->PubDate = item->PublishedDate;
    
                         //Only get first author in case of multiple entries.
                         item->Authors->Size > 0 ? feedItem->Author =
                               item->Authors->GetAt(0)->Name : feedItem->Author = L"";
    
                         if (feed->SourceFormat == SyndicationFormat::Atom10)
                         {
                               // Sometimes a post has only the link to the web page
                               if (item->Content != nullptr)
                               {
                                      feedItem->Content = helper->UnescapeText(item->Content->Text);
                               }
                               feedItem->Link = ref new Uri(item->Id);
                         }
                         else
                         {
                               feedItem->Content = item->Summary->Text;
                               feedItem->Link = item->Links->GetAt(0)->Uri;
                         }
                         feedData->Items->Append(feedItem);
                  };
           }
           else
           {
                  feedData->Description = "NO ITEMS AVAILABLE." + feedData->Description;
           }
    
           return feedData;
    
    } //end GetFeedData
    
  3. Теперь добавим экземпляр FeedDataSource в наше приложение. В файле app.xaml.h добавьте директиву #include для файла FeedData.h, чтобы сделать типы видимыми.

        #include "FeedData.h"
    
    • В общем проекте в файле App.xaml добавьте узел Application.Resources и поместите в него ссылку на FeedDataSource, чтобы страница выглядела указанным ниже образом.

          <Application
              x:Class="SimpleBlogReader.App"
              xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:local="using:SimpleBlogReader">
      
              <Application.Resources>
                  <local:FeedDataSource x:Key="feedDataSource" />    
              </Application.Resources>
      </Application>
      

      Такая разметка вызовет создание объекта FeedDataSource при запуске приложения. Доступ к этому объекту можно получить с любой страницы в приложении. При возникновении события OnLaunched объект приложения вызовет InitDataSource, чтобы экземпляр feedDataSource начал скачивание всех своих данных.

      Проект еще не закончен, так как нам необходимо добавить некоторые дополнительные определения класса.

Часть 4. Обработка синхронизации данных при возобновлении после завершения

При первом запуске и во время перехода пользователя между страницами не требуется синхронизация доступа к данным. Каналы появляются только на первой странице после их инициализации, а другие страницы не пытаются получить доступ к данным, пока пользователь не нажмет на видимый канал. После этого мы получаем доступ только для чтения. Мы никогда не изменяем данные источника. Однако, есть один сценарий, который требует синхронизации: когда приложение завершает работу при активной странице, основанной на определенном канале, эту страницу понадобится заново привязать к этим данным веб-канала после возобновления работы приложения. В этом случае страница может попытаться получить доступ к данным, которые еще не существуют. Поэтому нам нужен способ заставить страницу подождать, пока данные не будут готовы.

Следующие функции позволяют приложению запомнить, какой канал оно просматривало. Метод SetCurrentFeed просто сохраняет канал в локальные параметры, откуда он может быть получен даже после того, как приложение выходит из памяти. Метод GetCurrentFeedAsync также интересен, поскольку необходимо указать, что когда мы возвращаемся и хотим заново привязать последний канал, мы не пытаемся сделать это до перезагрузки канала. Мы рассмотрим этот код подробнее дальше. Мы добавим код в класс App, так как мы будем вызывать его и из приложения для Windows, и из приложения для телефона.

  1. В файле app.xaml.h добавьте указанные ниже подписи методов. Внутренняя доступность означает, что их можно использовать только из другого кода C++ в том же пространстве имен.

        internal:
        concurrency::task<FeedData^> GetCurrentFeedAsync();
        void SetCurrentFeed(FeedData^ feed); 
        FeedItem^ GetFeedItem(FeedData^ fd, Platform::String^ uri);
        void AddFeed(Platform::String^ feedUri);
        void RemoveFeeds(Platform::Collections::Vector<FeedData^>^ feedsToDelete);
        void DeleteUrlFromFeedFile(Platform::String^ s);
    
  2. Затем в начало файла app.xaml.cpp добавьте указанные ниже операторы using.

        using namespace concurrency;
        using namespace Platform::Collections;
        using namespace Windows::Storage;
    

    Вам потребуются пространство имен параллельной обработки для задачи, пространство имен Platform::Collections для Vector и пространство имен Windows::Storage для ApplicationData.

    В конец файла добавьте указанные ниже строки.

    ///<summary>
    /// Grabs the URI that the user entered, then inserts it into the in-memory list
    /// and retrieves the data. Then adds the new feed to the data file so it's 
    /// there the next time the app starts up.
    ///</summary>
    void App::AddFeed(String^ feedUri)
    {
        auto feedDataSource =
            safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource"));
        auto client = ref new Windows::Web::Syndication::SyndicationClient();
    
        // The UI is data-bound to the items collection and will update automatically
        // after we append to the collection.
        create_task(feedDataSource->RetrieveFeedAndInitData(feedUri, client))
            .then([this, feedUri] {
    
            // Add the uri to the roaming data. The API requires an IIterable so we have to 
            // put the uri in a Vector.
            Vector<String^>^ vec = ref new Vector<String^>();
            vec->Append(feedUri);
            concurrency::create_task(ApplicationData::Current->LocalFolder->
                CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists))
                .then([vec](StorageFile^ file)
            {
                FileIO::AppendLinesAsync(file, vec);
            });
        });
    }
    
    /// <summary>
    /// Called when the user chooses to remove some feeds which otherwise
    /// are valid Urls and currently are displaying in the UI, and are stored in 
    /// the Feeds collection as well as in the feeds.txt file.
    /// </summary>
    void App::RemoveFeeds(Vector<FeedData^>^ feedsToDelete)
    {
        // Create a new list of feeds, excluding the ones the user selected.
        auto feedDataSource =
            safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource"));  
    
        // If we delete the "last viewed feed" we need to also remove the reference to it
        // from local settings.
        ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings;
        String^ lastViewed;
    
        if (localSettings->Values->HasKey("LastViewedFeed"))
        {
            lastViewed =
                safe_cast<String^>(localSettings->Values->Lookup("LastViewedFeed"));
        }
    
        // When performance is an issue, consider using Vector::ReplaceAll
        for (const auto& item : feedsToDelete)
        {
            unsigned int index = -1;
            bool b = feedDataSource->Feeds->IndexOf(item, &index);
            if (index >= 0)
            {
                feedDataSource->Feeds->RemoveAt(index);           
            }
    
            // Prevent ourself from trying later to reference 
            // the page we just deleted.
            if (lastViewed != nullptr && lastViewed == item->Title)
            {
                localSettings->Values->Remove("LastViewedFeed");
            }
        }
    
        // Re-initialize feeds.txt with the new list of URLs.
        Vector<String^>^ newFeedList = ref new Vector<String^>();
        for (const auto& item : feedDataSource->Feeds)
        {
            newFeedList->Append(item->Uri);
        }
    
        // Overwrite the old data file with the new list.
        create_task(ApplicationData::Current->LocalFolder->
            CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists))
            .then([newFeedList](StorageFile^ file)
        {
            FileIO::WriteLinesAsync(file, newFeedList);
        });
    }
    
    
    ///<summary>
    /// This function enables the user to back out after
    /// entering a bad url in the "Add Feed" text box, for example pasting in a 
    /// partial address. This function will also be called if a URL that was previously 
    /// formatted correctly one day starts returning malformed XML when we try to load it.
    /// In either case, the FeedData was not added to the Feeds collection, and so 
    /// we only need to delete the URL from the data file.
    /// </summary>
    void App::DeleteUrlFromFeedFile(Platform::String^ s)
    {
        // Overwrite the old data file with the new list.
        create_task(ApplicationData::Current->LocalFolder->
            CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists))
            .then([this](StorageFile^ file)
        {
            return FileIO::ReadLinesAsync(file);
        }).then([this, s](IVector<String^>^ lines)
        {
            for (unsigned int i = 0; i < lines->Size; ++i)
            {
                if (lines->GetAt(i) == s)
                {
                    lines->RemoveAt(i);
                }
            }
            return lines;
        }).then([this](IVector<String^>^ lines)
        {
            create_task(ApplicationData::Current->LocalFolder->
                CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists))
                .then([this, lines](StorageFile^ file)
            {
                FileIO::WriteLinesAsync(file, lines);
            });
        });
    }
    
    ///<summary>
    /// Returns the feed that the user last selected from MainPage.
    ///<summary>
    task<FeedData^> App::GetCurrentFeedAsync()
    {
        FeedDataSource^ feedDataSource = 
            safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource"));
        return create_task(feedDataSource->m_LastViewedFeedEvent);
    }
    
    ///<summary>
    /// So that we can always get the current feed in the same way, we call this 
    // method from ItemsPage when we change the current feed. This way the caller 
    // doesn't care whether we're resuming from termination or new navigating.
    // The only other place we set the event is in InitDataSource in FeedData.cpp 
    // when resuming from termination.
    ///</summary>
    
    void App::SetCurrentFeed(FeedData^ feed)
    {
        // Enable any pages waiting on the FeedData to continue
        FeedDataSource^ feedDataSource = 
            safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource"));
        feedDataSource->m_LastViewedFeedEvent = task_completion_event<FeedData^>();
        feedDataSource->m_LastViewedFeedEvent.set(feed);
    
        // Store the current URI so that we can look up the correct feedData object on resume.
        ApplicationDataContainer^ localSettings = 
            ApplicationData::Current->LocalSettings;
        auto values = localSettings->Values;
        values->Insert("LastViewedFeed", 
            dynamic_cast<PropertyValue^>(PropertyValue::CreateString(feed->Uri)));
    }
    
    // We stored the string ID when the app was suspended
    // because storing the FeedItem itself would have required
    // more custom serialization code. Here is where we retrieve
    // the FeedItem based on its string ID.
    FeedItem^ App::GetFeedItem(FeedData^ fd, String^ uri)
    {
        auto items = fd->Items;
        auto itEnd = end(items);
        auto it = std::find_if(begin(items), itEnd,
            [uri](FeedItem^ fi)
        {
            return fi->Link->AbsoluteUri == uri;
        });
    
        if (it != itEnd)
            return *it;
    
        return nullptr;
    }
    

Часть 5. Преобразование данных в пригодные для использования формы

Не все необработанные данные существуют в пригодной для использования форме. Канал RSS или Atom отображает свою дату публикации как числовое значение RFC 822. Нам нужен способ преобразовать его в текст, который имеет смысл для пользователя. Для этого мы создадим пользовательский класс, который реализует преобразователь IValueConverter и принимает значение RFC833 как строки ввода и вывода для каждого компонента даты. Позже, в XAML, который представляет данные, мы привязываемся к данным вывода класса DateConverter вместо необработанного формата данных.

Hh465045.wedge(ru-ru,WIN.10).gifДобавление преобразователя дат

  1. В общем проекте создайте новый h-файл и добавьте следующий код:

    //DateConverter.h
    
    #pragma once
    #include <string> //for wcscmp
    #include <regex>
    
    namespace SimpleBlogReader
    {
        namespace WGDTF = Windows::Globalization::DateTimeFormatting;
    
        /// <summary>
        /// Implements IValueConverter so that we can convert the numeric date
        /// representation to a set of strings.
        /// </summary>
        public ref class DateConverter sealed : 
            public Windows::UI::Xaml::Data::IValueConverter
        {
        public:
            virtual Platform::Object^ Convert(Platform::Object^ value,
                Windows::UI::Xaml::Interop::TypeName targetType,
                Platform::Object^ parameter,
                Platform::String^ language)
            {
                if (value == nullptr)
                {
                    throw ref new Platform::InvalidArgumentException();
                }
                auto dt = safe_cast<Windows::Foundation::DateTime>(value);
                auto param = safe_cast<Platform::String^>(parameter);
                Platform::String^ result;
                if (param == nullptr)
                {
                    auto dtf = WGDTF::DateTimeFormatter::ShortDate::get();
                    result = dtf->Format(dt);
                }
                else if (wcscmp(param->Data(), L"month") == 0)
                {
                    auto formatter =
                        ref new WGDTF::DateTimeFormatter("{month.abbreviated(3)}");
                    result = formatter->Format(dt);
                }
                else if (wcscmp(param->Data(), L"day") == 0)
                {
                    auto formatter =
                        ref new WGDTF::DateTimeFormatter("{day.integer(2)}");
                    result = formatter->Format(dt);
                }
                else if (wcscmp(param->Data(), L"year") == 0)
                {
                    auto formatter =
                        ref new WGDTF::DateTimeFormatter("{year.full}");
                    auto tempResult = formatter->Format(dt); //e.g. "2014"
    
                    // Insert a hard return after second digit to get the rendering 
                    // effect we want
                    std::wregex r(L"(\\d\\d)(\\d\\d)");
                    result = ref new Platform::String(
                        std::regex_replace(tempResult->Data(), r, L"$1\n$2").c_str());
                }
                else
                {
                    // We don't handle other format types currently.
                    throw ref new Platform::InvalidArgumentException();
                }
    
                return result;
            }
    
            virtual Platform::Object^ ConvertBack(Platform::Object^ value,
                Windows::UI::Xaml::Interop::TypeName targetType,
                Platform::Object^ parameter,
                Platform::String^ language)
            {
                // Not needed in SimpleBlogReader. Left as an exercise.
                throw ref new Platform::NotImplementedException();
            }
        };
    }
    
  2. Теперь #включите его в файл App.xaml.h:

    #include "DateConverter.h"
    
  3. И создайте его экземпляр в файле App.xaml в узле Application.Resources:

    <local:DateConverter x:Key="dateConverter" />
    

Содержимое канала поставляется по проводу в виде HTML или, в некоторых случаях, форматированного текста XML. Для отображения этого содержимого в RichTextBlock необходимо преобразовать его в форматированный текст. Следующий класс использует функцию Windows HtmlUtilities для анализа HTML, а затем использует функции <regex>, чтобы разделить его на абзацы, чтобы мы могли создавать объекты форматированного текста. Мы не можем использовать привязку данных в этом сценарии, поэтому нет необходимости реализовывать IValueConverter для этого класса. Мы просто создадим его локальные экземпляры на необходимых нам страницах.

Hh465045.wedge(ru-ru,WIN.10).gifДобавление преобразователя текста

  1. В общем проекте добавьте новый h-файл, назовите его TextHelper.h и добавьте следующий код:

    #pragma once
    
    namespace SimpleBlogReader
    {
        namespace WFC = Windows::Foundation::Collections;
        namespace WF = Windows::Foundation;
        namespace WUIXD = Windows::UI::Xaml::Documents;
    
        public ref class TextHelper sealed
        {
        public:
            TextHelper();
            WFC::IVector<WUIXD::Paragraph^>^ CreateRichText(
                Platform::String^ fi,
                WF::TypedEventHandler < WUIXD::Hyperlink^,
                WUIXD::HyperlinkClickEventArgs^ > ^ context);
    
            Platform::String^ UnescapeText(Platform::String^ inStr);
    
        private:
    
            std::vector<std::wstring> SplitContentIntoParagraphs(const std::wstring& s, 
                const std::wstring& rgx);
            std::wstring UnescapeText(const std::wstring& input);
    
            // Maps some HTML entities that we'll use to replace the escape sequences
            // in the call to UnescapeText when we create feed titles and render text. 
            std::map<std::wstring, std::wstring> entities;
        };
    }
    
  2. Теперь добавьте TextHelper.cpp:

    #include "pch.h"
    #include "TextHelper.h"
    
    using namespace std;
    using namespace SimpleBlogReader;
    using namespace Platform;
    using namespace Platform::Collections;
    using namespace Windows::Foundation;
    using namespace Windows::Foundation::Collections;
    
    using namespace Windows::Data::Html;
    using namespace Windows::UI::Xaml::Documents;
    
    /// <summary>
    /// Note that in this example we don't map all the possible HTML entities. Feel free to improve this.
    /// Also note that we initialize the map like this because VS2013 Udpate 3 does not support list
    /// initializers in a member declaration.
    /// </summary>
    TextHelper::TextHelper() : entities(
        {
            { L"&#60;", L"<" }, { L"&#62;", L">" }, { L"&#38;", L"&" }, { L"&#162;", L"¢" }, 
            { L"&#163;", L"£" }, { L"&#165;", L"¥" }, { L"&#8364;", L"€" }, { L"&#8364;", L"©" },
            { L"&#174;", L"®" }, { L"&#8220;", L"“" }, { L"&#8221;", L"”" }, { L"&#8216;", L"‘" },
            { L"&#8217;", L"’" }, { L"&#187;", L"»" }, { L"&#171;", L"«" }, { L"&#8249;", L"‹" },
            { L"&#8250;", L"›" }, { L"&#8226;", L"•" }, { L"&#176;", L"°" }, { L"&#8230;", L"…" },
            { L"&#160;", L" " }, { L"&quot;", LR"(")" }, { L"&apos;", L"'" }, { L"&lt;", L"<" },
            { L"&gt;", L">" }, { L"&rsquo;", L"’" }, { L"&nbsp;", L" " }, { L"&amp;", L"&" }
        })
    {  
    }
    
    ///<summary>
    /// Accepts the Content property from a Feed and returns rich text
    /// paragraphs that can be passed to a RichTextBlock.
    ///</summary>
    String^ TextHelper::UnescapeText(String^ inStr)
    {
        wstring input(inStr->Data());
        wstring result = UnescapeText(input);
        return ref new Platform::String(result.c_str());
    }
    
    ///<summary>
    /// Create a RichText block from the text retrieved by the HtmlUtilies object. 
    /// For a more full-featured app, you could parse the content argument yourself and
    /// add the page's images to the inlines collection.
    ///</summary>
    IVector<Paragraph^>^ TextHelper::CreateRichText(String^ content,
        TypedEventHandler<Hyperlink^, HyperlinkClickEventArgs^>^ context)
    {
        std::vector<Paragraph^> blocks; 
    
        auto text = HtmlUtilities::ConvertToText(content);
        auto parts = SplitContentIntoParagraphs(wstring(text->Data()), LR"(\r\n)");
    
        // Add the link at the top. Don't set the NavigateUri property because 
        // that causes the link to open in IE even if the Click event is handled. 
        auto hlink = ref new Hyperlink();
        hlink->Click += context;
        auto linkText = ref new Run();
        linkText->Foreground = 
            ref new Windows::UI::Xaml::Media::SolidColorBrush(Windows::UI::Colors::DarkRed);
        linkText->Text = "Link";
        hlink->Inlines->Append(linkText);
        auto linkPara = ref new Paragraph();
        linkPara->Inlines->Append(hlink);
        blocks.push_back(linkPara);
    
        for (auto part : parts)
        {
            auto p = ref new Paragraph();
            p->TextIndent = 10;
            p->Margin = (10, 10, 10, 10);
            auto r = ref new Run();
            r->Text = ref new String(part.c_str());
            p->Inlines->Append(r);
            blocks.push_back(p);
        }
    
        return ref new Vector<Paragraph^>(blocks);
    }
    
    ///<summary>
    /// Split an input string which has been created by HtmlUtilities::ConvertToText
    /// into paragraphs. The rgx string we use here is LR("\r\n") . If we ever use
    /// other means to grab the raw text from a feed, then the rgx will have to recognize
    /// other possible new line formats. 
    ///</summary>
    vector<wstring> TextHelper::SplitContentIntoParagraphs(const wstring& s, const wstring& rgx)
    {    
        const wregex r(rgx);
        vector<wstring> result;
    
        // the -1 argument indicates that the text after this match until the next match
        // is the "capture group". In other words, this is how we match on what is between the tokens.
        for (wsregex_token_iterator rit(s.begin(), s.end(), r, -1), end; rit != end; ++rit)
        {
            if (rit->length() > 0)
            {
                result.push_back(*rit);
            }
        }
        return result;  
    }
    
    ///<summary>
    /// This is used to unescape html entities that occur in titles, subtitles, etc.
    //  entities is a map<wstring, wstring> with key-values like this: { L"&#60;", L"<" },
    /// CAUTION: we must not unescape any content that gets sent to the webView.
    ///</summary>
    wstring TextHelper::UnescapeText(const wstring& input)
    {
        wsmatch match;
    
        // match L"&#60;" as well as "&nbsp;"
        const wregex rgx(LR"(&#?\w*?;)");
        wstring result;
    
        // itrEnd needs to be visible outside the loop
        wsregex_iterator itrEnd, itrRemainingText;
    
        // Iterate over input and build up result as we go along
        // by first appending what comes before the match, then the 
        // unescaped replacement for the HTML entity which is the match,
        // then once at the end appending what comes after the last match.
    
        for (wsregex_iterator itr(input.cbegin(), input.cend(), rgx); itr != itrEnd; ++itr)    
        {
            wstring entity = itr->str();
            map<wstring, wstring>::const_iterator mit = entities.find(entity);
            if (mit != end(entities))
            {
                result.append(itr->prefix());
                result.append(mit->second); // mit->second is the replacement text
                itrRemainingText = itr;
            }
            else 
            {
                // we found an entity that we don't explitly map yet so just 
                // render it in raw form. Exercise for the user: add
                // all legal entities to the entities map.   
                result.append(entity);
                continue; 
            }        
        }
    
        // If we didn't find any entities to escape
        // then (a) don't try to dereference itrRemainingText
        // and (b) return input because result is empty!
        if (itrRemainingText == itrEnd)
        {
            return input;
        }
        else
        {
            // Add any text between the last match and end of input string.
            result.append(itrRemainingText->suffix());
            return result;
        }
    }
    

    Обратите внимание, что наш пользовательский класс TextHelper демонстрирует некоторые способы использования ISO C++ (std::map, std::regex, std::wstring) внутри в приложении на C++/CX. Мы создадим экземпляры этого класса локально на страницах, использующих его. Достаточно один раз включить его в файл App.xaml.h.

    #include "TextHelper.h"
    
  3. Теперь у вас должно получиться выполнить сборку и запуск приложения. Просто не ожидайте от них слишком многого.

Часть 6. Запуск, приостановка и возобновление работы приложения

Событие App::OnLaunched возникает, когда пользователь запускает приложение, нажав плитку приложения, а также когда пользователь возвращается к приложению после того, как система завершила работу приложения, чтобы освободить память для других приложений. В любом случае мы всегда обращаемся к интернету и перезагружаем данные в ответ на это событие. Однако существуют другие действия, которые нужно запускать только в одном случае либо в другом. Можно определить эти состояния, посмотрев на rootFrame в сочетании с аргументом LaunchActivatedEventArgs, который передается функции, а затем выполнить нужное действие. К счастью, класс SuspensionManager, который был добавлен автоматически с помощью MainPage, делает большую часть работы по сохранению и восстановлению состояния приложения при его приостановке и повторном запуске. Нужно просто вызвать его методы.

  1. Добавьте файлы с кодом SuspensionManager в проект в папке Common. Добавьте файл SuspensionManager.h и скопируйте в него следующий код:

    //
    // SuspensionManager.h
    // Declaration of the SuspensionManager class
    //
    
    #pragma once
    
    namespace SimpleBlogReader
    {
        namespace Common
        {
            /// <summary>
            /// SuspensionManager captures global session state to simplify process lifetime management
            /// for an application.  Note that session state will be automatically cleared under a variety
            /// of conditions and should only be used to store information that would be convenient to
            /// carry across sessions, but that should be disacarded when an application crashes or is
            /// upgraded.
            /// </summary>
            class SuspensionManager sealed
            {
            public:
                static void RegisterFrame(Windows::UI::Xaml::Controls::Frame^ frame, Platform::String^ sessionStateKey, Platform::String^ sessionBaseKey = nullptr);
                static void UnregisterFrame(Windows::UI::Xaml::Controls::Frame^ frame);
                static concurrency::task<void> SaveAsync();
                static concurrency::task<void> RestoreAsync(Platform::String^ sessionBaseKey = nullptr);
                static Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ SessionState();
                static Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ SessionStateForFrame(
                    Windows::UI::Xaml::Controls::Frame^ frame);
    
            private:
                static void RestoreFrameNavigationState(Windows::UI::Xaml::Controls::Frame^ frame);
                static void SaveFrameNavigationState(Windows::UI::Xaml::Controls::Frame^ frame);
    
                static Platform::Collections::Map<Platform::String^, Platform::Object^>^ _sessionState;
                static const wchar_t* sessionStateFilename;
    
                static std::vector<Platform::WeakReference> _registeredFrames;
                static Windows::UI::Xaml::DependencyProperty^ FrameSessionStateKeyProperty;
                static Windows::UI::Xaml::DependencyProperty^ FrameSessionBaseKeyProperty;
                static Windows::UI::Xaml::DependencyProperty^ FrameSessionStateProperty;
            };
        }
    }
    
  2. Добавьте файл с кодом SuspensionManager.cpp и скопируйте в него следующий код:

    //
    // SuspensionManager.cpp
    // Implementation of the SuspensionManager class
    //
    
    #include "pch.h"
    #include "SuspensionManager.h"
    
    #include <algorithm>
    
    using namespace SimpleBlogReader::Common;
    
    using namespace concurrency;
    using namespace Platform;
    using namespace Platform::Collections;
    using namespace Windows::Foundation;
    using namespace Windows::Foundation::Collections;
    using namespace Windows::Storage;
    using namespace Windows::Storage::FileProperties;
    using namespace Windows::Storage::Streams;
    using namespace Windows::UI::Xaml;
    using namespace Windows::UI::Xaml::Controls;
    using namespace Windows::UI::Xaml::Interop;
    
    Map<String^, Object^>^ SuspensionManager::_sessionState = ref new Map<String^, Object^>();
    
    const wchar_t* SuspensionManager::sessionStateFilename = L"_sessionState.dat";
    
    std::vector<WeakReference> SuspensionManager::_registeredFrames;
    
    DependencyProperty^ SuspensionManager::FrameSessionStateKeyProperty =
        DependencyProperty::RegisterAttached("_FrameSessionStateKeyProperty",
        TypeName(String::typeid), TypeName(SuspensionManager::typeid), nullptr);
    
    DependencyProperty^ SuspensionManager::FrameSessionBaseKeyProperty =
        DependencyProperty::RegisterAttached("_FrameSessionBaseKeyProperty",
        TypeName(String::typeid), TypeName(SuspensionManager::typeid), nullptr);
    
    DependencyProperty^ SuspensionManager::FrameSessionStateProperty =
        DependencyProperty::RegisterAttached("_FrameSessionStateProperty",
        TypeName(IMap<String^, Object^>::typeid), TypeName(SuspensionManager::typeid), nullptr);
    
    class ObjectSerializeHelper
    {
    public:
        // Codes used for identifying serialized types
        enum StreamTypes {
            NullPtrType = 0,
    
            // Supported IPropertyValue types
            UInt8Type, UInt16Type, UInt32Type, UInt64Type, Int16Type, Int32Type, Int64Type,
            SingleType, DoubleType, BooleanType, Char16Type, GuidType, StringType,
    
            // Additional supported types
            StringToObjectMapType,
    
            // Marker values used to ensure stream integrity
            MapEndMarker
        };
        static String^ ReadString(DataReader^ reader);
        static IMap<String^, Object^>^ ReadStringToObjectMap(DataReader^ reader);
        static Object^ ReadObject(DataReader^ reader);
        static void WriteString(DataWriter^ writer, String^ string);
        static void WriteProperty(DataWriter^ writer, IPropertyValue^ propertyValue);
        static void WriteStringToObjectMap(DataWriter^ writer, IMap<String^, Object^>^ map);
        static void WriteObject(DataWriter^ writer, Object^ object);
    };
    
    /// <summary>
    /// Provides access to global session state for the current session.  This state is serialized by
    /// <see cref="SaveAsync"/> and restored by <see cref="RestoreAsync"/> which require values to be
    /// one of the following: boxed values including integers, floating-point singles and doubles,
    /// wide characters, boolean, Strings and Guids, or Map<String^, Object^> where map values are
    /// subject to the same constraints.  Session state should be as compact as possible.
    /// </summary>
    IMap<String^, Object^>^ SuspensionManager::SessionState()
    {
        return _sessionState;
    }
    
    /// <summary>
    /// Registers a <see cref="Frame"/> instance to allow its navigation history to be saved to
    /// and restored from <see cref="SessionState"/>.  Frames should be registered once
    /// immediately after creation if they will participate in session state management.  Upon
    /// registration if state has already been restored for the specified key
    /// the navigation history will immediately be restored.  Subsequent invocations of
    /// <see cref="RestoreAsync(String)"/> will also restore navigation history.
    /// </summary>
    /// <param name="frame">An instance whose navigation history should be managed by
    /// <see cref="SuspensionManager"/></param>
    /// <param name="sessionStateKey">A unique key into <see cref="SessionState"/> used to
    /// store navigation-related information.</param>
    /// <param name="sessionBaseKey">An optional key that identifies the type of session.
    /// This can be used to distinguish between multiple application launch scenarios.</param>
    void SuspensionManager::RegisterFrame(Frame^ frame, String^ sessionStateKey, String^ sessionBaseKey)
    {
        if (frame->GetValue(FrameSessionStateKeyProperty) != nullptr)
        {
            throw ref new FailureException("Frames can only be registered to one session state key");
        }
    
        if (frame->GetValue(FrameSessionStateProperty) != nullptr)
        {
            throw ref new FailureException("Frames must be either be registered before accessing frame session state, or not registered at all");
        }
    
        if (sessionBaseKey != nullptr)
        {
            frame->SetValue(FrameSessionBaseKeyProperty, sessionBaseKey);
            sessionStateKey = sessionBaseKey + "_" + sessionStateKey;
        }
    
        // Use a dependency property to associate the session key with a frame, and keep a list of frames whose
        // navigation state should be managed
        frame->SetValue(FrameSessionStateKeyProperty, sessionStateKey);
        _registeredFrames.insert(_registeredFrames.begin(), WeakReference(frame));
    
        // Check to see if navigation state can be restored
        RestoreFrameNavigationState(frame);
    }
    
    /// <summary>
    /// Disassociates a <see cref="Frame"/> previously registered by <see cref="RegisterFrame"/>
    /// from <see cref="SessionState"/>.  Any navigation state previously captured will be
    /// removed.
    /// </summary>
    /// <param name="frame">An instance whose navigation history should no longer be
    /// managed.</param>
    void SuspensionManager::UnregisterFrame(Frame^ frame)
    {
        // Remove session state and remove the frame from the list of frames whose navigation
        // state will be saved (along with any weak references that are no longer reachable)
        auto key = safe_cast<String^>(frame->GetValue(FrameSessionStateKeyProperty));
        if (SessionState()->HasKey(key))
        {
            SessionState()->Remove(key);
        }
        _registeredFrames.erase(
            std::remove_if(_registeredFrames.begin(), _registeredFrames.end(), [=](WeakReference& e)
        {
            auto testFrame = e.Resolve<Frame>();
            return testFrame == nullptr || testFrame == frame;
        }),
            _registeredFrames.end()
            );
    }
    
    /// <summary>
    /// Provides storage for session state associated with the specified <see cref="Frame"/>.
    /// Frames that have been previously registered with <see cref="RegisterFrame"/> have
    /// their session state saved and restored automatically as a part of the global
    /// <see cref="SessionState"/>.  Frames that are not registered have transient state
    /// that can still be useful when restoring pages that have been discarded from the
    /// navigation cache.
    /// </summary>
    /// <remarks>Apps may choose to rely on <see cref="NavigationHelper"/> to manage
    /// page-specific state instead of working with frame session state directly.</remarks>
    /// <param name="frame">The instance for which session state is desired.</param>
    /// <returns>A collection of state subject to the same serialization mechanism as
    /// <see cref="SessionState"/>.</returns>
    IMap<String^, Object^>^ SuspensionManager::SessionStateForFrame(Frame^ frame)
    {
        auto frameState = safe_cast<IMap<String^, Object^>^>(frame->GetValue(FrameSessionStateProperty));
    
        if (frameState == nullptr)
        {
            auto frameSessionKey = safe_cast<String^>(frame->GetValue(FrameSessionStateKeyProperty));
            if (frameSessionKey != nullptr)
            {
                // Registered frames reflect the corresponding session state
                if (!_sessionState->HasKey(frameSessionKey))
                {
                    _sessionState->Insert(frameSessionKey, ref new Map<String^, Object^>());
                }
                frameState = safe_cast<IMap<String^, Object^>^>(_sessionState->Lookup(frameSessionKey));
            }
            else
            {
                // Frames that aren't registered have transient state
                frameState = ref new Map<String^, Object^>();
            }
            frame->SetValue(FrameSessionStateProperty, frameState);
        }
        return frameState;
    }
    
    void SuspensionManager::RestoreFrameNavigationState(Frame^ frame)
    {
        auto frameState = SessionStateForFrame(frame);
        if (frameState->HasKey("Navigation"))
        {
            frame->SetNavigationState(safe_cast<String^>(frameState->Lookup("Navigation")));
        }
    }
    
    void SuspensionManager::SaveFrameNavigationState(Frame^ frame)
    {
        auto frameState = SessionStateForFrame(frame);
        frameState->Insert("Navigation", frame->GetNavigationState());
    }
    
    /// <summary>
    /// Save the current <see cref="SessionState"/>.  Any <see cref="Frame"/> instances
    /// registered with <see cref="RegisterFrame"/> will also preserve their current
    /// navigation stack, which in turn gives their active <see cref="Page"/> an opportunity
    /// to save its state.
    /// </summary>
    /// <returns>An asynchronous task that reflects when session state has been saved.</returns>
    task<void> SuspensionManager::SaveAsync(void)
    {
        // Save the navigation state for all registered frames
        for (auto && weakFrame : _registeredFrames)
        {
            auto frame = weakFrame.Resolve<Frame>();
            if (frame != nullptr) SaveFrameNavigationState(frame);
        }
    
        // Serialize the session state synchronously to avoid asynchronous access to shared
        // state
        auto sessionData = ref new InMemoryRandomAccessStream();
        auto sessionDataWriter = ref new DataWriter(sessionData->GetOutputStreamAt(0));
        ObjectSerializeHelper::WriteObject(sessionDataWriter, _sessionState);
    
        // Once session state has been captured synchronously, begin the asynchronous process
        // of writing the result to disk
        return task<unsigned int>(sessionDataWriter->StoreAsync()).then([=](unsigned int)
        {
            return ApplicationData::Current->LocalFolder->CreateFileAsync(StringReference(sessionStateFilename),
                CreationCollisionOption::ReplaceExisting);
        })
            .then([=](StorageFile^ createdFile)
        {
            return createdFile->OpenAsync(FileAccessMode::ReadWrite);
        })
            .then([=](IRandomAccessStream^ newStream)
        {
            return RandomAccessStream::CopyAsync(
                sessionData->GetInputStreamAt(0), newStream->GetOutputStreamAt(0));
        })
            .then([=](UINT64 copiedBytes)
        {
            (void) copiedBytes; // Unused parameter
            return;
        });
    }
    
    /// <summary>
    /// Restores previously saved <see cref="SessionState"/>.  Any <see cref="Frame"/> instances
    /// registered with <see cref="RegisterFrame"/> will also restore their prior navigation
    /// state, which in turn gives their active <see cref="Page"/> an opportunity restore its
    /// state.
    /// </summary>
    /// <param name="sessionBaseKey">An optional key that identifies the type of session.
    /// This can be used to distinguish between multiple application launch scenarios.</param>
    /// <returns>An asynchronous task that reflects when session state has been read.  The
    /// content of <see cref="SessionState"/> should not be relied upon until this task
    /// completes.</returns>
    task<void> SuspensionManager::RestoreAsync(String^ sessionBaseKey)
    {
        _sessionState->Clear();
    
        task<StorageFile^> getFileTask(ApplicationData::Current->LocalFolder->GetFileAsync(StringReference(sessionStateFilename)));
        return getFileTask.then([=](StorageFile^ stateFile)
        {
            task<BasicProperties^> getBasicPropertiesTask(stateFile->GetBasicPropertiesAsync());
            return getBasicPropertiesTask.then([=](BasicProperties^ stateFileProperties)
            {
                auto size = unsigned int(stateFileProperties->Size);
                if (size != stateFileProperties->Size) throw ref new FailureException("Session state larger than 4GB");
                task<IRandomAccessStreamWithContentType^> openReadTask(stateFile->OpenReadAsync());
                return openReadTask.then([=](IRandomAccessStreamWithContentType^ stateFileStream)
                {
                    auto stateReader = ref new DataReader(stateFileStream);
                    return task<unsigned int>(stateReader->LoadAsync(size)).then([=](unsigned int bytesRead)
                    {
                        (void) bytesRead; // Unused parameter
                        // Deserialize the Session State
                        Object^ content = ObjectSerializeHelper::ReadObject(stateReader);
                        _sessionState = (Map<String^, Object^>^)content;
    
                        // Restore any registered frames to their saved state
                        for (auto && weakFrame : _registeredFrames)
                        {
                            auto frame = weakFrame.Resolve<Frame>();
                            if (frame != nullptr && safe_cast<String^>(frame->GetValue(FrameSessionBaseKeyProperty)) == sessionBaseKey)
                            {
                                frame->ClearValue(FrameSessionStateProperty);
                                RestoreFrameNavigationState(frame);
                            }
                        }
                    }, task_continuation_context::use_current());
                });
            });
        });
    }
    
    #pragma region Object serialization for a known set of types
    
    void ObjectSerializeHelper::WriteString(DataWriter^ writer, String^ string)
    {
        writer->WriteByte(StringType);
        writer->WriteUInt32(writer->MeasureString(string));
        writer->WriteString(string);
    }
    
    void ObjectSerializeHelper::WriteProperty(DataWriter^ writer, IPropertyValue^ propertyValue)
    {
        switch (propertyValue->Type)
        {
        case PropertyType::UInt8:
            writer->WriteByte(StreamTypes::UInt8Type);
            writer->WriteByte(propertyValue->GetUInt8());
            return;
        case PropertyType::UInt16:
            writer->WriteByte(StreamTypes::UInt16Type);
            writer->WriteUInt16(propertyValue->GetUInt16());
            return;
        case PropertyType::UInt32:
            writer->WriteByte(StreamTypes::UInt32Type);
            writer->WriteUInt32(propertyValue->GetUInt32());
            return;
        case PropertyType::UInt64:
            writer->WriteByte(StreamTypes::UInt64Type);
            writer->WriteUInt64(propertyValue->GetUInt64());
            return;
        case PropertyType::Int16:
            writer->WriteByte(StreamTypes::Int16Type);
            writer->WriteUInt16(propertyValue->GetInt16());
            return;
        case PropertyType::Int32:
            writer->WriteByte(StreamTypes::Int32Type);
            writer->WriteUInt32(propertyValue->GetInt32());
            return;
        case PropertyType::Int64:
            writer->WriteByte(StreamTypes::Int64Type);
            writer->WriteUInt64(propertyValue->GetInt64());
            return;
        case PropertyType::Single:
            writer->WriteByte(StreamTypes::SingleType);
            writer->WriteSingle(propertyValue->GetSingle());
            return;
        case PropertyType::Double:
            writer->WriteByte(StreamTypes::DoubleType);
            writer->WriteDouble(propertyValue->GetDouble());
            return;
        case PropertyType::Boolean:
            writer->WriteByte(StreamTypes::BooleanType);
            writer->WriteBoolean(propertyValue->GetBoolean());
            return;
        case PropertyType::Char16:
            writer->WriteByte(StreamTypes::Char16Type);
            writer->WriteUInt16(propertyValue->GetChar16());
            return;
        case PropertyType::Guid:
            writer->WriteByte(StreamTypes::GuidType);
            writer->WriteGuid(propertyValue->GetGuid());
            return;
        case PropertyType::String:
            WriteString(writer, propertyValue->GetString());
            return;
        default:
            throw ref new InvalidArgumentException("Unsupported property type");
        }
    }
    
    void ObjectSerializeHelper::WriteStringToObjectMap(DataWriter^ writer, IMap<String^, Object^>^ map)
    {
        writer->WriteByte(StringToObjectMapType);
        writer->WriteUInt32(map->Size);
        for (auto && pair : map)
        {
            WriteObject(writer, pair->Key);
            WriteObject(writer, pair->Value);
        }
        writer->WriteByte(MapEndMarker);
    }
    
    void ObjectSerializeHelper::WriteObject(DataWriter^ writer, Object^ object)
    {
        if (object == nullptr)
        {
            writer->WriteByte(NullPtrType);
            return;
        }
    
        auto propertyObject = dynamic_cast<IPropertyValue^>(object);
        if (propertyObject != nullptr)
        {
            WriteProperty(writer, propertyObject);
            return;
        }
    
        auto mapObject = dynamic_cast<IMap<String^, Object^>^>(object);
        if (mapObject != nullptr)
        {
            WriteStringToObjectMap(writer, mapObject);
            return;
        }
    
        throw ref new InvalidArgumentException("Unsupported data type");
    }
    
    String^ ObjectSerializeHelper::ReadString(DataReader^ reader)
    {
        int length = reader->ReadUInt32();
        String^ string = reader->ReadString(length);
        return string;
    }
    
    IMap<String^, Object^>^ ObjectSerializeHelper::ReadStringToObjectMap(DataReader^ reader)
    {
        auto map = ref new Map<String^, Object^>();
        auto size = reader->ReadUInt32();
        for (unsigned int index = 0; index < size; index++)
        {
            auto key = safe_cast<String^>(ReadObject(reader));
            auto value = ReadObject(reader);
            map->Insert(key, value);
        }
        if (reader->ReadByte() != StreamTypes::MapEndMarker)
        {
            throw ref new InvalidArgumentException("Invalid stream");
        }
        return map;
    }
    
    Object^ ObjectSerializeHelper::ReadObject(DataReader^ reader)
    {
        auto type = reader->ReadByte();
        switch (type)
        {
        case StreamTypes::NullPtrType:
            return nullptr;
        case StreamTypes::UInt8Type:
            return reader->ReadByte();
        case StreamTypes::UInt16Type:
            return reader->ReadUInt16();
        case StreamTypes::UInt32Type:
            return reader->ReadUInt32();
        case StreamTypes::UInt64Type:
            return reader->ReadUInt64();
        case StreamTypes::Int16Type:
            return reader->ReadInt16();
        case StreamTypes::Int32Type:
            return reader->ReadInt32();
        case StreamTypes::Int64Type:
            return reader->ReadInt64();
        case StreamTypes::SingleType:
            return reader->ReadSingle();
        case StreamTypes::DoubleType:
            return reader->ReadDouble();
        case StreamTypes::BooleanType:
            return reader->ReadBoolean();
        case StreamTypes::Char16Type:
            return (char16_t) reader->ReadUInt16();
        case StreamTypes::GuidType:
            return reader->ReadGuid();
        case StreamTypes::StringType:
            return ReadString(reader);
        case StreamTypes::StringToObjectMapType:
            return ReadStringToObjectMap(reader);
        default:
            throw ref new InvalidArgumentException("Unsupported property type");
        }
    }
    
    #pragma endregion
    
  3. В файле app.xaml.cpp добавьте указанную ниже директиву include.

    #include "Common\SuspensionManager.h"
    
  4. Добавьте директиву namespace.

    using namespace SimpleBlogReader::Common;
    
  5. Теперь замените существующую функцию указанным ниже кодом.

    void App::OnLaunched(LaunchActivatedEventArgs^ e)
    {
    
    #if _DEBUG
        if (IsDebuggerPresent())
        {
            DebugSettings->EnableFrameRateCounter = true;
        }
    #endif
    
        auto rootFrame = dynamic_cast<Frame^>(Window::Current->Content);
    
        // Do not repeat app initialization when the Window already has content,
        // just ensure that the window is active.
        if (rootFrame == nullptr)
        {
            // Create a Frame to act as the navigation context and associate it with
            // a SuspensionManager key
            rootFrame = ref new Frame();
            SuspensionManager::RegisterFrame(rootFrame, "AppFrame");
    
            // Initialize the Atom and RSS feed objects with data from the web
            FeedDataSource^ feedDataSource = 
                safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource"));
            if (feedDataSource->Feeds->Size == 0)
            {
                if (e->PreviousExecutionState == ApplicationExecutionState::Terminated)
                {
                    // On resume FeedDataSource needs to know whether the app was on a
                    // specific FeedData, which will be the unless it was on MainPage
                    // when it was terminated.
                    ApplicationDataContainer^ localSettings = ApplicationData::Current->LocalSettings;
                    auto values = localSettings->Values;
                    if (localSettings->Values->HasKey("LastViewedFeed"))
                    {
                        feedDataSource->CurrentFeedUri = 
                            safe_cast<String^>(localSettings->Values->Lookup("LastViewedFeed"));
                    }
                }
    
                feedDataSource->InitDataSource();
            }
    
            // We have 4 pages in the app
            rootFrame->CacheSize = 4;
            auto prerequisite = task<void>([](){});
            if (e->PreviousExecutionState == ApplicationExecutionState::Terminated)
            {
                // Now restore the pages if we are resuming
                prerequisite = Common::SuspensionManager::RestoreAsync();
            }
    
            // if we're starting fresh, prerequisite will execute immediately.
            // if resuming from termination, prerequisite will wait until RestoreAsync() completes.
            prerequisite.then([=]()
            {
                if (rootFrame->Content == nullptr)
                {
                    if (!rootFrame->Navigate(MainPage::typeid, e->Arguments))
                    {
                        throw ref new FailureException("Failed to create initial page");
                    }
                }
                // Place the frame in the current Window
                Window::Current->Content = rootFrame;
                Window::Current->Activate();
            }, task_continuation_context::use_current());
        }
    
        // There is a frame, but is has no content, so navigate to main page
        // and activate the window.
        else if (rootFrame->Content == nullptr)
        {
    #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP
            // Removes the turnstile navigation for startup.
            if (rootFrame->ContentTransitions != nullptr)
            {
                _transitions = ref new TransitionCollection();
                for (auto transition : rootFrame->ContentTransitions)
                {
                    _transitions->Append(transition);
                }
            }
    
            rootFrame->ContentTransitions = nullptr;
            _firstNavigatedToken = rootFrame->Navigated += 
                ref new NavigatedEventHandler(this, &App::RootFrame_FirstNavigated);
    
    
    #endif
            // When the navigation stack isn't restored navigate to the first page,
            // configuring the new page by passing required information as a navigation
            // parameter.
            if (!rootFrame->Navigate(MainPage::typeid, e->Arguments))
            {
                throw ref new FailureException("Failed to create initial page");
            }
    
            // Ensure the current window is active in this code path.
            // we also called this inside the task for the other path.
            Window::Current->Activate();
        }
    }
    

    Обратите внимание, что класс App находится в общем проекте, поэтому код, который мы записываем здесь, будет выполняться и в приложении для Windows, и в приложении для телефона, за исключением случаев, когда определен макрос WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP.

  6. Обработчик событий OnSuspending более прост. Он вызывается, когда система завершает работу приложения, а не когда его закрывает пользователь. Мы просто разрешаем SuspensionManager выполнить свою работу. Он вызывает обработчик событий SaveState на каждой странице в приложении и сериализирует все объекты, которые мы сохранили в объекте PageState каждой страницы, а затем восстанавливает эти значения в страницы, когда приложение возобновляет работу. Просмотрите SuspensionManager.cpp, если вы хотите увидеть код.

    Замените существующее тело функции OnSuspending следующим кодом:

    void App::OnSuspending(Object^ sender, SuspendingEventArgs^ e)
    {
        (void)sender;   // Unused parameter
        (void)e;        // Unused parameter
    
        // Save application state and stop any background activity
        auto deferral = e->SuspendingOperation->GetDeferral();
        create_task(Common::SuspensionManager::SaveAsync())
            .then([deferral]()
        {
            deferral->Complete();
        });
    }
    

Теперь мы можем запустить приложение и скачать данные веб-канала, но у нас нет способа отобразить их для пользователя. Давайте что-то с этим сделаем!

Часть 7. Добавление первой страницы пользовательского интерфейса, списка каналов

При открытии приложения нам нужно показать пользователю коллекцию верхнего уровня всех загруженных каналов. Пользователь может щелкнуть ссылку или нажать элемент в коллекции, чтобы перейти к определенному каналу, который будет содержать коллекцию элементов веб-канала или записи. Мы уже добавили страницы. В приложении для Windows это страница элементов, которая отображает GridView, если устройство находится в горизонтальном положении, и ListView, если устройство находится в вертикальном положении. Проекты для телефона не содержат страницу элементов, поэтому к имеющейся основной странице мы вручную добавляем элемент ListView. Представление списка изменяется автоматически при изменении ориентации устройства.

На этой и всех остальных страницах приложения обычно находятся одни и те же основные задачи, которые необходимо выполнить.

  • Добавление разметки XAML, описывающей пользовательский интерфейс и привязки к данным
  • Добавьте пользовательский код в функции-члены LoadState и SaveState.
  • Обработка событий, хотя бы одно из которых обычно содержит код, выполняющий переход на следующую страницу

Мы будем делать это по порядку, сначала в проекте для Windows.

Добавление разметки XAML (MainPage)

Основная страница отображает каждый объект FeedData в элементе управления GridView. Чтобы описать, как должны выглядеть данные, мы создаем DataTemplate, являющийся деревом XAML, которое будет использоваться для отображения каждого элемента. Возможности для DataTemplates в отношении макетов, шрифтов, цветов и т д, ограничиваются только вашим собственным воображением и чувством стиля. На этой странице мы будем использовать простой шаблон, который при отображении выглядит следующим образом:

Элемент канала

  1. Стиль XAML подобен стилю в Microsoft Word. Это удобный способ группировки набора значений свойств элемента XAML "TargetType". Стиль может основываться на другом стиле. Атрибут "x:Key" определяет имя, используемое для ссылки на стиль, когда мы используем его.

    Поместите этот шаблон и его поддерживаемые стили в узел Page.Resources файла MainPage.xaml (Windows 8.1). Они используются только в MainPage.

    <Style x:Key="GridTitleTextStyle" TargetType="TextBlock" 
            BasedOn="{StaticResource BaseTextBlockStyle}">
        <Setter Property="FontSize" Value="26.667"/>
        <Setter Property="Margin" Value="12,0,12,2"/>
    </Style>
    
    <Style x:Key="GridDescriptionTextStyle" TargetType="TextBlock" 
            BasedOn="{StaticResource BaseTextBlockStyle}">
        <Setter Property="VerticalAlignment" Value="Bottom"/>
        <Setter Property="Margin" Value="12,0,12,60"/>
    </Style>
    
    <DataTemplate x:Key="DefaultGridItemTemplate">
        <Grid HorizontalAlignment="Left" Width="250" Height="250"
            Background="{StaticResource BlockBackgroundBrush}" >
            <StackPanel Margin="0,22,16,0">
                <TextBlock Text="{Binding Title}" 
                            Style="{StaticResource GridTitleTextStyle}" 
                            Margin="10,10,10,10"/>
                <TextBlock Text="{Binding Description}" 
                            Style="{StaticResource GridDescriptionTextStyle}"
                            Margin="10,10,10,10" />
            </StackPanel>
            <Border BorderBrush="DarkRed" BorderThickness="4" VerticalAlignment="Bottom">
                <StackPanel VerticalAlignment="Bottom" Orientation="Horizontal" 
                            Background="{StaticResource GreenBlockBackgroundBrush}">
                    <TextBlock Text="Last Updated" FontWeight="Bold" Margin="12,4,0,8" 
                                Height="42"/>
                    <TextBlock Text="{Binding PubDate, Converter={StaticResource dateConverter}}" 
                                FontWeight="ExtraBold" Margin="4,4,12,8" Height="42" Width="88"/>
                </StackPanel>
            </Border>
        </Grid>
    </DataTemplate>
    

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

  2. Все еще в файле MainPage.xaml (Windows 8.1) удаляем локальный для страницы элемент AppName, чтобы он не скрывал глобальный элемент, который мы собираемся добавить в область приложения.

  3. Добавьте CollectionViewSource к узлу Page.Resources. Этот объект подключает наш элемент ListView к модели данных:

    <!-- Collection of items displayed by this page -->
            <CollectionViewSource
            x:Name="itemsViewSource"
            Source="{Binding Items}"/>
    

    Обратите внимание, что элемент страницы уже имеет набор атрибутов DataContext к свойству DefaultViewModel для класса MainPage. Мы назначаем это свойство в качестве FeedDataSource, и поэтому CollectionViewSource ищет в нем коллекцию элементов, которую и находит.

  4. В файле App.xaml добавим строку глобального ресурса для имени приложения вместе с некоторыми дополнительными ресурсами, на которые будут ссылаться многие страницы в приложении. Вставка ресурсов здесь избавляет нас от необходимости указывать их отдельно на каждой странице. Добавьте указанные ниже элементы в узел Resources в файле App.xaml.

            <x:String x:Key="AppName">Simple Blog Reader</x:String>        
    
            <SolidColorBrush x:Key="WindowsBlogBackgroundBrush" Color="#FF0A2562"/>
            <SolidColorBrush x:Key="GreenBlockBackgroundBrush" Color="#FF6BBD46"/>
            <Style x:Key="WindowsBlogLayoutRootStyle" TargetType="Panel">
                <Setter Property="Background" 
                        Value="{StaticResource WindowsBlogBackgroundBrush}"/>
            </Style>
    
            <!-- Green square in all ListViews that displays the date -->
            <ControlTemplate x:Key="DateBlockTemplate">
                <Viewbox Stretch="Fill">
                    <Canvas Height="86" Width="86"  Margin="4,0,4,4" 
                     HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                        <TextBlock TextTrimming="WordEllipsis" 
                                   Padding="0,0,0,0"
                                   TextWrapping="NoWrap" 
                                   Width="Auto"
                                   Height="Auto" 
                                   FontSize="32" 
                                   FontWeight="Bold">
                            <TextBlock.Text>
                                <Binding Path="PubDate" 
                                         Converter="{StaticResource dateConverter}"
                                         ConverterParameter="month"/>
                            </TextBlock.Text>
                        </TextBlock>
    
                        <TextBlock TextTrimming="WordEllipsis" 
                                   TextWrapping="Wrap" 
                                   Width="Auto" 
                                   Height="Auto" 
                                   FontSize="32" 
                                   FontWeight="Bold" 
                                   Canvas.Top="36">
                            <TextBlock.Text>
                                <Binding Path="PubDate"  
                                         Converter="{StaticResource dateConverter}"
                                         ConverterParameter="day"/>
                            </TextBlock.Text>
                        </TextBlock>
    
                        <Line Stroke="White" 
                              StrokeThickness="2" X1="50" Y1="46" X2="50" Y2="80" />
    
                        <TextBlock TextWrapping="Wrap"  
                                   Height="Auto"  
                                   FontSize="18" 
                                   FontWeight="Bold"
                             FontStretch="Condensed"
                                   LineHeight="18"
                                   LineStackingStrategy="BaselineToBaseline"
                                   Canvas.Top="38" 
                                   Canvas.Left="56">
                            <TextBlock.Text>
                                <Binding Path="PubDate" 
                                         Converter="{StaticResource dateConverter}"
                                         ConverterParameter="year"  />
                            </TextBlock.Text>
                        </TextBlock>
                    </Canvas>
                </Viewbox>
            </ControlTemplate>
    
            <!-- Describes the layout for items in all ListViews -->
            <DataTemplate x:Name="ListItemTemplate">
                <Grid Margin="5,0,0,0">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="72"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition MaxHeight="54"></RowDefinition>
                    </Grid.RowDefinitions>
                    <!-- Green date block -->
                    <Border Background="{StaticResource GreenBlockBackgroundBrush}"
                            VerticalAlignment="Top">
                        <ContentControl Template="{StaticResource DateBlockTemplate}" />
                    </Border>
                    <TextBlock Grid.Column="1"
                               Text="{Binding Title}"
                               Margin="10,0,0,0" FontSize="20" 
                               TextWrapping="Wrap"
                               MaxHeight="72" 
                               Foreground="#FFFE5815" />
                </Grid>
            </DataTemplate>
    

MainPage отображает список каналов. Если устройство находится в альбомной ориентации, то мы будем использовать элемент управления GridView, поддерживающий горизонтальную прокрутку. В альбомной ориентации мы воспользуемся ListView, который поддерживает вертикальную прокрутку. Нам нужно, чтобы пользователи могли использовать приложение в любой ориентации. Реализовать поддержку изменений ориентаций относительно просто.

  • Добавьте оба элемента управления на страницу и укажите ItemSource тому же collectionViewSource. Установите свойство видимости в ListView на Collapsed (Свернуто), чтобы по умолчанию он не был видимым.
  • Создайте набор из двух объектов VisualState, один из которых описывает поведение пользовательского интерфейса для альбомной ориентации, а второй описывает поведение для книжной ориентации.
  • Обработайте событие Window::SizeChanged, которое запускается при изменении ориентации или в случае, если пользователь сужает или расширяет окно. Оцените высоту и ширину нового размера. Если высота больше ширины, вызывайте VisualState для книжной ориентации. В противном случае вызовите состояние для альбомной ориентации.

Hh465045.wedge(ru-ru,WIN.10).gifДобавление элементов управления GridView и ListView

  1. В файле MainPage.xaml добавьте элементы управления GridView и ListView, а также сетку, содержащую кнопку возврата и название страницы.

     <Grid Style="{StaticResource WindowsBlogLayoutRootStyle}">
            <Grid.ChildrenTransitions>
                <TransitionCollection>
                    <EntranceThemeTransition/>
                </TransitionCollection>
            </Grid.ChildrenTransitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="140"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
    
            <!-- Horizontal scrolling grid -->
            <GridView
                x:Name="ItemGridView"
                AutomationProperties.AutomationId="ItemsGridView"
                AutomationProperties.Name="Items"
                TabIndex="1"
                Grid.RowSpan="2"
                Padding="116,136,116,46"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                SelectionMode="None"
                ItemTemplate="{StaticResource DefaultGridItemTemplate}"
                IsItemClickEnabled="true"
                IsSwipeEnabled="false"
                ItemClick="ItemGridView_ItemClick"  Margin="0,-10,0,10">
            </GridView>
    
            <!-- Vertical scrolling list -->
            <ListView
                x:Name="ItemListView"
                Visibility="Collapsed"            
                AutomationProperties.AutomationId="ItemsListView"
                AutomationProperties.Name="Items"
                TabIndex="1" Grid.Row="1" Margin="-10,-10,0,0"      
                IsItemClickEnabled="True"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                IsSwipeEnabled="False"            
                ItemClick="ItemGridView_ItemClick"
                ItemTemplate="{StaticResource ListItemTemplate}">
    
                <ListView.ItemContainerStyle>
                    <Style TargetType="FrameworkElement">
                        <Setter Property="Margin" Value="2,0,0,2"/>
                    </Style>
                </ListView.ItemContainerStyle>
            </ListView>
    
            <!-- Back button and page title -->
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="120"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Button x:Name="backButton" Margin="39,59,39,0" 
                        Command="{Binding NavigationHelper.GoBackCommand, ElementName=pageRoot}"
                        Style="{StaticResource NavigationBackButtonNormalStyle}"
                        VerticalAlignment="Top"
                        AutomationProperties.Name="Back"
                        AutomationProperties.AutomationId="BackButton"
                        AutomationProperties.ItemType="Navigation Button"/>
                <TextBlock x:Name="pageTitle" Text="{StaticResource AppName}" 
                        Style="{StaticResource HeaderTextBlockStyle}" Grid.Column="1" 
                        IsHitTestVisible="false" TextWrapping="NoWrap" 
                        VerticalAlignment="Bottom" Margin="0,0,30,40"/>
            </Grid>
    
  2. Обратите внимание, что оба элемента управления используют одну и ту же функцию-член для события ItemClick. Поместите точку вставки на один из них и нажмите клавишу F12 для автоматического создания заглушки обработчика событий. Мы добавим код для ее позже.

  3. Вставьте определение VisualStateGroups так, чтобы оно было последним элементом в корневой сетке (не помещайте его вне сетки, потому что в этом случае оно не будет работать). Обратите внимание, что существует два состояния, но только одно из них определяется явно. Это происходит потому, что состояние DefaultLayout уже описано в XAML этой страницы).

    <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="ViewStates">
            <VisualState x:Name="DefaultLayout"/>
            <VisualState x:Name="Portrait">
                <Storyboard>
                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" 
                           Storyboard.TargetProperty="Visibility">
                    <DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
                </ObjectAnimationUsingKeyFrames>
                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemGridView" 
                           Storyboard.TargetProperty="Visibility">
                    <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
                </ObjectAnimationUsingKeyFrames>
               </Storyboard>
            </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
    
  4. Теперь пользовательский интерфейс полностью определен. Нам просто нужно указать странице ее действия при загрузке.

LoadState и SaveState (MainPage приложения для Windows)

Две основные функции-члена, на которые нам необходимо обратить внимание на любой странице XAML — это LoadState и (иногда) SaveState. В LoadState мы заполняем данные для страницы, а в SaveState мы сохраняем все данные, которые потребуются для повторного заполнения страницы в случае, если приложение будет приостановлено, а затем запущено снова.

  • Замените реализацию LoadState следующим кодом, который вставляет данные веб-канала, загруженные (или данные, которые продолжают загружаться) элементом feedDataSource, который мы создали при запуске, и вставляет данные в ViewModel для этой страницы.

    void MainPage::LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e)
    {     
        auto feedDataSource = safe_cast<FeedDataSource^>  
        (App::Current->Resources->Lookup("feedDataSource"));
        this->DefaultViewModel->Insert("Items", feedDataSource->Feeds);
    }
    

    Нам не требуется вызывать SaveState для MainPage, потому что нет данных, которые этой странице необходимо сохранить. Она всегда отображает все веб-каналы.

Обработчики событий (MainPage приложения для Windows)

Все страницы выполняются по сути внутри элемента Frame. Именно элемент Frame мы используем для перехода вперед и назад между страницами. Второй параметр в вызове функции навигации используется для передачи данных из одной страницы в другую. Все объекты, которые мы передаем сюда, автоматически сохраняются и сериализуются SuspensionManager, когда приложение приостанавливается, чтобы значения можно было восстановить после возобновления работы приложения. SuspensionManager по умолчанию поддерживает только встроенные типы String и Guid. Если вам нужна более сложная сериализация, вы можете создать пользовательский SuspensionManager. Здесь мы передаем тип String, который SplitPage будет использовать для определения текущего канала.

Hh465045.wedge(ru-ru,WIN.10).gifПереход по щелчкам на элементе

  1. Когда пользователь нажимает на элемент в сетке, обработчик событий получает элемент, который был нажат, указывает его как "текущий канал" в случае, если приложение приостанавливается на последующем этапе, а затем переходит на следующую страницу. Он передает заголовок канала на следующую страницу, чтобы эта страница могла найти данные для этого канала. Вот код, который необходимо вставить:

    void MainPage::ItemGridView_ItemClick(Object^ sender, ItemClickEventArgs^ e)
    {
        // We must manually cast from Object^ to FeedData^.
        auto feedData = safe_cast<FeedData^>(e->ClickedItem);
    
        // Store the feed and tell other pages it's loaded and ready to go.
        auto app = safe_cast<App^>(App::Current);
        app->SetCurrentFeed(feedData);
    
        // Only navigate if there are items in the feed
        if (feedData->Items->Size > 0)
        {
            // Navigate to SplitPage and pass the title of the selected feed.
            // SplitPage will receive this in its LoadState method in the 
            // navigationParamter.
            this->Frame->Navigate(SplitPage::typeid, feedData->Title);
        }
    }
    
  2. Чтобы скомпилировать предыдущий код, с помощью директивы #include необходимо включить файл SplitPage.xaml.h в верхней части текущего файла MainPage.xaml.cpp (Windows 8.1).

    #include "SplitPage.xaml.h"
    

Hh465045.wedge(ru-ru,WIN.10).gifОбработка события Page_SizeChanged

  • В файле MainPage.xaml добавьте имя в корневой элемент. Для этого добавьте x:Name="pageRoot" к атрибутам элемента корневой страницы, а затем добавьте атрибут SizeChanged="pageRoot_SizeChanged", чтобы создать обработчик событий. Замените реализацию обработчика в CPP-файле указанным ниже кодом.

    void MainPage::pageRoot_SizeChanged(Platform::Object^ sender, SizeChangedEventArgs^ e)
    {
        if (e->NewSize.Height / e->NewSize.Width >= 1)
        {
            VisualStateManager::GoToState(this, "Portrait", false);
        }
        else
        {
            VisualStateManager::GoToState(this, "DefaultLayout", false);
        }
    } 
    

    Затем добавьте объявление этой функции в класс MainPage в файле MainPage.xaml.h.

    private:
        void pageRoot_SizeChanged(Platform::Object^ sender, SizeChangedEventArgs^ e);
    

    Этот код очень простой. Если вы сейчас запустите приложение в симуляторе и повернете устройство, то увидите, как в пользовательском интерфейсе элемент управления GridView сменится элементом управления ListView.

Добавление XAML (MainPage приложения для телефона)

А теперь сделаем так, чтобы главная страница приложения для телефона заработала. Для этого потребуется намного меньше кода, так как мы будем использовать весь код, который мы помещаем в общий проект. Также, приложения для телефона не поддерживают элементы управления GridView, поскольку экраны телефонов слишком маленькие, для того, чтобы они полноценно работали. Поэтому мы воспользуемся ListView, который автоматически приспосабливается к альбомной ориентации и не требует изменений VisualState. Начнем с добавления атрибута DataContext к элементу страницы. Он не создается автоматически в основной странице телефона, как это происходит в ItemsPage или SplitPage.

  1. Для реализации перемещения по страницам необходимо, чтобы страницы включали NavigationHelper, который в свою очередь, зависит от RelayCommand. Добавьте новый элемент RelayCommand.h и скопируйте в него указанный ниже код.

    //
    // RelayCommand.h
    // Declaration of the RelayCommand and associated classes
    //
    
    #pragma once
    
    // <summary>
    // A command whose sole purpose is to relay its functionality 
    // to other objects by invoking delegates. 
    // The default return value for the CanExecute method is 'true'.
    // <see cref="RaiseCanExecuteChanged"/> needs to be called whenever
    // <see cref="CanExecute"/> is expected to return a different value.
    // </summary>
    
    
    namespace SimpleBlogReader
    {
        namespace Common
        {
            [Windows::Foundation::Metadata::WebHostHidden]
            public ref class RelayCommand sealed :[Windows::Foundation::Metadata::Default] Windows::UI::Xaml::Input::ICommand
            {
            public:
                virtual event Windows::Foundation::EventHandler<Object^>^ CanExecuteChanged;
                virtual bool CanExecute(Object^ parameter);
                virtual void Execute(Object^ parameter);
                virtual ~RelayCommand();
    
            internal:
                RelayCommand(std::function<bool(Platform::Object^)> canExecuteCallback,
                    std::function<void(Platform::Object^)> executeCallback);
                void RaiseCanExecuteChanged();
    
            private:
                std::function<bool(Platform::Object^)> _canExecuteCallback;
                std::function<void(Platform::Object^)> _executeCallback;
            };
        }
    }
    
  2. В папке Common добавьте файл RelayCommand.cpp и скопируйте в него указанный ниже код.

    //
    // RelayCommand.cpp
    // Implementation of the RelayCommand and associated classes
    //
    
    #include "pch.h"
    #include "RelayCommand.h"
    #include "NavigationHelper.h"
    
    using namespace SimpleBlogReader::Common;
    
    using namespace Platform;
    using namespace Platform::Collections;
    using namespace Windows::Foundation;
    using namespace Windows::Foundation::Collections;
    using namespace Windows::System;
    using namespace Windows::UI::Core;
    using namespace Windows::UI::ViewManagement;
    using namespace Windows::UI::Xaml;
    using namespace Windows::UI::Xaml::Controls;
    using namespace Windows::UI::Xaml::Input;
    using namespace Windows::UI::Xaml::Navigation;
    
    /// <summary>
    /// Determines whether this <see cref="RelayCommand"/> can execute in its current state.
    /// </summary>
    /// <param name="parameter">
    /// Data used by the command. If the command does not require data to be passed, this object can be set to null.
    /// </param>
    /// <returns>true if this command can be executed; otherwise, false.</returns>
    bool RelayCommand::CanExecute(Object^ parameter)
    {
        return (_canExecuteCallback) (parameter);
    }
    
    /// <summary>
    /// Executes the <see cref="RelayCommand"/> on the current command target.
    /// </summary>
    /// <param name="parameter">
    /// Data used by the command. If the command does not require data to be passed, this object can be set to null.
    /// </param>
    void RelayCommand::Execute(Object^ parameter)
    {
        (_executeCallback) (parameter);
    }
    
    /// <summary>
    /// Method used to raise the <see cref="CanExecuteChanged"/> event
    /// to indicate that the return value of the <see cref="CanExecute"/>
    /// method has changed.
    /// </summary>
    void RelayCommand::RaiseCanExecuteChanged()
    {
        CanExecuteChanged(this, nullptr);
    }
    
    /// <summary>
    /// RelayCommand Class Destructor.
    /// </summary>
    RelayCommand::~RelayCommand()
    {
        _canExecuteCallback = nullptr;
        _executeCallback = nullptr;
    };
    
    /// <summary>
    /// Creates a new command that can always execute.
    /// </summary>
    /// <param name="canExecuteCallback">The execution status logic.</param>
    /// <param name="executeCallback">The execution logic.</param>
    RelayCommand::RelayCommand(std::function<bool(Platform::Object^)> canExecuteCallback,
        std::function<void(Platform::Object^)> executeCallback) :
        _canExecuteCallback(canExecuteCallback),
        _executeCallback(executeCallback)
        {
        }
    
  3. В папке Common добавьте файл NavigationHelper.h и скопируйте в него указанный ниже код.

    //
    // NavigationHelper.h
    // Declaration of the NavigationHelper and associated classes
    //
    
    #pragma once
    
    #include "RelayCommand.h"
    
    namespace SimpleBlogReader
    {
        namespace Common
        {
            /// <summary>
            /// Class used to hold the event data required when a page attempts to load state.
            /// </summary>
            public ref class LoadStateEventArgs sealed
            {
            public:
    
                /// <summary>
                /// The parameter value passed to <see cref="Frame->Navigate(Type, Object)"/> 
                /// when this page was initially requested.
                /// </summary>
                property Platform::Object^ NavigationParameter
                {
                    Platform::Object^ get();
                }
    
                /// <summary>
                /// A dictionary of state preserved by this page during an earlier
                /// session.  This will be null the first time a page is visited.
                /// </summary>
                property Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ PageState
                {
                    Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ get();
                }
    
            internal:
                LoadStateEventArgs(Platform::Object^ navigationParameter, Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ pageState);
    
            private:
                Platform::Object^ _navigationParameter;
                Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ _pageState;
            };
    
            /// <summary>
            /// Represents the method that will handle the <see cref="NavigationHelper->LoadState"/>event
            /// </summary>
            public delegate void LoadStateEventHandler(Platform::Object^ sender, LoadStateEventArgs^ e);
    
            /// <summary>
            /// Class used to hold the event data required when a page attempts to save state.
            /// </summary>
            public ref class SaveStateEventArgs sealed
            {
            public:
    
                /// <summary>
                /// An empty dictionary to be populated with serializable state.
                /// </summary>
                property Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ PageState
                {
                    Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ get();
                }
    
            internal:
                SaveStateEventArgs(Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ pageState);
    
            private:
                Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ _pageState;
            };
    
            /// <summary>
            /// Represents the method that will handle the <see cref="NavigationHelper->SaveState"/>event
            /// </summary>
            public delegate void SaveStateEventHandler(Platform::Object^ sender, SaveStateEventArgs^ e);
    
            /// <summary>
            /// NavigationHelper aids in navigation between pages.  It provides commands used to 
            /// navigate back and forward as well as registers for standard mouse and keyboard 
            /// shortcuts used to go back and forward in Windows and the hardware back button in
            /// Windows Phone.  In addition it integrates SuspensionManger to handle process lifetime
            /// management and state management when navigating between pages.
            /// </summary>
            /// <example>
            /// To make use of NavigationHelper, follow these two steps or
            /// start with a BasicPage or any other Page item template other than BlankPage.
            /// 
            /// 1) Create an instance of the NavigationHelper somewhere such as in the 
            ///     constructor for the page and register a callback for the LoadState and 
            ///     SaveState events.
            /// <code>
            ///     MyPage::MyPage()
            ///     {
            ///         InitializeComponent();
            ///         auto navigationHelper = ref new Common::NavigationHelper(this);
            ///         navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &MyPage::LoadState);
            ///         navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &MyPage::SaveState);
            ///     }
            ///     
            ///     void MyPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e)
            ///     { }
            ///     void MyPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e)
            ///     { }
            /// </code>
            /// 
            /// 2) Register the page to call into the NavigationHelper whenever the page participates 
            ///     in navigation by overriding the <see cref="Windows::UI::Xaml::Controls::Page::OnNavigatedTo"/> 
            ///     and <see cref="Windows::UI::Xaml::Controls::Page::OnNavigatedFrom"/> events.
            /// <code>
            ///     void MyPage::OnNavigatedTo(NavigationEventArgs^ e)
            ///     {
            ///         NavigationHelper->OnNavigatedTo(e);
            ///     }
            ///
            ///     void MyPage::OnNavigatedFrom(NavigationEventArgs^ e)
            ///     {
            ///         NavigationHelper->OnNavigatedFrom(e);
            ///     }
            /// </code>
            /// </example>
            [Windows::Foundation::Metadata::WebHostHidden]
            [Windows::UI::Xaml::Data::Bindable]
            public ref class NavigationHelper sealed
            {
            public:
                /// <summary>
                /// <see cref="RelayCommand"/> used to bind to the back Button's Command property
                /// for navigating to the most recent item in back navigation history, if a Frame
                /// manages its own navigation history.
                /// 
                /// The <see cref="RelayCommand"/> is set up to use the virtual method <see cref="GoBack"/>
                /// as the Execute Action and <see cref="CanGoBack"/> for CanExecute.
                /// </summary>
                property RelayCommand^ GoBackCommand
                {
                    RelayCommand^ get();
                }
    
                /// <summary>
                /// <see cref="RelayCommand"/> used for navigating to the most recent item in 
                /// the forward navigation history, if a Frame manages its own navigation history.
                /// 
                /// The <see cref="RelayCommand"/> is set up to use the virtual method <see cref="GoForward"/>
                /// as the Execute Action and <see cref="CanGoForward"/> for CanExecute.
                /// </summary>
                property RelayCommand^ GoForwardCommand
                {
                    RelayCommand^ get();
                }
    
            internal:
                NavigationHelper(Windows::UI::Xaml::Controls::Page^ page,
                    RelayCommand^ goBack = nullptr,
                    RelayCommand^ goForward = nullptr);
    
                bool CanGoBack();
                void GoBack();
                bool CanGoForward();
                void GoForward();
    
                void OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e);
                void OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e);
    
                event LoadStateEventHandler^ LoadState;
                event SaveStateEventHandler^ SaveState;
    
            private:
                Platform::WeakReference _page;
    
                RelayCommand^ _goBackCommand;
                RelayCommand^ _goForwardCommand;
    
    #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP
                Windows::Foundation::EventRegistrationToken _backPressedEventToken;
    
                void HardwareButton_BackPressed(Platform::Object^ sender,
                    Windows::Phone::UI::Input::BackPressedEventArgs^ e);
    #else
                bool _navigationShortcutsRegistered;
                Windows::Foundation::EventRegistrationToken _acceleratorKeyEventToken;
                Windows::Foundation::EventRegistrationToken _pointerPressedEventToken;
    
                void CoreDispatcher_AcceleratorKeyActivated(Windows::UI::Core::CoreDispatcher^ sender,
                    Windows::UI::Core::AcceleratorKeyEventArgs^ e);
                void CoreWindow_PointerPressed(Windows::UI::Core::CoreWindow^ sender,
                    Windows::UI::Core::PointerEventArgs^ e);
    #endif
    
                Platform::String^ _pageKey;
                Windows::Foundation::EventRegistrationToken _loadedEventToken;
                Windows::Foundation::EventRegistrationToken _unloadedEventToken;
                void OnLoaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e);
                void OnUnloaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e);
    
                ~NavigationHelper();
            };
        }
    }
    
  4. Теперь в той же папке добавьте файл реализации NavigationHelper.cpp с указанным ниже кодом.

    //
    // NavigationHelper.cpp
    // Implementation of the NavigationHelper and associated classes
    //
    
    #include "pch.h"
    #include "NavigationHelper.h"
    #include "RelayCommand.h"
    #include "SuspensionManager.h"
    
    using namespace SimpleBlogReader::Common;
    
    using namespace Platform;
    using namespace Platform::Collections;
    using namespace Windows::Foundation;
    using namespace Windows::Foundation::Collections;
    using namespace Windows::System;
    using namespace Windows::UI::Core;
    using namespace Windows::UI::ViewManagement;
    using namespace Windows::UI::Xaml;
    using namespace Windows::UI::Xaml::Controls;
    using namespace Windows::UI::Xaml::Input;
    using namespace Windows::UI::Xaml::Interop;
    using namespace Windows::UI::Xaml::Navigation;
    
    #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP
    using namespace Windows::Phone::UI::Input;
    #endif
    
    /// <summary>
    /// Initializes a new instance of the <see cref="LoadStateEventArgs"/> class.
    /// </summary>
    /// <param name="navigationParameter">
    /// The parameter value passed to <see cref="Frame->Navigate(Type, Object)"/> 
    /// when this page was initially requested.
    /// </param>
    /// <param name="pageState">
    /// A dictionary of state preserved by this page during an earlier
    /// session.  This will be null the first time a page is visited.
    /// </param>
    LoadStateEventArgs::LoadStateEventArgs(Object^ navigationParameter, IMap<String^, Object^>^ pageState)
    {
        _navigationParameter = navigationParameter;
        _pageState = pageState;
    }
    
    /// <summary>
    /// Gets the <see cref="NavigationParameter"/> property of <see cref"LoadStateEventArgs"/> class.
    /// </summary>
    Object^ LoadStateEventArgs::NavigationParameter::get()
    {
        return _navigationParameter;
    }
    
    /// <summary>
    /// Gets the <see cref="PageState"/> property of <see cref"LoadStateEventArgs"/> class.
    /// </summary>
    IMap<String^, Object^>^ LoadStateEventArgs::PageState::get()
    {
        return _pageState;
    }
    
    /// <summary>
    /// Initializes a new instance of the <see cref="SaveStateEventArgs"/> class.
    /// </summary>
    /// <param name="pageState">An empty dictionary to be populated with serializable state.</param>
    SaveStateEventArgs::SaveStateEventArgs(IMap<String^, Object^>^ pageState)
    {
        _pageState = pageState;
    }
    
    /// <summary>
    /// Gets the <see cref="PageState"/> property of <see cref"SaveStateEventArgs"/> class.
    /// </summary>
    IMap<String^, Object^>^ SaveStateEventArgs::PageState::get()
    {
        return _pageState;
    }
    
    /// <summary>
    /// Initializes a new instance of the <see cref="NavigationHelper"/> class.
    /// </summary>
    /// <param name="page">A reference to the current page used for navigation.  
    /// This reference allows for frame manipulation and to ensure that keyboard 
    /// navigation requests only occur when the page is occupying the entire window.</param>
    NavigationHelper::NavigationHelper(Page^ page, RelayCommand^ goBack, RelayCommand^ goForward) :
    _page(page),
    _goBackCommand(goBack),
    _goForwardCommand(goForward)
    {
        // When this page is part of the visual tree make two changes:
        // 1) Map application view state to visual state for the page
        // 2) Handle hardware navigation requests
        _loadedEventToken = page->Loaded += ref new RoutedEventHandler(this, &NavigationHelper::OnLoaded);
    
        //// Undo the same changes when the page is no longer visible
        _unloadedEventToken = page->Unloaded += ref new RoutedEventHandler(this, &NavigationHelper::OnUnloaded);
    }
    
    NavigationHelper::~NavigationHelper()
    {
        delete _goBackCommand;
        delete _goForwardCommand;
    
        _page = nullptr;
    }
    
    /// <summary>
    /// Invoked when the page is part of the visual tree
    /// </summary>
    /// <param name="sender">Instance that triggered the event.</param>
    /// <param name="e">Event data describing the conditions that led to the event.</param>
    void NavigationHelper::OnLoaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e)
    {
    #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP
        _backPressedEventToken = HardwareButtons::BackPressed +=
            ref new EventHandler<BackPressedEventArgs^>(this,
            &NavigationHelper::HardwareButton_BackPressed);
    #else
        Page ^page = _page.Resolve<Page>();
    
        // Keyboard and mouse navigation only apply when occupying the entire window
        if (page != nullptr &&
            page->ActualHeight == Window::Current->Bounds.Height &&
            page->ActualWidth == Window::Current->Bounds.Width)
        {
            // Listen to the window directly so focus isn't required
            _acceleratorKeyEventToken = Window::Current->CoreWindow->Dispatcher->AcceleratorKeyActivated +=
                ref new TypedEventHandler<CoreDispatcher^, AcceleratorKeyEventArgs^>(this,
                &NavigationHelper::CoreDispatcher_AcceleratorKeyActivated);
    
            _pointerPressedEventToken = Window::Current->CoreWindow->PointerPressed +=
                ref new TypedEventHandler<CoreWindow^, PointerEventArgs^>(this,
                &NavigationHelper::CoreWindow_PointerPressed);
    
            _navigationShortcutsRegistered = true;
        }
    #endif
    }
    
    /// <summary>
    /// Invoked when the page is removed from visual tree
    /// </summary>
    /// <param name="sender">Instance that triggered the event.</param>
    /// <param name="e">Event data describing the conditions that led to the event.</param>
    void NavigationHelper::OnUnloaded(Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e)
    {
    #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP
        HardwareButtons::BackPressed -= _backPressedEventToken;
    #else
        if (_navigationShortcutsRegistered)
        {
            Window::Current->CoreWindow->Dispatcher->AcceleratorKeyActivated -= _acceleratorKeyEventToken;
            Window::Current->CoreWindow->PointerPressed -= _pointerPressedEventToken;
            _navigationShortcutsRegistered = false;
        }
    #endif
    
        // Remove handler and release the reference to page
        Page ^page = _page.Resolve<Page>();
        if (page != nullptr)
        {
            page->Loaded -= _loadedEventToken;
            page->Unloaded -= _unloadedEventToken;
            delete _goBackCommand;
            delete _goForwardCommand;
            _goForwardCommand = nullptr;
            _goBackCommand = nullptr;
        }
    }
    
    #pragma region Navigation support
    
    /// <summary>
    /// Method used by the <see cref="GoBackCommand"/> property
    /// to determine if the <see cref="Frame"/> can go back.
    /// </summary>
    /// <returns>
    /// true if the <see cref="Frame"/> has at least one entry 
    /// in the back navigation history.
    /// </returns>
    bool NavigationHelper::CanGoBack()
    {
        Page ^page = _page.Resolve<Page>();
        if (page != nullptr)
        {
            auto frame = page->Frame;
            return (frame != nullptr && frame->CanGoBack);
        }
    
        return false;
    }
    
    /// <summary>
    /// Method used by the <see cref="GoBackCommand"/> property
    /// to invoke the <see cref="Windows::UI::Xaml::Controls::Frame::GoBack"/> method.
    /// </summary>
    void NavigationHelper::GoBack()
    {
        Page ^page = _page.Resolve<Page>();
        if (page != nullptr)
        {
            auto frame = page->Frame;
            if (frame != nullptr && frame->CanGoBack)
            {
                frame->GoBack();
            }
        }
    }
    
    /// <summary>
    /// Method used by the <see cref="GoForwardCommand"/> property
    /// to determine if the <see cref="Frame"/> can go forward.
    /// </summary>
    /// <returns>
    /// true if the <see cref="Frame"/> has at least one entry 
    /// in the forward navigation history.
    /// </returns>
    bool NavigationHelper::CanGoForward()
    {
        Page ^page = _page.Resolve<Page>();
        if (page != nullptr)
        {
            auto frame = page->Frame;
            return (frame != nullptr && frame->CanGoForward);
        }
    
        return false;
    }
    
    /// <summary>
    /// Method used by the <see cref="GoForwardCommand"/> property
    /// to invoke the <see cref="Windows::UI::Xaml::Controls::Frame::GoBack"/> method.
    /// </summary>
    void NavigationHelper::GoForward()
    {
        Page ^page = _page.Resolve<Page>();
        if (page != nullptr)
        {
            auto frame = page->Frame;
            if (frame != nullptr && frame->CanGoForward)
            {
                frame->GoForward();
            }
        }
    }
    
    /// <summary>
    /// Gets the <see cref="GoBackCommand"/> property of <see cref"NavigationHelper"/> class.
    /// </summary>
    RelayCommand^ NavigationHelper::GoBackCommand::get()
    {
        if (_goBackCommand == nullptr)
        {
            _goBackCommand = ref new RelayCommand(
                [this](Object^) -> bool
            {
                return CanGoBack();
            },
                [this](Object^) -> void
            {
                GoBack();
            }
            );
        }
        return _goBackCommand;
    }
    
    /// <summary>
    /// Gets the <see cref="GoForwardCommand"/> property of <see cref"NavigationHelper"/> class.
    /// </summary>
    RelayCommand^ NavigationHelper::GoForwardCommand::get()
    {
        if (_goForwardCommand == nullptr)
        {
            _goForwardCommand = ref new RelayCommand(
                [this](Object^) -> bool
            {
                return CanGoForward();
            },
                [this](Object^) -> void
            {
                GoForward();
            }
            );
        }
        return _goForwardCommand;
    }
    
    #if WINAPI_FAMILY==WINAPI_FAMILY_PHONE_APP
    /// <summary>
    /// Handles the back button press and navigates through the history of the root frame.
    /// </summary>
    void NavigationHelper::HardwareButton_BackPressed(Object^ sender, BackPressedEventArgs^ e)
    {
        if (this->GoBackCommand->CanExecute(nullptr))
        {
            e->Handled = true;
            this->GoBackCommand->Execute(nullptr);
        }
    }
    #else
    /// <summary>
    /// Invoked on every keystroke, including system keys such as Alt key combinations, when
    /// this page is active and occupies the entire window.  Used to detect keyboard navigation
    /// between pages even when the page itself doesn't have focus.
    /// </summary>
    /// <param name="sender">Instance that triggered the event.</param>
    /// <param name="e">Event data describing the conditions that led to the event.</param>
    void NavigationHelper::CoreDispatcher_AcceleratorKeyActivated(CoreDispatcher^ sender,
        AcceleratorKeyEventArgs^ e)
    {
        sender; // Unused parameter
        auto virtualKey = e->VirtualKey;
    
        // Only investigate further when Left, Right, or the dedicated Previous or Next keys
        // are pressed
        if ((e->EventType == CoreAcceleratorKeyEventType::SystemKeyDown ||
            e->EventType == CoreAcceleratorKeyEventType::KeyDown) &&
            (virtualKey == VirtualKey::Left || virtualKey == VirtualKey::Right ||
            virtualKey == VirtualKey::GoBack || virtualKey == VirtualKey::GoForward))
        {
            auto coreWindow = Window::Current->CoreWindow;
            auto downState = Windows::UI::Core::CoreVirtualKeyStates::Down;
            bool menuKey = (coreWindow->GetKeyState(VirtualKey::Menu) & downState) == downState;
            bool controlKey = (coreWindow->GetKeyState(VirtualKey::Control) & downState) == downState;
            bool shiftKey = (coreWindow->GetKeyState(VirtualKey::Shift) & downState) == downState;
            bool noModifiers = !menuKey && !controlKey && !shiftKey;
            bool onlyAlt = menuKey && !controlKey && !shiftKey;
    
            if ((virtualKey == VirtualKey::GoBack && noModifiers) ||
                (virtualKey == VirtualKey::Left && onlyAlt))
            {
                // When the previous key or Alt+Left are pressed navigate back
                e->Handled = true;
                GoBackCommand->Execute(this);
            }
            else if ((virtualKey == VirtualKey::GoForward && noModifiers) ||
                (virtualKey == VirtualKey::Right && onlyAlt))
            {
                // When the next key or Alt+Right are pressed navigate forward
                e->Handled = true;
                GoForwardCommand->Execute(this);
            }
        }
    }
    
    /// <summary>
    /// Invoked on every mouse click, touch screen tap, or equivalent interaction when this
    /// page is active and occupies the entire window.  Used to detect browser-style next and
    /// previous mouse button clicks to navigate between pages.
    /// </summary>
    /// <param name="sender">Instance that triggered the event.</param>
    /// <param name="e">Event data describing the conditions that led to the event.</param>
    void NavigationHelper::CoreWindow_PointerPressed(CoreWindow^ sender, PointerEventArgs^ e)
    {
        auto properties = e->CurrentPoint->Properties;
    
        // Ignore button chords with the left, right, and middle buttons
        if (properties->IsLeftButtonPressed ||
            properties->IsRightButtonPressed ||
            properties->IsMiddleButtonPressed)
        {
            return;
        }
    
        // If back or foward are pressed (but not both) navigate appropriately
        bool backPressed = properties->IsXButton1Pressed;
        bool forwardPressed = properties->IsXButton2Pressed;
        if (backPressed ^ forwardPressed)
        {
            e->Handled = true;
            if (backPressed)
            {
                if (GoBackCommand->CanExecute(this))
                {
                    GoBackCommand->Execute(this);
                }
            }
            else
            {
                if (GoForwardCommand->CanExecute(this))
                {
                    GoForwardCommand->Execute(this);
                }
            }
        }
    }
    #endif
    
    #pragma endregion
    
    #pragma region Process lifetime management
    
    /// <summary>
    /// Invoked when this page is about to be displayed in a Frame.
    /// </summary>
    /// <param name="e">Event data that describes how this page was reached.  The Parameter
    /// property provides the group to be displayed.</param>
    void NavigationHelper::OnNavigatedTo(NavigationEventArgs^ e)
    {
        Page ^page = _page.Resolve<Page>();
        if (page != nullptr)
        {
            auto frameState = SuspensionManager::SessionStateForFrame(page->Frame);
            _pageKey = "Page-" + page->Frame->BackStackDepth;
    
            if (e->NavigationMode == NavigationMode::New)
            {
                // Clear existing state for forward navigation when adding a new page to the
                // navigation stack
                auto nextPageKey = _pageKey;
                int nextPageIndex = page->Frame->BackStackDepth;
                while (frameState->HasKey(nextPageKey))
                {
                    frameState->Remove(nextPageKey);
                    nextPageIndex++;
                    nextPageKey = "Page-" + nextPageIndex;
                }
    
                // Pass the navigation parameter to the new page
                LoadState(this, ref new LoadStateEventArgs(e->Parameter, nullptr));
            }
            else
            {
                // Pass the navigation parameter and preserved page state to the page, using
                // the same strategy for loading suspended state and recreating pages discarded
                // from cache
                LoadState(this, ref new LoadStateEventArgs(e->Parameter, safe_cast<IMap<String^, Object^>^>(frameState->Lookup(_pageKey))));
            }
        }
    }
    
    /// <summary>
    /// Invoked when this page will no longer be displayed in a Frame.
    /// </summary>
    /// <param name="e">Event data that describes how this page was reached.  The Parameter
    /// property provides the group to be displayed.</param>
    void NavigationHelper::OnNavigatedFrom(NavigationEventArgs^ e)
    {
        Page ^page = _page.Resolve<Page>();
        if (page != nullptr)
        {
            auto frameState = SuspensionManager::SessionStateForFrame(page->Frame);
            auto pageState = ref new Map<String^, Object^>();
            SaveState(this, ref new SaveStateEventArgs(pageState));
            frameState->Insert(_pageKey, pageState);
        }
    }
    #pragma endregion
    
  5. Теперь добавьте код для включения NavigationHelper в файл заголовков MainPage.xaml.h, а также свойство DefaultViewModel, которое понадобится нам позже.

    //
    // MainPage.xaml.h
    // Declaration of the MainPage class
    //
    
    #pragma once
    
    #include "MainPage.g.h"
    #include "Common\NavigationHelper.h"
    
    namespace SimpleBlogReader
    {
    
        namespace WFC = Windows::Foundation::Collections;
        namespace WUIX = Windows::UI::Xaml;
        namespace WUIXNav = Windows::UI::Xaml::Navigation;
        namespace WUIXControls = Windows::UI::Xaml::Controls;
    
        /// <summary>
        /// A basic page that provides characteristics common to most applications.
        /// </summary>
        [Windows::Foundation::Metadata::WebHostHidden]
        public ref class MainPage sealed
        {
        public:
            MainPage();
    
            /// <summary>
            /// Gets the view model for this <see cref="Page"/>. 
            /// This can be changed to a strongly typed view model.
            /// </summary>
            property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel
            {
                WFC::IObservableMap<Platform::String^, Platform::Object^>^  get();
            }
    
            /// <summary>
            /// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>.
            /// </summary>
            property Common::NavigationHelper^ NavigationHelper
            {
                Common::NavigationHelper^ get();
            }
    
        protected:
            virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override;
            virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override;
    
        private:
            void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e);
            void SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e);
    
            static WUIX::DependencyProperty^ _defaultViewModelProperty;
            static WUIX::DependencyProperty^ _navigationHelperProperty;
    
        };
    
    }
    
  6. В файле MainPage.xaml.cpp добавьте реализацию NavigationHelper и заглушки для загрузки и сохранения состояния и свойства DefaultViewModel. Кроме того, с помощью директив namespace вы добавите необходимые элементы, поэтому окончательный код будет выглядеть следующим образом:

    //
    // MainPage.xaml.cpp
    // Implementation of the MainPage class
    //
    
    #include "pch.h"
    #include "MainPage.xaml.h"
    
    using namespace SimpleBlogReader;
    using namespace Platform;
    using namespace Platform::Collections;
    using namespace Windows::Foundation;
    using namespace Windows::Foundation::Collections;
    using namespace Windows::UI::Xaml;
    using namespace Windows::UI::Xaml::Controls;
    using namespace Windows::UI::Xaml::Controls::Primitives;
    using namespace Windows::UI::Xaml::Data;
    using namespace Windows::UI::Xaml::Input;
    using namespace Windows::UI::Xaml::Media;
    using namespace Windows::UI::Xaml::Navigation;
    using namespace Windows::UI::Xaml::Interop;
    
    // The Basic Page item template is documented at https://go.microsoft.com/fwlink/?LinkID=390556
    
    MainPage::MainPage()
    {
        InitializeComponent();
        SetValue(_defaultViewModelProperty, 
            ref new Platform::Collections::Map<String^, Object^>(std::less<String^>()));
        auto navigationHelper = ref new Common::NavigationHelper(this);
        SetValue(_navigationHelperProperty, navigationHelper);
        navigationHelper->LoadState += 
            ref new Common::LoadStateEventHandler(this, &MainPage::LoadState);
        navigationHelper->SaveState += 
            ref new Common::SaveStateEventHandler(this, &MainPage::SaveState);
    }
    
    DependencyProperty^ MainPage::_defaultViewModelProperty =
    DependencyProperty::Register("DefaultViewModel",
    TypeName(IObservableMap<String^, Object^>::typeid), TypeName(MainPage::typeid), nullptr);
    
    /// <summary>
    /// Used as a trivial view model.
    /// </summary>
    IObservableMap<String^, Object^>^ MainPage::DefaultViewModel::get()
    {
        return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty));
    }
    
    DependencyProperty^ MainPage::_navigationHelperProperty =
    DependencyProperty::Register("NavigationHelper",
    TypeName(Common::NavigationHelper::typeid), TypeName(MainPage::typeid), nullptr);
    
    /// <summary>
    /// Gets an implementation of <see cref="NavigationHelper"/> designed to be
    /// used as a trivial view model.
    /// </summary>
    Common::NavigationHelper^ MainPage::NavigationHelper::get()
    {
        return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty));
    }
    
    #pragma region Navigation support
    
    /// The methods provided in this section are simply used to allow
    /// NavigationHelper to respond to the page's navigation methods.
    /// 
    /// Page specific logic should be placed in event handlers for the  
    /// <see cref="NavigationHelper::LoadState"/>
    /// and <see cref="NavigationHelper::SaveState"/>.
    /// The navigation parameter is available in the LoadState method 
    /// in addition to page state preserved during an earlier session.
    
    void MainPage::OnNavigatedTo(NavigationEventArgs^ e)
    {
        NavigationHelper->OnNavigatedTo(e);
    }
    
    void MainPage::OnNavigatedFrom(NavigationEventArgs^ e)
    {
        NavigationHelper->OnNavigatedFrom(e);
    }
    
    #pragma endregion
    
    /// <summary>
    /// Populates the page with content passed during navigation. Any saved state is also
    /// provided when recreating a page from a prior session.
    /// </summary>
    /// <param name="sender">
    /// The source of the event; typically <see cref="NavigationHelper"/>
    /// </param>
    /// <param name="e">Event data that provides both the navigation parameter passed to
    /// <see cref="Frame::Navigate(Type, Object)"/> when this page was initially requested and
    /// a dictionary of state preserved by this page during an earlier
    /// session. The state will be null the first time a page is visited.</param>
    void MainPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e)
    {
     (void) sender; // Unused parameter
        (void) e; // Unused parameter
    }
    
    /// <summary>
    /// Preserves state associated with this page in case the application is suspended or the
    /// page is discarded from the navigation cache.  Values must conform to the serialization
    /// requirements of <see cref="SuspensionManager::SessionState"/>.
    /// </summary>
    /// <param name="sender">The source of the event; typically <see cref="NavigationHelper"/></param>
    /// <param name="e">Event data that provides an empty dictionary to be populated with
    /// serializable state.</param>
    void MainPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e)
    {
        (void) sender;  // Unused parameter
        (void) e; // Unused parameter
    }
    
  7. Оставаясь в файле MainPage.xaml (Windows Phone 8.1), переместитесь вниз по странице, найдите комментарий "Панель названия" и удалите всю панель StackPanel. В телефоне нам необходимо доступное пространство на экране, чтобы отобразить список каналов блогов.

  8. Продвигаясь вниз по странице, вы увидите сетку со следующим комментарием: "TODO: Content should be placed within the following grid". Поместите этот ListView в данную сетку.

        <!-- Vertical scrolling item list -->
         <ListView
            x:Name="itemListView"           
            AutomationProperties.AutomationId="itemListView"
            AutomationProperties.Name="Items"
            TabIndex="1" 
            IsItemClickEnabled="True"
            ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
            IsSwipeEnabled="False"
            ItemClick="ItemListView_ItemClick"
            SelectionMode="Single"
            ItemTemplate="{StaticResource ListItemTemplate}">
    
            <ListView.ItemContainerStyle>
                <Style TargetType="FrameworkElement">
                    <Setter Property="Margin" Value="2,0,0,2"/>
                </Style>
            </ListView.ItemContainerStyle>
        </ListView>
    
  9. Теперь поместите курсор над событием ItemListView_ItemClick и нажмите F12 (Перейти к определению). Visual Studio создаст для нас пустую функцию обработчика событий. Позже мы добавим в нее некоторый код. На данном этапе, чтобы скомпилировать приложение, нам достаточно создать функцию.

Часть 8. Создание списка записей и отображение текстового представления выбранной записи

В этой части мы добавим две страницы в приложение для телефона: страницу, которая выводит список записей и страницу, на которой показана текстовая версия выбранной записи. В приложении для Windows необходимо просто добавить одну страницу с именем SplitPage, которая будет отображать список на одной стороне и текст выбранной записи на другой стороне. Сначала сделаем это для страниц телефона.

Добавьте разметку XAML (FeedPage приложения для телефона)

Давайте останемся в проекте для телефона и поработаем над FeedPage, которая создает список записей канала, выбранного пользователем.

Hh465045.wedge(ru-ru,WIN.10).gif

  1. В файле FeedPage.xaml (Windows Phone 8.1) добавьте контекст данных к элементу страницы:

    DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
    
  2. Теперь добавьте CollectionViewSource после открывающего элемента Page.

    <Page.Resources>
        <!-- Collection of items displayed by this page -->
        <CollectionViewSource
        x:Name="itemsViewSource"
        Source="{Binding Items}"/>
    </Page.Resources>
    
  3. В элементе Grid добавьте панель StackPanel.

            <!-- TitlePanel -->
            <StackPanel Grid.Row="0" Margin="24,17,0,28">
                <TextBlock Text="{StaticResource AppName}" 
                           Style="{ThemeResource TitleTextBlockStyle}" 
                           Typography.Capitals="SmallCaps"/>
            </StackPanel>
    
  4. Затем добавьте ListView в сетку (сразу после открывающего элемента).

                <!-- Vertical scrolling item list -->
                <ListView
                x:Name="itemListView"
                AutomationProperties.AutomationId="ItemsListView"
                AutomationProperties.Name="Items"
                TabIndex="1"
                Grid.Row="1"
                Margin="-10,-10,0,0" 
                IsItemClickEnabled="True"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                IsSwipeEnabled="False"
                ItemClick="ItemListView_ItemClick"
                ItemTemplate="{StaticResource ListItemTemplate}">
    
                    <ListView.ItemContainerStyle>
                        <Style TargetType="FrameworkElement">
                            <Setter Property="Margin" Value="2,0,0,2"/>
                        </Style>
                    </ListView.ItemContainerStyle>
                </ListView>
    

    Обратите внимание, что свойство ItemsSource элемента управления ListView привязывается к свойству CollectionViewSource, которое, в свою очередь, привязывается к нашему свойству FeedData::Items, которое мы вставляем в свойство DefaultViewModel в LoadState в коде программной части (см. ниже).

  5. В ListView есть заявленное событие ItemClick. Установите курсор над ним и нажмите клавишу F12 для создания обработчика событий в коде программной части. Пока оставим его пустым.

LoadState и SaveState (FeedPage приложения для телефона)

На MainPage нам не нужно беспокоиться о сохранении состояния, поскольку страница всегда выполняет полную повторную инициализацию из интернета при запуске приложения по любой причине. Другим страницам необходимо запоминать свое состояние. Например, если приложение было завершено (выгружено из памяти) во время отображения FeedPage, если пользователь переходит к ней обратно, нам нужно, чтобы она отображалась так, как будто приложение никогда не закрывалось. Поэтому необходимо запомнить какой канал выбран. Область для хранения этих данных находится в локальном хранилище AppData и самое время сохранить их, когда пользователь нажимает на них на MainPage.

В этом есть только одна сложность — существуют ли эти данные фактически или еще нет? Если мы переходим на FeedPage с элемента MainPage после щелчка мышью, мы знаем точно, что выбранный объект FeedData уже существует, потому что иначе он не появится в списке MainPage. Однако, если приложение возобновляет работу, последний просмотренный объект FeedData может еще не быть загружен, когда FeedPage пытается привязаться к нему. Странице FeedPage (и другим страницам) нужен способ узнавать, когда FeedData доступна. Класс concurrency::task_completion_event предназначен как раз для такой ситуации. С его помощью мы можем безопасно получить объект FeedData в том же пути кода независимо от того, возобновляем ли мы работу или первый раз переходим с элемента MainPage. Из FeedPage мы всегда можем получить наш канал, вызвав GetCurrentFeedAsync. Если мы переходим с элемента MainPage, то событие уже было задано, когда пользователь щелкнул канал, поэтому метод возвратит канал немедленно. Если мы возобновляем работу приложения после приостановки, данное событие указывается в функции FeedDataSource::InitDataSource, и в этом случае FeedPage, возможно придется подождать перезагрузки канала. В данном случае ожидание лучше, чем аварийное завершение работы. Именно поэтому в файлах FeedData.cpp и App.xaml.cpp появилось большое количество сложного асинхронного кода. Тем не менее если вы внимательнее посмотрите на этот код, вы увидите, что он не настолько сложен, как может показаться.

  1. В файле FeedPage.xaml.cpp добавьте указанное ниже пространство имен, чтобы внести объекты задачи в область.

    using namespace concurrency;
    
  2. Кроме того, добавьте директиву #include для файла TextViewerPage.xaml.h.

    #include "TextViewerPage.xaml.h"
    

    В обращении к действию "Переход" требуется определение класса TextViewerPage, показанное ниже.

  3. Замените метод LoadState следующим кодом:

    void FeedPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e)
    {
        (void)sender;   // Unused parameter
    
        if (!this->DefaultViewModel->HasKey("Feed"))
        {
            auto app = safe_cast<App^>(App::Current);
            app->GetCurrentFeedAsync().then([this, e](FeedData^ fd)
            {
                // Insert into the ViewModel for this page to
                // initialize itemsViewSource->View
                this->DefaultViewModel->Insert("Feed", fd);
                this->DefaultViewModel->Insert("Items", fd->Items);
            }, task_continuation_context::use_current());
        }
    }
    

    Если мы переходим обратно к FeedPage со страницы, находящейся выше в стеке страниц, страница уже будет инициализирована (напр. DefaultViewModel будет иметь значение для элемента "Канал") и текущий канал уже будет настроен правильно. Но если мы переходим вперед с элемента MainPage, или возобновляем работу приложения, нам потребуется получить текущий канал, чтобы заполнить страницу правильными данными. Функция GetCurrentFeedAsync, при необходимости, подождет, пока не будут получены данные веб-канала после возобновления работы приложения. Мы задаем контекст use_current(), чтобы сообщить задачу вернуться в поток пользовательского интерфейса до попытки получить доступ к свойству зависимостей DefaultViewModel. К связанным с XAML объектам обычно нельзя получить доступ напрямую из фоновых потоков.

    Мы ничего не делаем с SaveState на этой странице, так как мы получаем состояние из метода GetCurrentFeedAsync при каждой загрузке страницы.

  4. Добавьте объявление LoadState в файле заголовков FeedPage.xaml.h, директиву include для файла Common\NavigationHelper.h и свойства NavigationHelper и DefaultViewModel.

    //
    // FeedPage.xaml.h
    // Declaration of the FeedPage class
    //
    
    #pragma once
    
    #include "FeedPage.g.h"
    #include "Common\NavigationHelper.h"
    
    namespace SimpleBlogReader
    {
    
        namespace WFC = Windows::Foundation::Collections;
        namespace WUIX = Windows::UI::Xaml;
        namespace WUIXNav = Windows::UI::Xaml::Navigation;
        namespace WUIXControls = Windows::UI::Xaml::Controls;
    
                    /// <summary>
                    /// A basic page that provides characteristics common to most applications.
                    /// </summary>
                    [Windows::Foundation::Metadata::WebHostHidden]
                    public ref class FeedPage sealed
                    {
                    public:
                                    FeedPage();
    
                                    /// <summary>
                                    /// Gets the view model for this <see cref="Page"/>. 
                                    /// This can be changed to a strongly typed view model.
                                    /// </summary>
                                    property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel
                                    {
                                                    WFC::IObservableMap<Platform::String^, Platform::Object^>^  get();
                                    }
    
                                    /// <summary>
                                    /// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>.
                                    /// </summary>
                                    property Common::NavigationHelper^ NavigationHelper
                                    {
                                                    Common::NavigationHelper^ get();
                                    }
    
                    protected:
                                    virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override;
                                    virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override;
    
                    private:
                                    void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e);
                                    void SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e);
    
                                    static Windows::UI::Xaml::DependencyProperty^ _defaultViewModelProperty;
                                    static Windows::UI::Xaml::DependencyProperty^ _navigationHelperProperty;
            void ItemListView_ItemClick(Platform::Object^ sender, WUIXControls::ItemClickEventArgs^ e);
        };
    
    }
    
  5. Добавьте реализацию этих свойств в файле FeedPage.xaml.cpp, который теперь выглядит указанным ниже образом.

    //
    // FeedPage.xaml.cpp
    // Implementation of the FeedPage class
    //
    
    #include "pch.h"
    #include "FeedPage.xaml.h"
    #include "TextViewerPage.xaml.h"
    
    using namespace SimpleBlogReader;
    using namespace concurrency;
    
    using namespace Platform;
    using namespace Platform::Collections;
    using namespace Windows::Foundation;
    using namespace Windows::Foundation::Collections;
    using namespace Windows::Graphics::Display;
    using namespace Windows::UI::Xaml;
    using namespace Windows::UI::Xaml::Controls;
    using namespace Windows::UI::Xaml::Controls::Primitives;
    using namespace Windows::UI::Xaml::Data;
    using namespace Windows::UI::Xaml::Input;
    using namespace Windows::UI::Xaml::Interop;
    using namespace Windows::UI::Xaml::Media;
    using namespace Windows::UI::Xaml::Navigation;
    
    // The Basic Page item template is documented at https://go.microsoft.com/fwlink/?LinkID=390556
    
    FeedPage::FeedPage()
    {
        InitializeComponent();
        SetValue(_defaultViewModelProperty, 
            ref new Platform::Collections::Map<String^, Object^>(std::less<String^>()));
        auto navigationHelper = ref new Common::NavigationHelper(this);
        SetValue(_navigationHelperProperty, navigationHelper);
        navigationHelper->LoadState += 
            ref new Common::LoadStateEventHandler(this, &FeedPage::LoadState);
        navigationHelper->SaveState += 
            ref new Common::SaveStateEventHandler(this, &FeedPage::SaveState);
    }
    
    DependencyProperty^ FeedPage::_defaultViewModelProperty =
    DependencyProperty::Register("DefaultViewModel",
    TypeName(IObservableMap<String^, Object^>::typeid), TypeName(FeedPage::typeid), nullptr);
    
    /// <summary>
    /// Used as a trivial view model.
    /// </summary>
    IObservableMap<String^, Object^>^ FeedPage::DefaultViewModel::get()
    {
        return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty));
    }
    
    DependencyProperty^ FeedPage::_navigationHelperProperty =
    DependencyProperty::Register("NavigationHelper",
    TypeName(Common::NavigationHelper::typeid), TypeName(FeedPage::typeid), nullptr);
    
    /// <summary>
    /// Gets an implementation of <see cref="NavigationHelper"/> designed to be
    /// used as a trivial view model.
    /// </summary>
    Common::NavigationHelper^ FeedPage::NavigationHelper::get()
    {
        return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty));
    }
    
    #pragma region Navigation support
    
    /// The methods provided in this section are simply used to allow
    /// NavigationHelper to respond to the page's navigation methods.
    /// 
    /// Page specific logic should be placed in event handlers for the  
    /// <see cref="NavigationHelper::LoadState"/>
    /// and <see cref="NavigationHelper::SaveState"/>.
    /// The navigation parameter is available in the LoadState method 
    /// in addition to page state preserved during an earlier session.
    
    void FeedPage::OnNavigatedTo(NavigationEventArgs^ e)
    {
        NavigationHelper->OnNavigatedTo(e);
    }
    
    void FeedPage::OnNavigatedFrom(NavigationEventArgs^ e)
    {
        NavigationHelper->OnNavigatedFrom(e);
    }
    
    #pragma endregion
    
    /// <summary>
    /// Populates the page with content passed during navigation. Any saved state is also
    /// provided when recreating a page from a prior session.
    /// </summary>
    /// <param name="sender">
    /// The source of the event; typically <see cref="NavigationHelper"/>
    /// </param>
    /// <param name="e">Event data that provides both the navigation parameter passed to
    /// <see cref="Frame::Navigate(Type, Object)"/> when this page was initially requested and
    /// a dictionary of state preserved by this page during an earlier
    /// session. The state will be null the first time a page is visited.</param>
    void FeedPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e)
    {
    
        (void)sender; // Unused parameter
    
        if (!this->DefaultViewModel->HasKey("Feed"))
        {
            auto app = safe_cast<App^>(App::Current);
            app->GetCurrentFeedAsync().then([this, e](FeedData^ fd)
            {
                // Insert into the ViewModel for this page to 
                // initialize itemsViewSource->View
                this->DefaultViewModel->Insert("Feed", fd);
                this->DefaultViewModel->Insert("Items", fd->Items);
            }, task_continuation_context::use_current());
        }
    }
    
    /// <summary>
    /// Preserves state associated with this page in case the application is suspended or the
    /// page is discarded from the navigation cache.  Values must conform to the serialization
    /// requirements of <see cref="SuspensionManager::SessionState"/>.
    /// </summary>
    /// <param name="sender">The source of the event; typically <see cref="NavigationHelper"/></param>
    /// <param name="e">Event data that provides an empty dictionary to be populated with
    /// serializable state.</param>
    void FeedPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e)
    {
        (void)sender; // Unused parameter
    }
    
    

Обработчики событий (FeedPage приложения для телефона)

На странице FeedPage мы обрабатываем событие ItemClick, которое выполняет переход вперед на страницу, на которой пользователь может прочитать запись. Вы уже создали заглушку обработчика событий, когда нажали F12 на имени события в XAML.

  1. А теперь давайте заменим реализацию следующим кодом.

    void FeedPage::ItemListView_ItemClick(Platform::Object^ sender, ItemClickEventArgs^ e)
    {
        FeedItem^ clickedItem = dynamic_cast<FeedItem^>(e->ClickedItem);
        this->Frame->Navigate(TextViewerPage::typeid, clickedItem->Link->AbsoluteUri);
    }
    
  2. Нажмите F5 для сборки и запуска приложения для телефона в эмуляторе. Теперь при выборе элемента с MainPage, приложение должно переходить на страницу FeedPage и отображать список каналов. Следующий этап — отображение текста для выбранного веб-канала.

Hh465045.wedge(ru-ru,WIN.10).gifДобавьте разметку XAML (TextViewerPage приложения для телефона)

  1. В проекте Phone в файле TextViewerPage.xaml замените панель названия и сетку для содержимого указанной ниже разметкой, которая будет отображать имя приложения (ненавязчиво) и название текущей записи вместе с простым текстовым отображением содержимого.

     <!-- TitlePanel -->
            <StackPanel Grid.Row="0" Margin="24,17,0,28">
                <TextBlock Text="{StaticResource AppName}" 
                           Style="{ThemeResource TitleTextBlockStyle}" 
                           Typography.Capitals="SmallCaps"/>
                <TextBlock x:Name="FeedItemTitle" Margin="0,12,0,0" 
                           Style="{StaticResource SubheaderTextBlockStyle}" 
                           TextWrapping="Wrap"/>
            </StackPanel>
    
            <!--TODO: Content should be placed within the following grid-->
            <Grid Grid.Row="1" x:Name="ContentRoot">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>
    
                <ScrollViewer
                x:Name="itemDetail"
                AutomationProperties.AutomationId="ItemDetailScrollViewer"
                Grid.Row="1"
                Padding="20,20,20,20"
                HorizontalScrollBarVisibility="Disabled" 
                    VerticalScrollBarVisibility="Auto"
                ScrollViewer.HorizontalScrollMode="Disabled" 
                    ScrollViewer.VerticalScrollMode="Enabled"
                ScrollViewer.ZoomMode="Disabled" Margin="4,0,-4,0">
                    <!--Border enables background color for rich text block-->
                    <Border x:Name="contentViewBorder" BorderBrush="#FFFE5815"  
                            Background="AntiqueWhite" BorderThickness="6" Grid.Row="1">
                        <RichTextBlock x:Name="BlogTextBlock" Foreground="Black" 
                                       FontFamily="Segoe WP" FontSize="24" 
                                       Padding="10,10,10,10" 
                                       VerticalAlignment="Bottom" >
                        </RichTextBlock>
                    </Border>
                </ScrollViewer>
            </Grid>
    
  2. В файле TextViewerPage.xaml.h добавьте свойства NavigationHelper и DefaultViewItems, а также частный член m_FeedItem для хранения ссылки на текущий элемент канала после того, как мы просмотрим его в первый раз с помощью функции GetFeedItem, которую мы добавили в класс App на предыдущем шаге.

    Кроме того, добавьте функцию RichTextHyperlinkClicked. Теперь файл TextViewerPage.xaml.h должен выглядеть следующим образом:

    //
    // TextViewerPage.xaml.h
    // Declaration of the TextViewerPage class
    //
    
    #pragma once
    
    #include "TextViewerPage.g.h"
    #include "Common\NavigationHelper.h"
    
    namespace SimpleBlogReader
    {
        namespace WFC = Windows::Foundation::Collections;
        namespace WUIX = Windows::UI::Xaml;
        namespace WUIXNav = Windows::UI::Xaml::Navigation;
        namespace WUIXDoc = Windows::UI::Xaml::Documents;
        namespace WUIXControls = Windows::UI::Xaml::Controls;
    
                    /// <summary>
                    /// A basic page that provides characteristics common to most applications.
                    /// </summary>
                    [Windows::Foundation::Metadata::WebHostHidden]
                    public ref class TextViewerPage sealed
                    {
                    public:
                                    TextViewerPage();
    
                                    /// <summary>
                                    /// Gets the view model for this <see cref="Page"/>. 
                                    /// This can be changed to a strongly typed view model.
                                    /// </summary>
                                    property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel
                                    {
                                                    WFC::IObservableMap<Platform::String^, Platform::Object^>^  get();
                                    }
    
                                    /// <summary>
                                    /// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>.
                                    /// </summary>
                                    property Common::NavigationHelper^ NavigationHelper
                                    {
                                                    Common::NavigationHelper^ get();
                                    }
    
    
    
                    protected:
                                    virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override;
                                    virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override;
                                    void RichTextHyperlinkClicked(WUIXDoc::Hyperlink^ link, 
                                         WUIXDoc::HyperlinkClickEventArgs^ args);
    
                    private:
                                    void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e);
                                    void SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e);
    
                                    static Windows::UI::Xaml::DependencyProperty^ _defaultViewModelProperty;
                                    static Windows::UI::Xaml::DependencyProperty^ _navigationHelperProperty;
    
            FeedItem^ m_feedItem;
        };
    
    }
    

Hh465045.wedge(ru-ru,WIN.10).gifLoadState и SaveState (TextViewerPage приложения для телефона)

  1. В файле TextViewerPage.xaml.cpp добавьте указанную ниже директиву include.

    #include "WebViewerPage.xaml.h"
    
  2. Добавьте две указанные ниже директивы namespace.

    using namespace concurrency;
    using namespace Windows::UI::Xaml::Documents;
    
  3. Добавьте код для NavigationHelper и DefaultViewModel.

    TextViewerPage::TextViewerPage()
    {
        InitializeComponent();
        SetValue(_defaultViewModelProperty, 
            ref new Platform::Collections::Map<String^, Object^>(std::less<String^>()));
        auto navigationHelper = ref new Common::NavigationHelper(this);
        SetValue(_navigationHelperProperty, navigationHelper);
        navigationHelper->LoadState += 
            ref new Common::LoadStateEventHandler(this, &TextViewerPage::LoadState);
        navigationHelper->SaveState += 
            ref new Common::SaveStateEventHandler(this, &TextViewerPage::SaveState);
    
      //  this->DataContext = DefaultViewModel;
    
    }
    
    DependencyProperty^ TextViewerPage::_defaultViewModelProperty =
    DependencyProperty::Register("DefaultViewModel",
    TypeName(IObservableMap<String^, Object^>::typeid), TypeName(TextViewerPage::typeid), nullptr);
    
    /// <summary>
    /// Used as a trivial view model.
    /// </summary>
    IObservableMap<String^, Object^>^ TextViewerPage::DefaultViewModel::get()
    {
        return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty));
    }
    
    DependencyProperty^ TextViewerPage::_navigationHelperProperty =
    DependencyProperty::Register("NavigationHelper",
    TypeName(Common::NavigationHelper::typeid), TypeName(TextViewerPage::typeid), nullptr);
    
    /// <summary>
    /// Gets an implementation of <see cref="NavigationHelper"/> designed to be
    /// used as a trivial view model.
    /// </summary>
    Common::NavigationHelper^ TextViewerPage::NavigationHelper::get()
    {
        return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty));
    }
    
    #pragma region Navigation support
    
    /// The methods provided in this section are simply used to allow
    /// NavigationHelper to respond to the page's navigation methods.
    /// 
    /// Page specific logic should be placed in event handlers for the  
    /// <see cref="NavigationHelper::LoadState"/>
    /// and <see cref="NavigationHelper::SaveState"/>.
    /// The navigation parameter is available in the LoadState method 
    /// in addition to page state preserved during an earlier session.
    
    void TextViewerPage::OnNavigatedTo(NavigationEventArgs^ e)
    {
        NavigationHelper->OnNavigatedTo(e);
    }
    
    void TextViewerPage::OnNavigatedFrom(NavigationEventArgs^ e)
    {
        NavigationHelper->OnNavigatedFrom(e);
    }
    
    #pragma endregion
    
  4. Теперь замените реализации LoadState и SaveState указанным ниже кодом.

    void TextViewerPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e)
    {
        (void)sender;   // Unused parameter
        // (void)e; // Unused parameter
    
        auto app = safe_cast<App^>(App::Current);
        app->GetCurrentFeedAsync().then([this, app, e](FeedData^ fd)
        {        
            m_feedItem = app->GetFeedItem(fd, safe_cast<String^>(e->NavigationParameter));
            FeedItemTitle->Text = m_feedItem->Title;
            BlogTextBlock->Blocks->Clear();
            TextHelper^ helper = ref new TextHelper();
    
            auto blocks = helper->
                CreateRichText(m_feedItem->Content, 
                    ref new TypedEventHandler<Hyperlink^, HyperlinkClickEventArgs^>
                    (this, &TextViewerPage::RichTextHyperlinkClicked));
            for (auto b : blocks)
            {
                BlogTextBlock->Blocks->Append(b);
            }
        }, task_continuation_context::use_current());    
    }
    
    void TextViewerPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e)
    {
        (void)sender;   // Unused parameter
    
        e->PageState->Insert("Uri", m_feedItem->Link->AbsoluteUri);
    }
    

    Мы не можем выполнить привязку к RichTextBlock, поэтому мы создаем его содержимое вручную с помощью класса TextHelper. Для простоты мы используем функцию HtmlUtilities::ConvertToText, которая извлекает из веб-канала только текст. Как упражнение, вы можете попробовать выполнить анализ html или xml и добавить ссылки на изображения, а также на текст в коллекцию Blocks. У SyndicationClient есть функция анализа XML-каналов. Некоторые веб-каналы являются XML-документами правильного формата, а некоторые — нет.

Hh465045.wedge(ru-ru,WIN.10).gifОбработчики событий (TextViewerPage приложения для телефона)

  1. В TextViewerPage перейдем к WebViewerPage с помощью Hyperlink в формате RichText. Это обычно не является способом перехода между страницами, но он кажется подходящим в данном случае и позволяет нам увидеть, как работают гиперссылки. Мы уже добавили подпись функции в файл TextViewerPage.xaml.h. Теперь добавьте реализацию в TextViewerPage.xaml.cpp:

    ///<summary>
    /// Invoked when the user clicks on the "Link" text at the top of the rich text 
    /// view of the feed. This navigates to the web page. Identical action to using
    /// the App bar "forward" button.
    ///</summary>
    void TextViewerPage::RichTextHyperlinkClicked(Hyperlink^ hyperLink, 
        HyperlinkClickEventArgs^ args)
    {
        this->Frame->Navigate(WebViewerPage::typeid, m_feedItem->Link->AbsoluteUri);
    }
    
  2. Установите проект для телефона как запускаемый проект и нажмите F5. Вы должны иметь возможность нажать на элемент на странице канала и перейти к TextViewerPage, на которой можно прочитать запись блога. В этих блогах есть очень интересные записи!

Добавьте XAML (SplitPage приложения для Windows)

Приложение Windows работает несколько иначе, чем приложение для телефона. Мы уже знакомы с тем, как MainPage.xaml в проекте для Windows использует шаблон ItemsPage, который недоступен в приложениях для телефона. А теперь попробуем добавить страницу SplitPage, также недоступную для телефона. Когда устройство находится в альбомной ориентации, страница SplitPage в приложениях для Windows имеет правую и левую панель. Когда пользователь переходит на страницу в нашем приложении, он увидит список элементов веб-канала в левой панели и отображение текста текущего выбранного веб-канала в правой панели. Если устройство находится в книжной ориентации или окно имеет неполную ширину, то страница SplitPage будет использовать VisualStates, чтобы работать как две отдельные страницы. В коде это называется "логическая навигация по страницам".

  1. Начните работу с указанного ниже кода, представляющего собой XAML для базовой страницы SplitPage, которая использовалась в качестве шаблона по умолчанию в проектах для Windows 8.

    <Page
        x:Name="pageRoot"
        x:Class="SimpleBlogReader.SplitPage"
        DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
        xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="using:SimpleBlogReader"
        xmlns:common="using:SimpleBlogReader.Common"
        xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d">
    
    
        <Page.Resources>
            <!-- Collection of items displayed by this page -->
            <CollectionViewSource
            x:Name="itemsViewSource"
            Source="{Binding Items}"/>
        </Page.Resources>
    
        <Page.TopAppBar>
            <AppBar Padding="10,0,10,0">
                <Grid>
                    <AppBarButton x:Name="fwdButton" Height="95" Margin="150,46,0,0"
                              Command="{Binding NavigationHelper.GoForwardCommand, ElementName=pageRoot}" 
                              AutomationProperties.Name="Forward"
                              AutomationProperties.AutomationId="ForwardButton"
                              AutomationProperties.ItemType="Navigation Button"
                              HorizontalAlignment="Right"
                              Icon="Forward"
                              Click="fwdButton_Click"/>
                </Grid>
            </AppBar>
        </Page.TopAppBar>
        <!--
            This grid acts as a root panel for the page that defines two rows:
            * Row 0 contains the back button and page title
            * Row 1 contains the rest of the page layout
        -->
        <Grid Style="{StaticResource WindowsBlogLayoutRootStyle}">
            <Grid.ChildrenTransitions>
                <TransitionCollection>
                    <EntranceThemeTransition/>
                </TransitionCollection>
            </Grid.ChildrenTransitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="140"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition x:Name="primaryColumn" Width="420"/>
                <ColumnDefinition x:Name="secondaryColumn" Width="*"/>
            </Grid.ColumnDefinitions>
    
            <!-- Back button and page title -->
            <Grid x:Name="titlePanel" Grid.ColumnSpan="1">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="120"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Button x:Name="backButton" Margin="39,59,39,0" 
                        Command="{Binding NavigationHelper.GoBackCommand, ElementName=pageRoot}"
                            Style="{StaticResource NavigationBackButtonNormalStyle}"
                            VerticalAlignment="Top"
                            AutomationProperties.Name="Back"
                            AutomationProperties.AutomationId="BackButton"
                            AutomationProperties.ItemType="Navigation Button"/>
    
                <TextBlock x:Name="pageTitle" Grid.Column="1" Text="{Binding Title}" 
                           Style="{StaticResource HeaderTextBlockStyle}"
                           IsHitTestVisible="false" TextWrapping="NoWrap" 
                           VerticalAlignment="Bottom" Padding="10,10,10,10" Margin="0,0,30,40">
                    <TextBlock.Transitions>
                        <TransitionCollection>
                            <ContentThemeTransition/>
                        </TransitionCollection>
                    </TextBlock.Transitions>
                </TextBlock>
            </Grid>
    
            <!-- Vertical scrolling item list -->
            <ListView
                x:Name="itemListView"
                AutomationProperties.AutomationId="ItemsListView"
                AutomationProperties.Name="Items"
                TabIndex="1"
                Grid.Row="1"
                Margin="-10,-10,0,0"
                Padding="120,0,0,60"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                IsSwipeEnabled="False"
                SelectionChanged="ItemListView_SelectionChanged">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <Grid Margin="6">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <Border Background="{ThemeResource ListViewItemPlaceholderBackgroundThemeBrush}" Width="60" Height="60">
                                <Image Source="{Binding ImagePath}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/>
                            </Border>
                            <StackPanel Grid.Column="1" Margin="10,0,0,0">
                                <TextBlock Text="{Binding Title}" Style="{StaticResource TitleTextBlockStyle}" TextWrapping="NoWrap" MaxHeight="40"/>
                                <TextBlock Text="{Binding Subtitle}" Style="{StaticResource CaptionTextBlockStyle}" TextWrapping="NoWrap"/>
                            </StackPanel>
                        </Grid>
                    </DataTemplate>
                </ListView.ItemTemplate>
                <ListView.ItemContainerStyle>
                    <Style TargetType="FrameworkElement">
                        <Setter Property="Margin" Value="0,0,0,10"/>
                    </Style>
                </ListView.ItemContainerStyle>
            </ListView>
    
    
            <!-- Details for selected item -->
            <ScrollViewer
                x:Name="itemDetail"
                AutomationProperties.AutomationId="ItemDetailScrollViewer"
                Grid.Column="1"
                Grid.RowSpan="2"
                Padding="60,0,66,0"
                DataContext="{Binding SelectedItem, ElementName=itemListView}"
                HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"
                ScrollViewer.HorizontalScrollMode="Disabled" ScrollViewer.VerticalScrollMode="Enabled"
                ScrollViewer.ZoomMode="Disabled">
    
                <Grid x:Name="itemDetailGrid" Margin="0,60,0,50">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
    
                    <Image Grid.Row="1" Margin="0,0,20,0" Width="180" Height="180" Source="{Binding ImagePath}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/>
                    <StackPanel x:Name="itemDetailTitlePanel" Grid.Row="1" Grid.Column="1">
                        <TextBlock x:Name="itemTitle" Margin="0,-10,0,0" Text="{Binding Title}" Style="{StaticResource SubheaderTextBlockStyle}"/>
                        <TextBlock x:Name="itemSubtitle" Margin="0,0,0,20" Text="{Binding Subtitle}" Style="{StaticResource SubtitleTextBlockStyle}"/>
                    </StackPanel>
                    <TextBlock Grid.Row="2" Grid.ColumnSpan="2" Margin="0,20,0,0" Text="{Binding Content}" Style="{StaticResource BodyTextBlockStyle}"/>
                </Grid>
            </ScrollViewer>
    
    
            <VisualStateManager.VisualStateGroups>
    
                <!-- Visual states reflect the application's view state -->
                <VisualStateGroup x:Name="ViewStates">
                    <VisualState x:Name="PrimaryView" />
                    <VisualState x:Name="SinglePane">
                        <Storyboard>
                            <ObjectAnimationUsingKeyFrames 
                                Storyboard.TargetName="primaryColumn" 
                                Storyboard.TargetProperty="Width">
                                <DiscreteObjectKeyFrame KeyTime="0" Value="*"/>
                            </ObjectAnimationUsingKeyFrames>
                            <ObjectAnimationUsingKeyFrames 
                                Storyboard.TargetName="secondaryColumn" 
                                Storyboard.TargetProperty="Width">
                                <DiscreteObjectKeyFrame KeyTime="0" Value="0"/>
                            </ObjectAnimationUsingKeyFrames>
                            <ObjectAnimationUsingKeyFrames 
                                Storyboard.TargetName="itemDetail" 
                                Storyboard.TargetProperty="Visibility">
                                <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
                            </ObjectAnimationUsingKeyFrames>
                            <ObjectAnimationUsingKeyFrames 
                                Storyboard.TargetName="ItemListView" 
                                Storyboard.TargetProperty="Padding">
                                <DiscreteObjectKeyFrame KeyTime="0" Value="120,0,90,60"/>
                            </ObjectAnimationUsingKeyFrames>
                        </Storyboard>
                    </VisualState>
                    <!--
                        When an item is selected and only one pane is shown the details display requires more extensive changes:
                         * Hide the master list and the column it was in
                         * Move item details down a row to make room for the title
                         * Move the title directly above the details
                         * Adjust padding for details
                     -->
                    <VisualState x:Name="SinglePane_Detail">
                        <Storyboard>
                            <ObjectAnimationUsingKeyFrames 
                                Storyboard.TargetName="primaryColumn" 
                                Storyboard.TargetProperty="Width">
                                <DiscreteObjectKeyFrame KeyTime="0" Value="0"/>
                            </ObjectAnimationUsingKeyFrames>
                            <ObjectAnimationUsingKeyFrames 
                                Storyboard.TargetName="secondaryColumn" 
                                Storyboard.TargetProperty="Width">
                                <DiscreteObjectKeyFrame KeyTime="0" Value="*"/>
                            </ObjectAnimationUsingKeyFrames>
                            <ObjectAnimationUsingKeyFrames 
                                Storyboard.TargetName="ItemListView" 
                                Storyboard.TargetProperty="Visibility">
                                <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
                            </ObjectAnimationUsingKeyFrames>
                            <ObjectAnimationUsingKeyFrames 
                                Storyboard.TargetName="titlePanel" 
                                Storyboard.TargetProperty="(Grid.Column)">
                                <DiscreteObjectKeyFrame KeyTime="0" Value="0"/>
                            </ObjectAnimationUsingKeyFrames>
                            <ObjectAnimationUsingKeyFrames 
                                Storyboard.TargetName="itemDetail" 
                                Storyboard.TargetProperty="Padding">
                                <DiscreteObjectKeyFrame KeyTime="0" Value="10,0,10,0"/>
                            </ObjectAnimationUsingKeyFrames>
                        </Storyboard>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>
        </Grid>
    </Page>
    
  2. Для страницы, используемой по умолчанию, уже настроены контекст данных и элемент CollectionViewSource.

    Давайте оптимизируем сетку titlePanel таким образом, чтобы она занимала два столбца. Это позволит заголовку веб-канала отображаться на полную ширину экрана:

    <Grid x:Name="titlePanel" Grid.ColumnSpan="2">
    
  3. Теперь найдите TextBlock с именем pageTitle в этой же сетке и измените привязку с Title на Feed.Title.

    Text="{Binding Feed.Title}"
    
  4. Теперь найдите комментарий "Vertical scrolling item list" (Список элементов с вертикальной прокруткой) и замените элемент ListView по умолчанию следующим:

            <!-- Vertical scrolling item list -->
            <ListView
                x:Name="itemListView"
                AutomationProperties.AutomationId="ItemsListView"
                AutomationProperties.Name="Items"
                TabIndex="1"
                Grid.Row="1"
                Margin="10,10,0,0"
                Padding="10,0,0,60"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                IsSwipeEnabled="False"
                SelectionChanged="ItemListView_SelectionChanged"
                ItemTemplate="{StaticResource ListItemTemplate}">
    
                <ListView.ItemContainerStyle>
                    <Style TargetType="FrameworkElement">
                        <Setter Property="Margin" Value="0,0,0,10"/>
                    </Style>
                </ListView.ItemContainerStyle>
            </ListView>
    
  5. Область сведений страницы SplitPage может содержать все, что вы хотите. В этом приложении мы поместим в нее RichTextBlock и отобразим простую текстовую версию записи блога. Мы можем использовать служебную функцию, предоставляемую Windows для анализа HTML из FeedItem и вернуть Platform::String, а затем мы используем наш собственный вспомогательный класс для разделения возвращенной строки на абзацы и построения элементов форматированного текста. Это представление не будет отображать изображения, но оно быстро загружается и, если вы хотите расширить это приложение, можно позднее добавить для пользователя параметр выбора шрифта и размера шрифта.

    Найдите элемент ScrollViewer под комментарием "Details for selected item" (Сведения для выбранного элемента) и удалите его. Затем вставьте эту разметку:

            <!-- Details for selected item -->
            <Grid x:Name="itemDetailGrid" 
                  Grid.Row="1"
                  Grid.Column="1"
                  Margin="10,10,10,10">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>
                <TextBlock x:Name="itemTitle" Margin="10,10,10,10" 
                           DataContext="{Binding SelectedItem, ElementName=itemListView}" 
                           Text="{Binding Title}" 
                           Style="{StaticResource SubheaderTextBlockStyle}"/>
                <ScrollViewer
                x:Name="itemDetail"
                AutomationProperties.AutomationId="ItemDetailScrollViewer"
                Grid.Row="1"
                Padding="20,20,20,20"
                DataContext="{Binding SelectedItem, ElementName=itemListView}"
                HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"
                ScrollViewer.HorizontalScrollMode="Disabled" ScrollViewer.VerticalScrollMode="Enabled"
                ScrollViewer.ZoomMode="Disabled" Margin="4,0,-4,0">
                    <Border x:Name="contentViewBorder" BorderBrush="#FFFE5815" 
                            Background="Honeydew" BorderThickness="5" Grid.Row="1">
                        <RichTextBlock x:Name="BlogTextBlock" Foreground="Black" 
                                       FontFamily="Lucida Sans" 
                                       FontSize="32"
                                       Margin="20,20,20,20">                        
                        </RichTextBlock>
                    </Border>
                </ScrollViewer>
            </Grid>
    

LoadState и SaveState (SplitPage приложения для Windows)

  1. Замените созданную вами страницу SplitPage указанным ниже кодом.

    Файл SplitPage.xaml.h должен выглядеть следующим образом:

    //
    // SplitPage.xaml.h
    // Declaration of the SplitPage class
    //
    
    #pragma once
    
    #include "SplitPage.g.h"
    #include "Common\NavigationHelper.h"
    
    namespace SimpleBlogReader
    {
        namespace WFC = Windows::Foundation::Collections;
        namespace WUIX = Windows::UI::Xaml;
        namespace WUIXNav = Windows::UI::Xaml::Navigation;
        namespace WUIXDoc = Windows::UI::Xaml::Documents;
        namespace WUIXControls = Windows::UI::Xaml::Controls;
    
           /// <summary>
           /// A page that displays a group title, a list of items within the group, and details for the
           /// currently selected item.
           /// </summary>
           [Windows::Foundation::Metadata::WebHostHidden]
           public ref class SplitPage sealed
           {
           public:
                  SplitPage();
    
                  /// <summary>
                  /// This can be changed to a strongly typed view model.
                  /// </summary>
                  property WFC::IObservableMap<Platform::String^, 
                Platform::Object^>^ DefaultViewModel
                  {
                         WFC::IObservableMap<Platform::String^, Platform::Object^>^  get();
                  }
    
                  /// <summary>
                  /// NavigationHelper is used on each page to aid in navigation and 
                  /// process lifetime management
                  /// </summary>
                  property Common::NavigationHelper^ NavigationHelper
                  {
                         Common::NavigationHelper^ get();
                  }
           protected:
                  virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override;
                  virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override;
    
           private:
                  void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e);
                  void SaveState(Object^ sender, Common::SaveStateEventArgs^ e);
                  bool CanGoBack();
                  void GoBack();
    
    #pragma region Logical page navigation
    
                  // The split page isdesigned so that when the Window does have enough space to show
                  // both the list and the dteails, only one pane will be shown at at time.
                  //
                  // This is all implemented with a single physical page that can represent two logical
                  // pages.  The code below achieves this goal without making the user aware of the
                  // distinction.
    
                  void Window_SizeChanged(Platform::Object^ sender, 
                Windows::UI::Core::WindowSizeChangedEventArgs^ e);
                  void ItemListView_SelectionChanged(Platform::Object^ sender, 
                WUIXControls::SelectionChangedEventArgs^ e);
                  bool UsingLogicalPageNavigation();
                  void InvalidateVisualState();
                  Platform::String^ DetermineVisualState();
    
    #pragma endregion
    
                  static Windows::UI::Xaml::DependencyProperty^ _defaultViewModelProperty;
                  static Windows::UI::Xaml::DependencyProperty^ _navigationHelperProperty;
                  static const int MinimumWidthForSupportingTwoPanes = 768;
    
            void fwdButton_Click(Platform::Object^ sender, WUIX::RoutedEventArgs^ e);
            void pageRoot_SizeChanged(Platform::Object^ sender, WUIX::SizeChangedEventArgs^ e);
           };
    }
    

    В файле SplitPage.xaml.cpp в качестве отправной точки используйте следующий код: Благодаря этому будет реализована базовая страница SplitPage и добавлена поддержка таких же элементов NavigationHelper и SuspensionManager, которые вы добавили на другие страницы. Кроме того, будет добавлен такой же обработчик событий SizeChanged, как и на предыдущей странице.

    //
    // SplitPage.xaml.cpp
    // Implementation of the SplitPage class
    //
    
    #include "pch.h"
    #include "SplitPage.xaml.h"
    
    using namespace SimpleBlogReader;
    using namespace SimpleBlogReader::Common;
    
    using namespace Platform;
    using namespace Platform::Collections;
    using namespace concurrency;
    using namespace Windows::Foundation;
    using namespace Windows::Foundation::Collections;
    using namespace Windows::UI::Core;
    using namespace Windows::UI::ViewManagement;
    using namespace Windows::UI::Xaml;
    using namespace Windows::UI::Xaml::Controls;
    using namespace Windows::UI::Xaml::Controls::Primitives;
    using namespace Windows::UI::Xaml::Data;
    using namespace Windows::UI::Xaml::Documents;
    using namespace Windows::UI::Xaml::Input;
    using namespace Windows::UI::Xaml::Interop;
    using namespace Windows::UI::Xaml::Media;
    using namespace Windows::UI::Xaml::Navigation;
    
    // The Split Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234234
    
    SplitPage::SplitPage()
    {
        InitializeComponent();
        SetValue(_defaultViewModelProperty, ref new Map<String^, Object^>(std::less<String^>()));
        auto navigationHelper = ref new Common::NavigationHelper(this,
            ref new Common::RelayCommand(
            [this](Object^) -> bool
        {
            return CanGoBack();
        },
            [this](Object^) -> void
        {
            GoBack();
        }
            )
            );
        SetValue(_navigationHelperProperty, navigationHelper);
        navigationHelper->LoadState += 
            ref new Common::LoadStateEventHandler(this, &SplitPage::LoadState);
        navigationHelper->SaveState += 
            ref new Common::SaveStateEventHandler(this, &SplitPage::SaveState);
    
        ItemListView->SelectionChanged += 
            ref new SelectionChangedEventHandler(this, &SplitPage::ItemListView_SelectionChanged);
        Window::Current->SizeChanged += 
            ref new WindowSizeChangedEventHandler(this, &SplitPage::Window_SizeChanged);
        InvalidateVisualState();
    
    }
    
    DependencyProperty^ SplitPage::_defaultViewModelProperty =
    DependencyProperty::Register("DefaultViewModel",
    TypeName(IObservableMap<String^, Object^>::typeid), TypeName(SplitPage::typeid), nullptr);
    
    /// <summary>
    /// used as a trivial view model.
    /// </summary>
    IObservableMap<String^, Object^>^ SplitPage::DefaultViewModel::get()
    {
        return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty));
    }
    
    DependencyProperty^ SplitPage::_navigationHelperProperty =
    DependencyProperty::Register("NavigationHelper",
    TypeName(Common::NavigationHelper::typeid), TypeName(SplitPage::typeid), nullptr);
    
    /// <summary>
    /// Gets an implementation of <see cref="NavigationHelper"/> designed to be
    /// used as a trivial view model.
    /// </summary>
    Common::NavigationHelper^ SplitPage::NavigationHelper::get()
    {
        //        return _navigationHelper;
        return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty));
    }
    
    #pragma region Page state management
    
    /// <summary>
    /// Populates the page with content passed during navigation.  Any saved state is also
    /// provided when recreating a page from a prior session.
    /// </summary>
    /// <param name="navigationParameter">The parameter value passed to
    /// <see cref="Frame::Navigate(Type, Object)"/> when this page was initially requested.
    /// </param>
    /// <param name="pageState">A map of state preserved by this page during an earlier
    /// session.  This will be null the first time a page is visited.</param>
    void SplitPage::LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e)
    {
        if (!this->DefaultViewModel->HasKey("Feed"))
        {
            auto app = safe_cast<App^>(App::Current);
            app->GetCurrentFeedAsync().then([this, app, e](FeedData^ fd)
            {
                // Insert into the ViewModel for this page to initialize itemsViewSource->View
                this->DefaultViewModel->Insert("Feed", fd);
                this->DefaultViewModel->Insert("Items", fd->Items);
    
                if (e->PageState == nullptr)
                {
                    // When this is a new page, select the first item automatically 
                    // unless logical page navigation is being used (see the logical
                    // page navigation #region below).
                    if (!UsingLogicalPageNavigation() && itemsViewSource->View != nullptr)
                    {
                        this->itemsViewSource->View->MoveCurrentToFirst();
                    }
                    else
                    {
                        this->itemsViewSource->View->MoveCurrentToPosition(-1);
                    }
                }
                else
                {
                    auto itemUri = safe_cast<String^>(e->PageState->Lookup("SelectedItemUri"));
                    auto app = safe_cast<App^>(App::Current);
                    auto selectedItem = app->GetFeedItem(fd, itemUri);
    
                    if (selectedItem != nullptr)
                    {
                        this->itemsViewSource->View->MoveCurrentTo(selectedItem);
                    }
                }
            }, task_continuation_context::use_current());
        }
    }
    
    /// <summary>
    /// Preserves state associated with this page in case the application is suspended or the
    /// page is discarded from the navigation cache.  Values must conform to the serialization
    /// requirements of <see cref="SuspensionManager::SessionState"/>.
    /// </summary>
    /// <param name="sender">The source of the event; typically <see cref="NavigationHelper"/></param>
    /// <param name="e">Event data that provides an empty dictionary to be populated with
    /// serializable state.</param>
    void SplitPage::SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e)
    {
        if (itemsViewSource->View != nullptr)
        {
            auto selectedItem = itemsViewSource->View->CurrentItem;
            if (selectedItem != nullptr)
            {
                auto feedItem = safe_cast<FeedItem^>(selectedItem);
                e->PageState->Insert("SelectedItemUri", feedItem->Link->AbsoluteUri);
            }
        }
    }
    
    #pragma endregion
    
    #pragma region Logical page navigation
    
    // Visual state management typically reflects the four application view states directly (full
    // screen landscape and portrait plus snapped and filled views.)  The split page is designed so
    // that the snapped and portrait view states each have two distinct sub-states: either the item
    // list or the details are displayed, but not both at the same time.
    //
    // This is all implemented with a single physical page that can represent two logical pages.
    // The code below achieves this goal without making the user aware of the distinction.
    
    /// <summary>
    /// Invoked to determine whether the page should act as one logical page or two.
    /// </summary>
    /// <returns>True when the current view state is portrait or snapped, false
    /// otherwise.</returns>
    bool SplitPage::CanGoBack()
    {
        if (UsingLogicalPageNavigation() && ItemListView->SelectedItem != nullptr)
        {
            return true;
        }
        else
        {
            return NavigationHelper->CanGoBack();
        }
    }
    
    void SplitPage::GoBack()
    {
        if (UsingLogicalPageNavigation() && ItemListView->SelectedItem != nullptr)
        {
            // When logical page navigation is in effect and there's a selected item that
            // item's details are currently displayed.  Clearing the selection will return to
            // the item list.  From the user's point of view this is a logical backward
            // navigation.
            ItemListView->SelectedItem = nullptr;
        }
        else
       {
            NavigationHelper->GoBack();
        }
    }
    
    /// <summary>
    /// Invoked with the Window changes size
    /// </summary>
    /// <param name="sender">The current Window</param>
    /// <param name="e">Event data that describes the new size of the Window</param>
    void SplitPage::Window_SizeChanged(Platform::Object^ sender, WindowSizeChangedEventArgs^ e)
    {
        InvalidateVisualState();
    }
    
    /// <summary>
    /// Invoked when an item within the list is selected.
    /// </summary>
    /// <param name="sender">The GridView displaying the selected item.</param>
    /// <param name="e">Event data that describes how the selection was changed.</param>
    void SplitPage::ItemListView_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e)
    {
           if (UsingLogicalPageNavigation())
           {
                  InvalidateVisualState();
           }
    }
    
    /// <summary>
    /// Invoked to determine whether the page should act as one logical page or two.
    /// </summary>
    /// <returns>True if the window should show act as one logical page, false
    /// otherwise.</returns>
    bool SplitPage::UsingLogicalPageNavigation()
    {
        return Window::Current->Bounds.Width <= MinimumWidthForSupportingTwoPanes;
    }
    
    void SplitPage::InvalidateVisualState()
    {
        auto visualState = DetermineVisualState();
        VisualStateManager::GoToState(this, visualState, false);
        NavigationHelper->GoBackCommand->RaiseCanExecuteChanged();
    }
    
    /// <summary>
    /// Invoked to determine the name of the visual state that corresponds to an application
    /// view state.
    /// </summary>
    /// <returns>The name of the desired visual state.  This is the same as the name of the
    /// view state except when there is a selected item in portrait and snapped views where
    /// this additional logical page is represented by adding a suffix of _Detail.</returns>
    Platform::String^ SplitPage::DetermineVisualState()
    {
        if (!UsingLogicalPageNavigation())
            return "PrimaryView";
    
        // Update the back button's enabled state when the view state changes
        auto logicalPageBack = UsingLogicalPageNavigation() 
            && ItemListView->SelectedItem != nullptr;
    
        return logicalPageBack ? "SinglePane_Detail" : "SinglePane";
    }
    
    #pragma endregion
    
    #pragma region Navigation support
    
    /// The methods provided in this section are simply used to allow
    /// NavigationHelper to respond to the page's navigation methods.
    /// 
    /// Page specific logic should be placed in event handlers for the  
    /// <see cref="NavigationHelper::LoadState"/>
    /// and <see cref="NavigationHelper::SaveState"/>.
    /// The navigation parameter is available in the LoadState method 
    /// in addition to page state preserved during an earlier session.
    
    void SplitPage::OnNavigatedTo(NavigationEventArgs^ e)
    {
        NavigationHelper->OnNavigatedTo(e);
    }
    
    void SplitPage::OnNavigatedFrom(NavigationEventArgs^ e)
    {
        NavigationHelper->OnNavigatedFrom(e);
    
    }
    #pragma endregion
    
    void SimpleBlogReader::SplitPage::fwdButton_Click(Platform::Object^ sender, RoutedEventArgs^ e)
    {
        // Navigate to the appropriate destination page, and configure the new page
        // by passing required information as a navigation parameter.
        auto selectedItem = dynamic_cast<FeedItem^>(this->ItemListView->SelectedItem);
    
        // selectedItem will be nullptr if the user invokes the app bar
        // and clicks on "view web page" without selecting an item.
        if (this->Frame != nullptr && selectedItem != nullptr)
        {
            auto itemUri = safe_cast<String^>(selectedItem->Link->AbsoluteUri);
            this->Frame->Navigate(WebViewerPage::typeid, itemUri);
        }
    }
    
    /// <summary>
    /// 
    /// 
    /// </summary>
    void SimpleBlogReader::SplitPage::pageRoot_SizeChanged(
        Platform::Object^ sender,
        SizeChangedEventArgs^ e)
    {
        if (e->NewSize.Height / e->NewSize.Width >= 1)
        {
            VisualStateManager::GoToState(this, "SinglePane", true);
        }
        else
        {
            VisualStateManager::GoToState(this, "PrimaryView", true);
        }
    }
    
  2. В файле SplitPage.xaml.cpp добавьте эту директиву using:

    using namespace Windows::UI::Xaml::Documents;
    
  3. Теперь замените LoadState и SaveState этим кодом:

    void SplitPage::LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e)
    {
        if (!this->DefaultViewModel->HasKey("Feed"))
        {
            auto app = safe_cast<App^>(App::Current);
            app->GetCurrentFeedAsync().then([this, app, e](FeedData^ fd)
            {
                // Insert into the ViewModel for this page to initialize itemsViewSource->View
                this->DefaultViewModel->Insert("Feed", fd);
                this->DefaultViewModel->Insert("Items", fd->Items);
    
                if (e->PageState == nullptr)
                {
                    // When this is a new page, select the first item automatically unless logical page
                    // navigation is being used (see the logical page navigation #region below).
                    if (!UsingLogicalPageNavigation() && itemsViewSource->View != nullptr)
                    {
                        this->itemsViewSource->View->MoveCurrentToFirst();
                    }
                    else
                    {
                        this->itemsViewSource->View->MoveCurrentToPosition(-1);
                    }
                }
                else
                {
                    auto itemUri = safe_cast<String^>(e->PageState->Lookup("SelectedItemUri"));
                    auto app = safe_cast<App^>(App::Current);
                    auto selectedItem = GetFeedItem(fd, itemUri);
    
                    if (selectedItem != nullptr)
                    {
                        this->itemsViewSource->View->MoveCurrentTo(selectedItem);
                    }
                }
            }, task_continuation_context::use_current());
        }
    }
    
    /// <summary>
    /// Preserves state associated with this page in case the application is suspended or the
    /// page is discarded from the navigation cache.  Values must conform to the serialization
    /// requirements of <see cref="SuspensionManager::SessionState"/>.
    /// </summary>
    /// <param name="sender">The source of the event; typically <see cref="NavigationHelper"/></param>
    /// <param name="e">Event data that provides an empty dictionary to be populated with
    /// serializable state.</param>
    void SplitPage::SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e)
    {
        if (itemsViewSource->View != nullptr)
        {
            auto selectedItem = itemsViewSource->View->CurrentItem;
            if (selectedItem != nullptr)
            {
                auto feedItem = safe_cast<FeedItem^>(selectedItem);
                e->PageState->Insert("SelectedItemUri", feedItem->Link->AbsoluteUri);
            }
        }
    }
    

    Обратите внимание, что мы используем метод GetCurrentFeedAsync, который мы добавили в общий проект раньше. Единственное различие между этой страницей и страницей приложения для телефона это то, что теперь мы продолжаем отслеживание выбранного элемента. В SaveState мы вставляем текущий выбранный элемент в объект PageState, поэтому SuspensionManager сохраняет его по мере необходимости и он будет доступен нам в объекте PageState снова при вызове LoadState. Нам понадобится эта строка для определения текущего FeedItem в текущем Канале.

Обработчики событий (SplitPage приложения для Windows)

Если выбранный элемент изменяется, область сведений будет использовать класс TextHelper для обработки текста.

  1. В файле SplitPage.xaml.cpp добавьте следующие директивы #include:

    #include "TextHelper.h"
    #include "WebViewerPage.xaml.h"
    
  2. Замените заглушку обработчика событий SelectionChanged по умолчанию этой заглушкой:

    void SimpleBlogReader::SplitPage::ItemListView_SelectionChanged(
        Platform::Object^ sender,
        SelectionChangedEventArgs^ e)
    {
        if (UsingLogicalPageNavigation())
        {
            InvalidateVisualState();
        }
    
        // Sometimes there is no selected item, e.g. when navigating back
        // from detail in logical page navigation.
        auto fi = dynamic_cast<FeedItem^>(itemListView->SelectedItem);
        if (fi != nullptr)
        {
            BlogTextBlock->Blocks->Clear();
            TextHelper^ helper = ref new TextHelper();
            auto blocks = helper->CreateRichText(fi->Content, 
                ref new TypedEventHandler<Hyperlink^, 
                HyperlinkClickEventArgs^>(this, &SplitPage::RichTextHyperlinkClicked));
            for (auto b : blocks)
            {
                BlogTextBlock->Blocks->Append(b);
            }
        }
    }
    

    Эта функция определяет обратный вызов, который будет передан гиперссылке, которую мы создаем в форматированном тексте.

  3. Добавьте эту частную функцию-член в файл SplitPage.xaml.h:

    void RichTextHyperlinkClicked(Windows::UI::Xaml::Documents::Hyperlink^ link, 
        Windows::UI::Xaml::Documents::HyperlinkClickEventArgs^ args);
    
  4. Добавьте эту реализацию в SplitPage.xaml.cpp:

    /// <summary>
    ///  Navigate to the appropriate destination page, and configure the new page
    ///  by passing required information as a navigation parameter.
    /// </summary>
    void SplitPage::RichTextHyperlinkClicked(
        Hyperlink^ hyperLink,
        HyperlinkClickEventArgs^ args)
    {
    
        auto selectedItem = dynamic_cast<FeedItem^>(this->itemListView->SelectedItem);
    
        // selectedItem will be nullptr if the user invokes the app bar
        // and clicks on "view web page" without selecting an item.
        if (this->Frame != nullptr && selectedItem != nullptr)
        {
            auto itemUri = safe_cast<String^>(selectedItem->Link->AbsoluteUri);
            this->Frame->Navigate(WebViewerPage::typeid, itemUri);
        }
    }
    

    Эта функция, в свою очередь, ссылается на следующую страницу в стеке навигации. Теперь можно нажать F5 и увидеть обновление текста по мере изменения выбора. Запустите в симуляторе и поверните виртуальное устройство, чтобы увидеть, что объекты VisualState по умолчанию обрабатывают книжную и альбомную ориентации именно так, как ожидается. Щелкните на текст Ссылки в тексте блога и перейдите на страницу WebViewerPage. Конечно, она пока имеет содержимого. Мы вернемся к этому после того, как дойдем до этого этапа в проекте приложения для телефона.

Об обратной навигации

Возможно, вы заметили, что в приложении для Windows SplitPage предоставляет кнопку обратной навигации, которая возвращает вас на страницу MainPage без дополнительного программирования с вашей стороны. В приложении для телефона, функции кнопки "Назад" предоставляются аппаратной кнопкой "Назад", а не программными кнопками. Навигация в телефоне с помощью кнопки "Назад" обрабатывается классом NavigationHelper в папке "Common". Чтобы увидеть соответствующий код в вашем решении, выполните поиск "BackPressed" (Ctrl + Shift + F). Напоминаем, что ничего дополнительное здесь делать не требуется. Он просто работает!

Часть 9. Добавление представления веб-сайта выбранной записи.

Последней страницей, которую мы добавим, является страница, которая будет отображать запись блога на его первоначальной веб-странице. Иногда читатель может захотеть увидеть и изображения! Недостатком просмотра веб-страниц является то, что текст может быть трудно читать с экрана телефона, а не все веб-страницы правильно отформатированы для мобильных устройств. Иногда поля выходят за края экрана и требуют много горизонтальной прокрутки. Наша страница WebViewerPage относительно проста. Мы просто добавляем на страницу элемент управления WebView и позволяем ему сделать всю работу. Начнем с проекта приложения для телефона:

Hh465045.wedge(ru-ru,WIN.10).gifДобавьте XAML (WebViewerPage приложения для телефона)

  • В файле WebViewerPage.xaml добавьте панель названия и сетку contentRoot:

            <!-- TitlePanel -->
            <StackPanel Grid.Row="0" Margin="10,10,10,10">
                <TextBlock Text="{StaticResource AppName}" 
                           Style="{ThemeResource TitleTextBlockStyle}" 
                           Typography.Capitals="SmallCaps"/>
            </StackPanel>
    
            <!--TODO: Content should be placed within the following grid-->
            <Grid Grid.Row="1" x:Name="ContentRoot">
                <!-- Back button and page title -->
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
    
                <!--This will render while web page is still downloading, 
                indicating that something is happening-->
                <TextBlock x:Name="pageTitle" Text="{Binding Title}" Grid.Column="1" 
                           IsHitTestVisible="false" 
                           TextWrapping="WrapWholeWords"  
                           VerticalAlignment="Center"  
                           HorizontalAlignment="Center"  
                           Margin="40,20,40,20"/>
    
            </Grid>
    

Hh465045.wedge(ru-ru,WIN.10).gifLoadState и SaveState (WebViewerPage приложения для телефона)

  1. Начните страницу WebViewerPage, как и любую другую, с обеспечения поддержки элементов NavigationHelper и DefaultItems в файле WebViewerPage.xaml.h и реализации их в файле WebViewerPage.xaml.cpp.

    Файл WebViewerPage.xaml.h должен начинаться так:

    //
    // WebViewerPage.xaml.h
    // Declaration of the WebViewerPage class
    //
    
    #pragma once
    
    #include "WebViewerPage.g.h"
    #include "Common\NavigationHelper.h"
    
    namespace SimpleBlogReader
    {
        namespace WFC = Windows::Foundation::Collections;
        namespace WUIX = Windows::UI::Xaml;
        namespace WUIXNav = Windows::UI::Xaml::Navigation;
        namespace WUIXControls = Windows::UI::Xaml::Controls;
    
                    /// <summary>
                    /// A basic page that provides characteristics common to most applications.
                    /// </summary>
                    [Windows::Foundation::Metadata::WebHostHidden]
                    public ref class WebViewerPage sealed
                    {
                    public:
                                    WebViewerPage();
    
                                    /// <summary>
                                    /// This can be changed to a strongly typed view model.
                                    /// </summary>
                                    property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel
                                    {
                                                    WFC::IObservableMap<Platform::String^, Platform::Object^>^  get();
                                    }
    
                                    /// <summary>
                                    /// NavigationHelper is used on each page to aid in navigation and 
                                    /// process lifetime management
                                    /// </summary>
                                    property Common::NavigationHelper^ NavigationHelper
                                    {
                                                    Common::NavigationHelper^ get();
                                    }
    
                    protected:
                                    virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override;
                                    virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override;
    
                    private:
                                    void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e);
                                    void SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e);
    
                                    static Windows::UI::Xaml::DependencyProperty^ _defaultViewModelProperty;
                                    static Windows::UI::Xaml::DependencyProperty^ _navigationHelperProperty;
    
    
                    };
    }
    

    Файл WebViewerPage.xaml.cpp должен начинаться так:

    //
    // WebViewerPage.xaml.cpp
    // Implementation of the WebViewerPage class
    //
    
    #include "pch.h"
    #include "WebViewerPage.xaml.h"
    
    using namespace SimpleBlogReader;
    using namespace concurrency;
    
    using namespace Platform;
    using namespace Platform::Collections;
    using namespace Windows::Foundation;
    using namespace Windows::Foundation::Collections;
    
    using namespace Windows::UI::Xaml;
    using namespace Windows::UI::Xaml::Controls;
    using namespace Windows::UI::Xaml::Controls::Primitives;
    using namespace Windows::UI::Xaml::Data;
    using namespace Windows::UI::Xaml::Input;
    using namespace Windows::UI::Xaml::Interop;
    using namespace Windows::UI::Xaml::Media;
    using namespace Windows::UI::Xaml::Media::Animation;
    using namespace Windows::UI::Xaml::Navigation;
    
    // The Basic Page item template is documented at 
    // https://go.microsoft.com/fwlink/?LinkId=234237
    
    WebViewerPage::WebViewerPage()
    {
        InitializeComponent();
        SetValue(_defaultViewModelProperty, ref new Map<String^, Object^>(std::less<String^>()));
        auto navigationHelper = ref new Common::NavigationHelper(this);
        SetValue(_navigationHelperProperty, navigationHelper);
        navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &WebViewerPage::LoadState);
        navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &WebViewerPage::SaveState);
    }
    
    DependencyProperty^ WebViewerPage::_defaultViewModelProperty =
    DependencyProperty::Register("DefaultViewModel",
    TypeName(IObservableMap<String^, Object^>::typeid), TypeName(WebViewerPage::typeid), nullptr);
    
    /// <summary>
    /// used as a trivial view model.
    /// </summary>
    IObservableMap<String^, Object^>^ WebViewerPage::DefaultViewModel::get()
    {
        return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty));
    }
    
    DependencyProperty^ WebViewerPage::_navigationHelperProperty =
    DependencyProperty::Register("NavigationHelper",
    TypeName(Common::NavigationHelper::typeid), TypeName(WebViewerPage::typeid), nullptr);
    
    /// <summary>
    /// Gets an implementation of <see cref="NavigationHelper"/> designed to be
    /// used as a trivial view model.
    /// </summary>
    Common::NavigationHelper^ WebViewerPage::NavigationHelper::get()
    {
        return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty));
    }
    
    #pragma region Navigation support
    
    /// The methods provided in this section are simply used to allow
    /// NavigationHelper to respond to the page's navigation methods.
    /// 
    /// Page specific logic should be placed in event handlers for the  
    /// <see cref="NavigationHelper::LoadState"/>
    /// and <see cref="NavigationHelper::SaveState"/>.
    /// The navigation parameter is available in the LoadState method 
    /// in addition to page state preserved during an earlier session.
    
    void WebViewerPage::OnNavigatedTo(NavigationEventArgs^ e)
    {
        NavigationHelper->OnNavigatedTo(e);
    }
    
    void WebViewerPage::OnNavigatedFrom(NavigationEventArgs^ e)
    {
        NavigationHelper->OnNavigatedFrom(e);
    }
    
    #pragma endregion
    
    
  2. В файле WebViewerPage.xaml.h добавьте указанную ниже переменную частного члена.

    Windows::Foundation::Uri^ m_feedItemUri;
    
  3. В файле WebViewerPage.xaml.cpp замените LoadState и SaveState следующим кодом:

    void WebViewerPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e)
    {
        (void)sender;   // Unused parameter
        // Run the PopInThemeAnimation. 
        Storyboard^ sb = dynamic_cast<Storyboard^>(this->FindName("PopInStoryboard"));
        if (sb != nullptr)
        {
            sb->Begin();
        }
    
        if (e->PageState == nullptr)
        {
            m_feedItemUri = safe_cast<String^>(e->NavigationParameter);
            contentView->Navigate(ref new Uri(m_feedItemUri));
        }
        // We are resuming from suspension:
        else
        {
            m_feedItemUri = safe_cast<String^>(e->PageState->Lookup("FeedItemUri"));
            contentView->Navigate(ref new Uri(m_feedItemUri));
        }
    }
    
    void WebViewerPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e)
    {
        (void)sender;   // Unused parameter
        (void)e; // Unused parameter
        e->PageState->Insert("FeedItemUri", m_feedItemUri);
    }
    

    Обратите внимание на ничем не оправданную анимацию в начале функции. Дополнительную информацию об этой анимации см. в Центре разработчиков для Windows. Обратите внимание, что здесь снова потребуется иметь дело с двумя возможными способами, которыми мы можем перейти на эту страницу. Если мы активируем приложение, нам потребуется определить наше состояние.

Вот и все! Нажмите F5. Теперь вы можете переходить от TextViewerPage к WebViewerPage!

Теперь вернитесь в проект для Windows. Эта будет очень похоже на то, что мы делали для телефона.

Hh465045.wedge(ru-ru,WIN.10).gifДобавьте XAML (WebViewerPage приложения для Windows)

  1. В файле WebViewerPage.xaml добавьте событие SizeChanged к элементу Page и назовите его pageRoot_SizeChanged. Поместите на него точку вставки и нажмите клавишу F12, чтобы создать код программной части.

  2. Найдите сетку "Кнопка «Назад» и заголовок страницы" и удалите элемент TextBlock. Заголовок страницы будет отображаться на веб-странице, поэтому нам не нужно, чтобы он занимал тут место.

  3. А теперь, сразу же под этой сеткой кнопки "Назад", добавьте Border с элементом управления WebView:

    <Border x:Name="contentViewBorder" BorderBrush="Gray" BorderThickness="2" 
                    Grid.Row="1" Margin="20,20,20,20">
                <WebView x:Name="contentView" ScrollViewer.HorizontalScrollMode="Enabled"
                         ScrollViewer.VerticalScrollMode="Enabled"/>
            </Border> 
    

    Элемент управления WebView выполняет основную часть работы бесплатно, но имеет свои совместимости, которые отличают его в некоторой степени от других элементов управления XAML. Вам определенно стоит почитать о нем подробно, если вы собираетесь широко использовать его в приложении.

Hh465045.wedge(ru-ru,WIN.10).gifДобавьте переменную-член

  1. Добавьте следующее частное объявление в WebViewerPage.xaml.h:

    Platform::String^ m_feedItemUri;
    

Hh465045.wedge(ru-ru,WIN.10).gifLoadState и SaveState (WebViewerPage приложения для Windows)

  1. Замените функции LoadState и SaveState указанным ниже кодом, который очень похож на код для страницы телефона.

    void WebViewerPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e)
    {
        (void)sender;   // Unused parameter
    
        // Run the PopInThemeAnimation. 
        auto sb = dynamic_cast<Storyboard^>(this->FindName("PopInStoryboard"));
        if (sb != nullptr)
        {
            sb->Begin();
        }
    
        // We are navigating forward from SplitPage
        if (e->PageState == nullptr)
        {
            m_feedItemUri = safe_cast<String^>(e->NavigationParameter);
            contentView->Navigate(ref new Uri(m_feedItemUri));
        }
    
        // We are resuming from suspension:
        else
        {
            contentView->Navigate(
                ref new Uri(safe_cast<String^>(e->PageState->Lookup("FeedItemUri")))
                );
        }
    }
    
    void WebViewerPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e)
    {
        (void)sender;   // Unused parameter
    
        // Store the info needed to reconstruct the page on back navigation,
        // or in case we are terminated.
        e->PageState->Insert("FeedItemUri", m_feedItemUri);
    }
    

    \

  2. В качестве стартового проекта укажите проект Windows и нажмите клавишу F5. При выборе ссылки на странице TextViewerPage вы должны перейти на страницу WebViewerPage, а при нажатии кнопки "Назад" на странице WebViewerPage вы должны вернуться на страницу TextViewerPage.

Часть 10. Добавление и удаление веб-каналов

Приложение теперь работает отлично и в Windows и в телефоне, если исходить из того, что пользователь никогда не захочет почитать ничего, кроме трех канало, которые мы жестко задали в нем. Но на последнем этапе давайте будем реалистами и разрешим пользователю добавлять и удалять веб-каналы по своему выбору. Мы покажем им несколько веб-каналов по умолчанию так, чтобы экран не был пустым при первом запуске приложения. Затем мы добавим кнопки, чтобы дать им возможность добавлять и удалять веб-каналы. Конечно, нам придется сохранить список каналов пользователя, чтобы они не изменялись от сеанса к сеансу. Этот подходящее время, чтобы подробнее узнать о локальных данных приложения.

Начнем с того, что нам все-таки потребуется сохранить несколько веб-каналов по умолчанию при первом запуске приложения. Но вместо сложного программирования этого действия можно вставить их в строковый файл ресурсов, в котором ResourceLoader сможет их найти. Нам необходимо, чтобы эти ресурсы компилировались в приложении для Windows и в приложении для телефона, поэтому мы создаем файл .resw в общем проекте.

Hh465045.wedge(ru-ru,WIN.10).gifДобавьте строковые ресурсы

  1. В обозревателе решений выберите общий проект, щелкните правой кнопкой мыши и добавьте новый элемент. На левой панели выберите Ресурс, а затем в средней панели выберите Файл ресурсов (.resw). (Не выбирайте файл .rc, так как он предназначен для классических приложений). Оставьте имя по умолчанию или присвойте ему любое имя. Затем щелкните на "Добавить".

  2. Добавьте следующие пары имя-значение.

    Редактор ресурсов должен выглядеть так по завершении.

    Строковые ресурсы

Hh465045.wedge(ru-ru,WIN.10).gifДобавьте общий код для добавления и удаления веб-каналов

  1. Мы добавим код для загрузки URL-адресов классу FeedDataSource. В файле feeddata.h добавьте эту частную функцию-член к FeedDataSource:

    concurrency::task<Windows::Foundation::Collections::IVector<Platform::String^>^> GetUserURLsAsync();
    
  2. Добавьте эти операторы в FeedData.cpp

    using namespace Windows::Storage;
    using namespace Windows::Storage::Streams;
    
  3. А затем добавьте реализацию:

    /// <summary>
    /// The first time the app runs, the default feed URLs are loaded from the local resources
    /// into a text file that is stored in the app folder. All subsequent additions and lookups 
    /// are against that file. The method has to return a task because the file access is an 
    /// async operation, and the call site needs to be able to continue from it with a .then method.
    /// </summary>
    
    task<IVector<String^>^> FeedDataSource::GetUserURLsAsync()
    {
    
        return create_task(ApplicationData::Current->LocalFolder->
            CreateFileAsync("Feeds.txt", CreationCollisionOption::OpenIfExists))
            .then([](StorageFile^ file)
        {
            return FileIO::ReadLinesAsync(file);
        }).then([](IVector<String^>^ t)
        {
            if (t->Size == 0)
            {
                // The data file is new, so we'll populate it with the 
                // default URLs that are stored in the apps resources.
                auto loader = ref new Resources::ResourceLoader();
    
                t->Append(loader->GetString("URL_1\n"));
                t->Append(loader->GetString("URL_2"));
                t->Append(loader->GetString("URL_3"));
    
                // Before we return the URLs, let's create the new file asynchronously 
                //  for use next time. We don't need the result of the operation now 
                // because we already have vec, so we can just kick off the task to
                // run whenever it gets scheduled.
                create_task(ApplicationData::Current->LocalFolder->
                    CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists))
                    .then([t](StorageFile^ file)
                {
                    OutputDebugString(L"append lines async\n");
                    FileIO::AppendLinesAsync(file, t);
                });
            }
    
            // Return the URLs
            return create_task([t]()
            {
                OutputDebugString(L"returning t\n");
                return safe_cast<IVector<String^>^>(t);
            });
        });
    }
    

    GetUserURLsAsync проверит, существует ли файл feeds.txt. Если нет, создает его и добавляет URL-адреса из строковых ресурсов. Все файлы, которые добавляет пользователь, будут записываться в файл feeds.txt. Поскольку все операции записи файлов являются асинхронными, мы используем задачу и продолжение .then, чтобы убедиться в том, что асинхронная работа выполнена до того, как мы попытаемся получить доступ к данным файла.

  4. Теперь замените старую реализация InitDataSource следующим кодом, который вызывает GetUerURLsAsync:

    ///<summary>
    /// Retrieve the data for each atom or rss feed and put it into our custom data structures.
    ///</summary>
    void FeedDataSource::InitDataSource()
    {
        auto urls = GetUserURLsAsync()
            .then([this](IVector<String^>^ urls)
        {
            // Populate the list of feeds.
            SyndicationClient^ client = ref new SyndicationClient();
            for (auto url : urls)
            {
                RetrieveFeedAndInitData(url, client);
            }
        });
    }
    
  5. Функции для добавления и удаления каналов одинаковы в Windows и в телефоне, поэтому мы поместим в класс App. В файле App.xaml.h

  6. Добавьте эти внутренние члены:

    void AddFeed(Platform::String^ feedUri);
    void RemoveFeed(Platform::String^ feedUri);
    
  7. В файле App.xaml.cpp добавьте это пространство имен:

    using namespace Platform::Collections;
    
  8. В файле App.xaml.cpp:

    void App::AddFeed(String^ feedUri)
    {
        auto feedDataSource = 
            safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource"));
        auto client = ref new Windows::Web::Syndication::SyndicationClient();
    
        // The UI is data-bound to the items collection and will update automatically
        // after we append to the collection.
        feedDataSource->RetrieveFeedAndInitData(feedUri, client);
    
        // Add the uri to the roaming data. The API requires an IIterable so we have to 
        // put the uri in a Vector.
        Vector<String^>^ vec = ref new Vector<String^>();
        vec->Append(feedUri);
        concurrency::create_task(ApplicationData::Current->LocalFolder->
            CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists))
            .then([vec](StorageFile^ file)
        {
            FileIO::AppendLinesAsync(file, vec);
        });
    }
    void App::RemoveFeed(Platform::String^ feedTitle)
    {
        // Create a new list of feeds, excluding the one the user selected.
        auto feedDataSource = 
            safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource"));
        int feedListIndex = -1;
        Vector<String^>^  newFeeds = ref new Vector<String^>();
        for (unsigned int i = 0; i < feedDataSource->Feeds->Size; ++i)
        {
            if (feedDataSource->Feeds->GetAt(i)->Title == feedTitle)
            {
                feedListIndex = i;
            }
            else
            {
                newFeeds->Append(feedDataSource->Feeds->GetAt(i)->Uri);
            }
        }
    
        // Delete the selected item from the list view and the Feeds collection.
        feedDataSource->Feeds->RemoveAt(feedListIndex);
    
        // Overwrite the old data file with the new list.
        create_task(ApplicationData::Current->LocalFolder->
            CreateFileAsync("feeds.txt", CreationCollisionOption::OpenIfExists))
            .then([newFeeds](StorageFile^ file)
        {
            FileIO::WriteLinesAsync(file, newFeeds);
        });
    }
    

Hh465045.wedge(ru-ru,WIN.10).gifДобавьте разметку XAML для кнопок добавления и удаления (Windows 8.1)

  1. Кнопки для добавления и удаления каналов должны располагаться на MainPage. Мы положим кнопки в TopAppBar в приложении для Windows и BottomAppBar в приложении для телефона (у приложений для телефона нет верхних панелей приложения). В проекте для Windows в файле MainPage.xaml добавьте TopAppBar сразу после узла Page.Resources:

    <Page.TopAppBar>
            <CommandBar x:Name="cmdBar" IsSticky="False" Padding="10,0,10,0">
    
                <AppBarButton x:Name="addButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Icon="Add">
                    <Button.Flyout>
                        <Flyout Placement="Top">
                            <Grid>
                                <StackPanel>
                                    <TextBox x:Name="tbNewFeed" Width="400"/>
                                    <Button Click="AddFeed_Click">Add feed</Button>
                                </StackPanel>
                            </Grid>
                        </Flyout>
                    </Button.Flyout>
                </AppBarButton>
    
                <AppBarButton x:Name="removeButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Icon="Remove"
                              Click="removeFeed_Click"/>
    
                <!--These buttons appear when the user clicks the remove button to 
                signal that they want to remove a feed. Delete removes the feed(s)  
                and returns to the normal visual state and cancel just returns 
                to the normal state. -->
                <AppBarButton x:Name="deleteButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Visibility="Collapsed"
                              Icon="Delete" Click="deleteButton_Click"/>
    
                <AppBarButton x:Name="cancelButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Visibility="Collapsed"
                              Icon="Cancel"
                              Click="cancelButton_Click"/>
            </CommandBar>
        </Page.TopAppBar>
    
  2. В каждом из четырех имен обработчиков события нажатия (add, remove, delete, cancel), установите курсор на имени обработчика и нажмите клавишу F12 для компиляции функций в коде программной части.

  3. Добавьте вторую VisualStateGroup в элемент <VisualStateManager.VisualStateGroups>:

    <VisualStateGroup x:Name="SelectionStates">
        <VisualState x:Name="Normal"/>
            <VisualState x:Name="Checkboxes">
                <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" 
                            Storyboard.TargetProperty="SelectionMode">
                        <DiscreteObjectKeyFrame KeyTime="0" Value="Multiple"/>
                    </ObjectAnimationUsingKeyFrames>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemListView" 
                            Storyboard.TargetProperty="IsItemClickEnabled">
                        <DiscreteObjectKeyFrame KeyTime="0" Value="False"/>
                    </ObjectAnimationUsingKeyFrames>                  
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="cmdBar" 
                             Storyboard.TargetProperty="IsSticky">
                         <DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
                    </ObjectAnimationUsingKeyFrames>
                 </Storyboard>
          </VisualState>
    </VisualStateGroup>
    

Hh465045.wedge(ru-ru,WIN.10).gifДобавьте обработчики событий добавления и удаления элементов веб-каналов (Windows 8.1):

  • В файле MainPage.xaml.cpp замените четыре заглушки обработчика событий следующим кодом:

    /// <summary>
    /// Invoked when the user clicks the "add" button to add a new feed.  
    /// Retrieves the feed data, updates the UI, adds the feed to the ListView
    /// and appends it to the data file.
    /// </summary>
    void MainPage::AddFeed_Click(Object^ sender, RoutedEventArgs^ e)
    {
        auto app = safe_cast<App^>(App::Current);
        app->AddFeed(tbNewFeed->Text);
    }
    
    /// <summary>
    /// Invoked when the user clicks the remove button. This changes the grid or list
    ///  to multi-select so that clicking on an item adds a check mark to it without 
    /// any navigation action. This method also makes the "delete" and  "cancel" buttons
    /// visible so that the user can delete selected items, or cancel the operation.
    /// </summary>
    void MainPage::removeFeed_Click(Object^ sender, RoutedEventArgs^ e)
    {
        VisualStateManager::GoToState(this, "Checkboxes", false);
        removeButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
        addButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
        deleteButton->Visibility = Windows::UI::Xaml::Visibility::Visible;
        cancelButton->Visibility = Windows::UI::Xaml::Visibility::Visible;
    }
    
    ///<summary>
    /// Invoked when the user presses the "trash can" delete button on the app bar.
    ///</summary>
    void SimpleBlogReader::MainPage::deleteButton_Click(Object^ sender, RoutedEventArgs^ e)
    {
    
        // Determine whether listview or gridview is active
        IVector<Object^>^ itemsToDelete;
        if (itemListView->ActualHeight > 0)
        {
            itemsToDelete = itemListView->SelectedItems;
        }
        else
        {
            itemsToDelete = itemGridView->SelectedItems;
        }
    
        for (auto item : itemsToDelete)
        {       
            // Get the feed the user selected.
            Object^ proxy = safe_cast<Object^>(item);
            FeedData^ item = safe_cast<FeedData^>(proxy);
    
            // Remove it from the data file and app-wide feed collection
            auto app = safe_cast<App^>(App::Current);
            app->RemoveFeed(item->Title);
        }
    
        VisualStateManager::GoToState(this, "Normal", false);
        removeButton->Visibility = Windows::UI::Xaml::Visibility::Visible;
        addButton->Visibility = Windows::UI::Xaml::Visibility::Visible;
        deleteButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
        cancelButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
    }
    
    ///<summary>
    /// Invoked when the user presses the "X" cancel button on the app bar. Returns the app 
    /// to the state where clicking on an item causes navigation to the feed.
    ///</summary>
    void MainPage::cancelButton_Click(Object^ sender, RoutedEventArgs^ e)
    {
        VisualStateManager::GoToState(this, "Normal", false);
        removeButton->Visibility = Windows::UI::Xaml::Visibility::Visible;
        addButton->Visibility = Windows::UI::Xaml::Visibility::Visible;
        deleteButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
        cancelButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
    }
    

    Нажмите F5 с проектом для Windows как запускаемым проектом. Можно заметить, что каждая из этих функций-членов устанавливает свойству видимости на кнопках соответствующее значение, а затем переходит к обычному визуальному состоянию.

Hh465045.wedge(ru-ru,WIN.10).gifДобавьте разметку XAML для кнопок добавления и удаления (Windows Phone 8.1)

  1. Добавьте нижнюю панель приложения с кнопками после узла Page.Resources:

     <Page.BottomAppBar>
    
            <CommandBar x:Name="cmdBar" Padding="10,0,10,0">
    
                <AppBarButton x:Name="addButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Icon="Add"
                              >
                    <Button.Flyout>
                        <Flyout Placement="Top">
                            <Grid Background="Black">
                                <StackPanel>
                                    <TextBox x:Name="tbNewFeed" Width="400"/>
                                    <Button Click="AddFeed_Click">Add feed</Button>
                                </StackPanel>
                            </Grid>
                        </Flyout>
                    </Button.Flyout>
    
                </AppBarButton>
                <AppBarButton x:Name="removeButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Icon="Remove"
                              Click="removeFeed_Click"/>
    
    
                <!--These buttons appear when the user clicks the remove button to 
                signal that they want to remove a feed. Delete removes the feed(s)  
                and returns to the normal visual state. Cancel just returns to the normal state. -->
                <AppBarButton x:Name="deleteButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Visibility="Collapsed"
                              Icon="Delete" Click="deleteButton_Click"/>
    
    
                <AppBarButton x:Name="cancelButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Visibility="Collapsed"
                              Icon="Cancel"
                              Click="cancelButton_Click"/>
            </CommandBar>
        </Page.BottomAppBar>
    
  2. Нажмите F12 на каждом из имен события нажатия для создания кода программной части.

  3. Добавьте "Флажки" VisualStateGroup, чтобы весь узел VisualStateGroups выглядел следующим образом:

    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="SelectionStates">
            <VisualState x:Name="Normal"/>
            <VisualState x:Name="Checkboxes">
                <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemListView" 
                                Storyboard.TargetProperty="SelectionMode">
                        <DiscreteObjectKeyFrame KeyTime="0" Value="Multiple"/>
                    </ObjectAnimationUsingKeyFrames>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemListView" 
                                Storyboard.TargetProperty="IsItemClickEnabled">
                        <DiscreteObjectKeyFrame KeyTime="0" Value="False"/>
                    </ObjectAnimationUsingKeyFrames>
                    </ObjectAnimationUsingKeyFrames>
                </Storyboard>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    

Hh465045.wedge(ru-ru,WIN.10).gifДобавьте обработчики событий для кнопок добавления и удаления веб-каналов (Windows Phone 8.1)

  • В файле MainPage.xaml.cpp (Windows Phone 8.1) заменим обработчики заглушки, которые вы только что создали, следующим кодом:

    void MainPage::AddFeed_Click(Platform::Object^ sender, RoutedEventArgs^ e)
    {
        if (tbNewFeed->Text->Length() > 9)
        {
            auto app = static_cast<App^>(App::Current);
            app->AddFeed(tbNewFeed->Text);
        }
    }
    
    
    void MainPage::removeFeed_Click(Platform::Object^ sender, RoutedEventArgs^ e)
    {
        VisualStateManager::GoToState(this, "Checkboxes", false);
        removeButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
        addButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
        deleteButton->Visibility = Windows::UI::Xaml::Visibility::Visible;
        cancelButton->Visibility = Windows::UI::Xaml::Visibility::Visible;
    }
    
    
    void MainPage::deleteButton_Click(Platform::Object^ sender, RoutedEventArgs^ e)
    {
        for (auto item : ItemListView->SelectedItems)
        {
            // Get the feed the user selected.
            Object^ proxy = safe_cast<Object^>(item);
            FeedData^ item = safe_cast<FeedData^>(proxy);
    
            // Remove it from the data file and app-wide feed collection
            auto app = safe_cast<App^>(App::Current);
            app->RemoveFeed(item->Title);
        }
    
        VisualStateManager::GoToState(this, "Normal", false);
        removeButton->Visibility = Windows::UI::Xaml::Visibility::Visible;
        addButton->Visibility = Windows::UI::Xaml::Visibility::Visible;
        deleteButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
        cancelButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
    
    }
    
    void MainPage::cancelButton_Click(Platform::Object^ sender, RoutedEventArgs^ e)
    {
        VisualStateManager::GoToState(this, "Normal", false);
        removeButton->Visibility = Windows::UI::Xaml::Visibility::Visible;
        addButton->Visibility = Windows::UI::Xaml::Visibility::Visible;
        deleteButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
        cancelButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
    }
    

    Нажмите F5 попробуйте воспользоваться новыми кнопками, чтобы добавлять и удалять веб-каналы! Чтобы добавить канал на телефоне, нажмите на ссылку RSS на веб-странице, а затем выберите "Сохранить". Нажмите на поле ввода, которое содержит имя URL-адреса, а затем нажмите значок копирования. Вернитесь в приложение и установите точки вставки в поле ввода и нажмите значок копирования снова, чтобы вставить url-адрес. Вы должны будете увидеть, что веб-канал появился в списке каналов почти мгновенно.

    Приложение SimpleBlogReader теперь в хорошем, пригодном для использования состоянии. Оно готово к развертыванию на вашем устройстве Windows.

Чтобы развернуть приложение в собственном телефоне, его необходимо сначала зарегистрировать в соответствии с процедурой, описанной в разделе Регистрация устройства Windows Phone.

Hh465045.wedge(ru-ru,WIN.10).gifРазвертывание на разблокированном Windows Phone

  1. Создайте конечную сборку приложения.

    Сборка выпуска C++ VS 2013

  2. В главном меню выберите Проект | Магазин | Создание пакетов приложений. Вы НЕ хотите развертывать приложение в магазине в этом упражнении. Примите значения по умолчанию на следующем экране, если у вас нет причин изменить их.

  3. Если пакеты созданы успешно, вы увидите предложение запустить Комплект сертификации приложений для Windows (WACK). Возможно, вы захотите сделать это, чтобы убедиться, что приложение не имеет никаких скрытых дефектов, которые помешают его принятию магазином. Но так как мы не развертываем его в магазине, этот этап необязателен.

  4. В главном меню выберите Средства | Windows Phone 8.1 | Развертывание приложения. Запустится Мастер развертывания приложения и на первом экране Цель должно появиться сообщение "Устройство". Щелкните на кнопке Обзор, чтобы перейти в папку AppPackages в дереве проекта на том же уровне, что и папки "Debug" и "Release". Найдите последний пакет в этой папке (если их больше одного) и дважды щелкните на нем, а затем щелкните на файле appx или appxbundle внутри него.

  5. Убедитесь, что телефон подключен к компьютеру и не заблокирован экраном блокировки. Нажмите кнопку Развернуть в мастере развертывания и подождите завершения развертывания. Должно пройти всего несколько секунд, пока вы не увидите сообщение "Развертывание выполнено успешно". Найдите приложение в списке приложений в телефоне и коснитесь его, чтобы запустить.

    Примечание. Добавление новых URL-адресов может быть немного неинтуитивным при первом входе в систему. Найдите URL-адрес, который требуется добавить, а затем коснитесь ссылки. В запросе ответьте, что вы собираетесь открыть ее. Скопируйте url-адрес RSS, например http://feeds.bbci.co.uk/news/world/rss.xml, а НЕ имя временного XML-файла, которое появляется после того, как IE откроет файл. Если страница XML открывается в IE, необходимо вернуться к предыдущему экрану IE, чтобы захватить необходимый вам URL-адрес из адресной строки. После того, как вы его скопируете, вернитесь к приложению "Simple Blog Reader" и вставьте его в текстовый блок "Добавить канал", а затем нажмите кнопку "Добавить канал". Вы увидите, что полностью инициализированный канал очень быстро появится на вашей основной странице. Упражнение для читателя: реализуйте контракт отправки данных или другое средство упростить добавление новых URL-адресов для приложения SimpleBlogReader. Приятного чтения!

Что дальше?

В этом учебнике мы узнали, как использовать встроенные шаблоны страниц из Microsoft Visual Studio Express 2012 для Windows 8, чтобы создать многостраничное приложение, и как обеспечить навигацию и передачу данных между страницами. Мы научились использовать стили и шаблоны, чтобы наше приложение выглядело, как веб-сайт блогов команды разработчиков Windows. Мы также научились использовать анимации темы и панель приложения, чтобы наше приложение отвечало духу Магазина Windows. И наконец, мы научились адаптировать приложение к различным макетам и ориентациям, чтобы оно всегда выглядело наилучшим образом.

Наше приложение почти готово для отправки в Магазин Windows. Дополнительные сведения о том, как отправить приложение в Магазин Windows, см. в разделе:

Связанные разделы

Схема создания приложений среды выполнения Windows на C++