Freigeben über


Erstellen eines Bloglesers als universelle Windows-Plattform-App (C++)

Hier zeigen wir Ihnen Schritt für Schritt, wie Sie mit C++ und XAML eine UWP-App (Universelle Windows-Plattform) entwickeln, die Sie unter Windows 10 bereitstellen können. Die App liest Blogs aus RSS 2.0- oder Atom 1.0-Feeds.

In diesem Lernprogramm wird vorausgesetzt, dass Sie bereits mit den Konzepten vertraut sind, die in Erstellen Ihrer ersten Windows Store-App mit C++ behandelt wurden.

Wenn Sie sich die fertige Version dieser App ansehen möchten, können Sie diese auf der Website der MSDN Code Gallery herunterladen.

Für dieses Lernprogramm verwenden wir Visual Studio Community 2015 oder höher. Wenn Sie eine andere Edition von Visual Studio verwenden, können die Menübefehle leicht abweichen.

Informationen zu Lernprogrammen in anderen Programmiersprachen finden Sie unter:

Ziele

In diesem Lernprogramm wird erläutert, wie Sie eine mehrseitige Windows Store-App erstellen und wann und wie die Visual C++-Komponentenerweiterungen (C++/CX) verwendet werden können, um die Codierung für die Windows-Runtime zu vereinfachen. Außerdem erfahren Sie im Rahmen dieses Lernprogramms, wie die concurrency::task-Klasse verwendet wird, um asynchrone Windows-Runtime-APIs zu nutzen.

Die SimpleBlogReader-App verfügt über die folgenden Features:

  • Internetzugriff auf RSS- und Atom-Feeddaten
  • Anzeigen einer Liste der Feeds und Feedtitel
  • Zwei Möglichkeiten zum Lesen eines Beitrags – als einfacher Text oder als Webseite.
  • Unterstützung der Prozesslebensdauer-Verwaltung. Außerdem wird der Status korrekt gespeichert und erneut geladen, wenn der Blogleser vom System heruntergefahren wird, während eine andere Aufgabe im Vordergrund ausgeführt wird.
  • Anpassung an verschiedene Fenstergrößen und Geräteausrichtungen (Quer- oder Hochformat)
  • Hinzufügen und Entfernen von Feeds durch Benutzer

Teil 1: Einrichten des Projekts

Zunächst verwenden wir die C++-Vorlage „Leere App (Universelle Windows-App)“, um ein Projekt zu erstellen.

Hh465045.wedge(de-de,WIN.10).gifSo erstellen Sie ein neues Projekt

  • Wählen Sie in Visual Studio Datei > Neu > Projekt aus, und wählen Sie anschließend Installiert > Visual C++ > Windows > Universell aus. Wählen Sie dann im mittleren Bereich die Vorlage Leere App (Universelle Windows-App) aus. Geben Sie der Projektmappe den Namen „SimpleBlogReader“. Eine ausführliche Anleitung finden Sie unter Erstellen der App „Hello, world“ (C++).

Wir beginnen, indem wir alle Seiten hinzufügen. Es ist einfacher, die Seiten alle auf einmal hinzuzufügen, da jede Seite beim Starten der Programmierung die Seite, zu der sie navigiert, mit „#include“ laden muss.

Hh465045.wedge(de-de,WIN.10).gifHinzufügen der Windows-App-Seiten

  1. Unser erster Schritt besteht darin, eine Datei zu löschen. Klicken Sie mit der rechten Maustaste auf „MainPage.xaml“, wählen Sie Entfernen aus, und klicken Sie dann auf Löschen, um die Datei und die zugehörigen CodeBehind-Dateien dauerhaft zu löschen. Es handelt sich dabei um einen leeren Seitentyp, der nicht die erforderliche Navigationsunterstützung bietet. Klicken Sie jetzt mit der rechten Maustaste auf den Projektknoten, und wählen Sie Hinzufügen > Neues Element aus. Hinzufügen eines neuen Elements in Visual C++
  2. Wählen Sie im linken Bereich „XAML“ und im mittleren Bereich Seite „Elemente“ aus. Nennen Sie die Datei „MainPage.xaml“, und klicken Sie auf OK. Daraufhin wird ein Meldungsfeld angezeigt, in dem Sie gefragt werden, ob dem Projekt einige neue Dateien hinzufügt werden dürfen. Klicken Sie auf Ja. In unserem Startcode müssen wir auf die SuspensionManager-Klasse und die NavigationHelper-Klasse verweisen, die in diesen Dateien definiert sind und von Visual Studio in einem neuen Ordner namens „Common“ abgelegt werden.
  3. Fügen Sie eine SplitPage hinzu, und übernehmen Sie den Standardnamen.
  4. Fügen Sie eine BasicPage hinzu, und geben Sie Ihr den Namen „WebViewerPage“.

Wir werden diesen Seiten später UI-Elemente hinzufügen.

Hh465045.wedge(de-de,WIN.10).gifHinzufügen der Phone-App-Seiten

  1. Erweitern Sie im Projektmappen-Explorer das Windows Phone 8.1-Projekt. Klicken Sie mit der rechten Maustaste auf „MainPage.xaml“, und wählen Sie Entfernen > Endgültig löschen aus.
  2. Fügen Sie eine neue XAML-Standardseite hinzu, und nennen Sie sie „MainPage.xaml“. Klicken Sie wie beim Windows-Projekt auf Ja.
  3. Ihnen ist vielleicht aufgefallen, dass die Auswahl der Seitenvorlagen im Phone-Projekt eingeschränkter ist. Dies liegt daran, dass wir in dieser App nur Standardseiten verwenden. Fügen Sie drei weitere Standardseiten hinzu, und nennen Sie sie „FeedPage“, „TextViewerPage“ und „WebViewerPage“.

Teil 2: Erstellen eines Datenmodells

Die auf Visual Studio-Vorlagen basierenden Store-Apps sind lose an eine MVVM-Architektur angelehnt. In unserer App besteht das Modell aus Klassen, die Blogfeeds kapseln. Jede XAML-Seite in der App stellt eine bestimmte Datenansicht dar, und jede Seitenklasse verfügt über ein eigenes Ansichtsmodell, das einer Eigenschaft namens DefaultViewModel vom Typ Map<String^,Object^> entspricht. Diese Zuordnung speichert die Daten, an die die XAML-Steuerelemente der Seite gebunden sind. Außerdem dient sie als Datenkontext für die Seite.

Unser Modell besteht aus drei Klassen. Die FeedData-Klasse stellt den URI der obersten Ebene und die Metadaten für einen Blogfeed dar. Der Feed unter https://blogs.windows.com/windows/b/buildingapps/rss.aspx ist ein Beispiel dafür, was von der FeedData-Klasse gekapselt wird. Ein Feed enthält eine Liste von Blogbeiträgen, die wir als FeedItem-Objekte darstellen. Jeder FeedItem stellt einen Beitrag dar und enthält Titel, Inhalt, URI und sonstige Metadaten. Der Beitrag unter https://blogs.windows.com/windows/b/buildingapps/archive/2014/05/28/using-the-windows-phone-emulator-for-testing-apps-with-geofencing.aspx ist ein Beispiel für ein FeedItem. Die erste Seite unserer App entspricht einer Ansicht der Feeds, die zweite Seite einer Ansicht der FeedItems für einen einzelnen Feed, und die letzten beiden Seiten stellen unterschiedliche Ansichten eines einzelnen Beitrags bereit: als reiner Text oder als Webseite.

Die FeedDataSource-Klasse enthält eine Auflistung von FeedData-Elementen sowie Methoden zum Herunterladen der Elemente.

Zur Erinnerung:

  • FeedData enthält Informationen zum RSS- oder Atom-Feed.

  • FeedItem enthält Informationen zu einzelnen Blogbeiträgen im Feed.

  • FeedDataSource enthält Methoden zum Herunterladen der Feeds und zum Initialisieren unserer Datenklassen.

Wir definieren diese Klassen als öffentliche Referenzklassen, um die Datenbindung zu ermöglichen. Die XAML-Steuerelemente können nicht mit C++-Standardklassen interagieren. Über das Bindable-Attribut teilen wir dem XAML-Compiler mit, dass wir eine dynamische Bindung an Instanzen dieser Typen vornehmen. In einer öffentlichen Verweisklasse werden öffentliche Datenmember als Eigenschaften verfügbar gemacht. Eigenschaften ohne spezielle Logik benötigen keinen benutzerspezifischen Getter und Setter, da der Compiler Getter und Setter bereitstellt. Beachten Sie in der FeedData-Klasse, wie ein öffentlicher Auflistungstyp durch Windows::Foundation::Collections::IVector verfügbar gemacht wird. Wir verwenden die Platform::Collections::Vector-Klasse intern als den konkreten Typ, der IVector implementiert.

Sowohl Windows- als auch Windows Phone-Projekte verwenden dasselbe Datenmodell, sodass wir die Klassen in das freigegebene Projekt einfügen.

Hh465045.wedge(de-de,WIN.10).gifSo erstellen Sie benutzerdefinierte Datenklassen

  1. Klicken Sie im Projektmappen-Explorer im Kontextmenü für den Projektknoten SimpleBlogReader.Shared auf Hinzufügen > Neues Element. Wählen Sie die Option Headerdatei (.h) aus, und nennen Sie die Datei FeedData.h.

  2. Öffnen Sie FeedData.h, und fügen Sie den folgenden Code ein. Beachten Sie die #include-Direktive für „pch.h“ – dies ist unser vorkompilierter Header, in dem die Systemheader abgelegt werden, die sich nur geringfügig oder überhaupt nicht ändern. pch.h enthält standardmäßig die Datei collection.h, die für den Platform::Collections::Vector-Typ erforderlich ist, sowie die Datei „ppltasks.h“, die für „concurrency::task“ und verwandte Typen erforderlich ist. Diese Header enthalten sowohl das <string>-Element als auch das <vector>-Element, die von unserer App benötigt werden. Wir müssen die Elemente also nicht explizit einschließen.

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

    Die Klassen sind Verweisklassen, weil die Windows-Runtime-XAML-Klassen mit ihnen interagieren müssen, um Daten an die Benutzeroberfläche zu binden. Das [Bindable]-Attribut für diese Klassen ist ebenfalls für die Datenbindung erforderlich. Ohne dieses Attribut sind die Daten für den Bindungsmechanismus nicht sichtbar.

Teil 3: Herunterladen der Daten

Die FeedDataSource-Klasse enthält die Methoden zum Herunterladen der Feeds sowie einige andere Hilfsmethoden. Sie enthält auch die Auflistung der heruntergeladenen Feeds, die dem Items-Wert im DefaultViewModel der App-Hauptseite hinzugefügt werden. FeedDataSource verwendet die Windows::Web::Syndication::SyndicationClient-Klasse für den Download. Da Netzwerkvorgänge viel Zeit beanspruchen können, laufen sie asynchron ab. Nachdem ein Feed vollständig heruntergeladen wurde, wird das FeedData-Objekt initialisiert und der FeedDataSource::Feeds-Auflistung hinzugefügt. Dabei handelt es sich um eine IObservable<T>. Die Benutzeroberfläche wird also benachrichtigt, sobald ein Element hinzufügt wird, und zeigt das Element auf der Hauptseite an. Für asynchrone Vorgänge verwenden wir die concurrency::task-Klasse sowie verwandte Klassen und Methoden aus „ppltasks.h“. Die Create_task-Funktion wird als Wrapper für IAsyncOperation- und IAsyncAction-Funktionsaufrufe in der Windows-API verwendet. Die task::then-Memberfunktion wird zum Ausführen von Code verwendet, der auf die Beendigung des Tasks warten muss.

Ein nützliches App-Feature besteht darin, dass der Benutzer nicht warten muss, bis alle Feeds heruntergeladen sind. Er kann auf einen Feed klicken, sobald dieser angezeigt wird, und wechselt sofort zu einer neuen Seite, auf der alle Elemente dieses Feeds angezeigt werden. Dies ist ein Beispiel für eine schnelle und dynamische Benutzeroberfläche. Diese zeichnet sich dadurch aus, dass ein großer Teil der Aufgaben in Hintergrundthreads verlagert wird. Nachdem wir die XAML-Hauptseite hinzugefügt haben, sehen wir sie in Aktion.

Durch asynchrone Vorgänge erhöht sich jedoch auch die Komplexität – „schnell und dynamisch“ hat also auch seinen Preis. Wenn Sie die vorangehenden Lernprogramme durchgearbeitet haben, wissen Sie, dass eine inaktive App vom System beendet werden kann, um Arbeitsspeicher freizugeben. Die App wird wiederhergestellt, sobald der Benutzer wieder zur App zurückwechselt. Beim Herunterfahren unserer App werden nicht alle Feeddaten gespeichert, da dies viel Speicherplatz beanspruchen und eine Menge veralteter Daten generieren würde. Die Feeds werden bei jedem Start heruntergeladen. Das heißt aber auch, dass wir für ein Szenario vorsorgen müssen, in dem die App den Betrieb wiederaufnimmt und sofort versucht, ein FeedData-Objekt anzuzeigen, das noch nicht vollständig heruntergeladen wurde. Wir müssen dafür sorgen, dass Daten erst für die Anzeige abgerufen werden, wenn sie verfügbar sind. In diesem Fall empfiehlt sich anstelle der then-Methode ein „task_completed_event“. Dieses Ereignis verhindert Codezugriffe auf ein FeedData-Objekt, bevor das Objekt vollständig geladen wurde.

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

  1. Fügen Sie die FeedDataSource-Klasse zu „FeedData.h“ als Teil des Namespace „SimpleBlogReader“ hinzu:

        /// <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. Erstellen Sie dann eine Datei namens „FeedData.cpp“ im freigegebenen Projekt, und fügen Sie den folgenden Code ein:

    #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. Als Nächstes holen wir eine FeedDataSource-Instanz in unsere App. Fügen Sie in „app.xaml.h“ eine #include-Direktive für „FeedData.h“ hinzu, um die Typen sichtbar zu machen.

        #include "FeedData.h"
    
    • Fügen Sie dem freigegebenen Projekt in „App.xaml“ den Knoten „Application.Resources“ hinzu, und binden Sie einen Verweis auf FeedDataSource ein. Die Seite sieht nun wie folgt aus:

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

      Dieses Markup führt dazu, dass beim Starten der App ein FeedDataSource-Objekt erstellt wird und dass von jeder Seite der App auf das Objekt zugegriffen werden kann. Beim Auslösen des OnLaunched-Ereignisses ruft das App-Objekt die InitDataSource-Implementierung auf, damit die feedDataSource-Instanz den Download aller zugehörigen Daten starten kann.

      Das Projekt wird noch nicht erstellt, weil wir erst noch einige zusätzliche Klassendefinitionen hinzufügen müssen.

Teil 4: Verarbeiten der Datensynchronisierung bei der Fortsetzung der App

Beim ersten Starten der App, und während der Benutzer zwischen Seiten vor- und zurückblättert, ist keine Synchronisierung des Datenzugriffs erforderlich. Die Feeds werden erst nach ihrer Initialisierung auf der ersten Seite angezeigt. Und bevor der Benutzer nicht auf einen sichtbaren Feed geklickt hat, erfolgt auch von den anderen Seiten kein Zugriff auf die Daten. Zudem ist der gesamte Zugriff schreibgeschützt. Unsere Quelldaten werden also niemals geändert. Es gibt jedoch ein Szenario, das eine Synchronisierung erfordert: Wenn die App beendet wird, während eine auf einem bestimmten Feed basierende Seite aktiv ist, muss diese Seite erneut an die Feeddaten gebunden werden, sobald die App fortgesetzt wird. In diesem Fall kann es vorkommen, dass eine Seite auf Daten zugreift, die noch nicht vorhanden sind. Deshalb benötigen wir eine Methode, um zu erzwingen, dass die Seite auf die Daten wartet.

Mithilfe der folgenden Funktionen kann sich die App „merken“, welchen Feed sie aufgerufen hat. Durch die SetCurrentFeed-Methode wird der Feed dauerhaft in den lokalen Einstellungen gespeichert. Von dort aus kann der Feed auch abgerufen werden, wenn die App nicht mehr über genügend Arbeitsspeicher verfügt. Besonders interessant ist die GetCurrentFeedAsync-Methode. Wir müssen sicherstellen, dass wir den letzten Feed nach dem Fortsetzen der App erst erneut laden, nachdem der Feed erneut geladen wurde. Auf diesen Code werden wir später noch zurückkommen. Wir fügen den Code der App-Klasse hinzu, da wir ihn sowohl aus der Windows-App als auch aus der Phone-App aufrufen.

  1. Fügen Sie in „app.xaml.h“ die folgenden Methodensignaturen hinzu. Die interne Zugänglichkeit bedeutet, dass die Signaturen nur von anderem C++-Code im selben Namespace verwendet werden können.

        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. Fügen Sie dann in „app.xaml.cpp“ Folgendes mit den Anweisungen oben hinzu:

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

    Sie benötigen den Parallelitätsnamespace für die Aufgabe, den Namespace „Platform::Collections“ für Vektoren und den Namespace „Windows::Storage“ für ApplicationData.

    Und fügen Sie diese Zeilen unten hinzu:

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

Teil 5: Konvertieren der Daten in verwendbare Formate

Nicht alle Rohdaten liegen immer in nutzbarer Form vor. Das Veröffentlichungsdatum eines RSS- oder Atom-Feeds wird als numerischer RFC 822-Wert ausgedrückt. Wir benötigen eine Methode, um diesen Wert in ein für den Benutzer verständliches Textformat umzuwandeln. Dazu erstellen wir eine benutzerdefinierte Klasse, die IValueConverter implementiert, einen RFC833-Wert als Eingabe akzeptiert und Zeichenfolgen für jeden Datumsbestandteil ausgibt. Später nehmen wir im XAML-Code, der die Daten anzeigt, eine Bindung an die Ausgabe unserer DateConverter-Klasse anstatt an das Rohdatenformat vor.

Hh465045.wedge(de-de,WIN.10).gifHinzufügen eines Datumskonverters

  1. Erstellen Sie im freigegebenen Projekt eine neue H-Datei, und fügen Sie den folgenden Code hinzu:

    //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. Fügen Sie ihn dann mit #include in „App.xaml.h“ ein:

    #include "DateConverter.h"
    
  3. Und erstellen Sie eine Instanz davon in der Datei „App.xaml“ im Knoten „Application.Resources“:

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

Feedinhalte werden als HTML-Code und in einigen Fällen als XML-formatierter Text übermittelt. Um diesen Inhalt in einem RichTextBlock anzuzeigen, müssen wir ihn in Rich-Text umwandeln. Die folgende Klasse verwendet die Windows-Funktion „HtmlUtilities“ zur Analyse des HTML-Codes und teilt den Code anschließend mithilfe von <regex>-Funktionen in Absätze auf, aus denen wir Rich-Text-Objekte erstellen können. In diesem Szenario können wir keine Datenbindung verwenden, sodass IValueConverter von der Klasse nicht implementiert werden muss. In den Seiten, in denen die Klasse benötigt wird, erstellen wir einfach lokale Instanzen der Klasse.

Hh465045.wedge(de-de,WIN.10).gifHinzufügen eines Textkonverters

  1. Fügen Sie im freigegebenen Projekt eine neue H-Datei hinzu, nennen Sie sie „TextHelper.h“, und fügen Sie den folgenden Code hinzu:

    #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. Fügen Sie dann „TextHelper.cpp“ hinzu:

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

    Beachten Sie, dass unsere benutzerdefinierte TextHelper-Klasse einige Möglichkeiten veranschaulicht, wie Sie ISO C++ (std::map, std::regex, std::wstring) intern in einer C++/CX-App verwenden können. Wir erstellen Instanzen dieser Klasse lokal in den Seiten, die die Klasse verwenden. Wir müssen die Klasse nur einmal in „App.xaml.h“ einfügen:

    #include "TextHelper.h"
    
  3. Nun sollten Sie die App erstellen und ausführen können. Zum jetzigen Zeitpunkt ist die App jedoch noch nicht sehr leistungsfähig.

Teil 6: Starten, Anhalten und Fortsetzen der App

Das App::OnLaunched-Ereignis wird entweder ausgelöst, wenn der Benutzer die App durch Drücken oder Anklicken der App-Kachel startet oder wenn der Benutzer zur App zurücknavigiert, nachdem sie vom System beendet wurde, um Arbeitsspeicher für andere Apps freizugeben. In beiden Fällen stellen wir eine Internetverbindung her und laden die Daten als Reaktion auf dieses Ereignis erneut. Es gibt jedoch andere Aktionen, die nur in dem einen oder anderen Fall aufgerufen werden müssen. Wir können den Zustand ableiten, indem wir das rootFrame-Element zusammen mit dem an die Funktion übergebenen LaunchActivatedEventArgs-Argument überprüfen und dann die richtige Maßnahme ergreifen. Die SuspensionManager-Klasse, die automatisch mit MainPage hinzugefügt wurde, erledigt praktischerweise die meiste Arbeit beim Speichern und Wiederherstellen des App-Zustands, nachdem die App angehalten und neu gestartet wurde. Wir müssen einfach die zugehörigen Methoden aufrufen.

  1. Fügen Sie die SuspensionManager-Codedateien im Ordner „Allgemein“ zum Projekt hinzu. Fügen Sie „SuspensionManager.h“ hinzu, und kopieren Sie folgenden Code hinein:

    //
    // 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. Fügen Sie die Codedatei „SuspensionManager.cpp“ hinzu, und kopieren Sie folgenden Code hinein:

    //
    // 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. Fügen Sie in „app.xaml.cpp“ die folgende include-Direktive hinzu:

    #include "Common\SuspensionManager.h"
    
  4. Fügen Sie die Namespacedirektive hinzu:

    using namespace SimpleBlogReader::Common;
    
  5. Ersetzen Sie jetzt die vorhandene Funktion durch den folgenden Code:

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

    Beachten Sie, dass die App-Klasse im freigegebenen Projekt enthalten ist, sodass der hier programmierte Code sowohl auf Windows- als auch auf Phone-Apps ausgeführt werden kann. Dies gilt jedoch nicht, wenn das Makro WINAPI_FAMILY == WINAPI_FAMILY_PHONE_APP definiert ist.

  6. Der OnSuspending-Handler ist einfacher zu handhaben. Es wird aufgerufen, wenn die App vom System heruntergefahren wird, und nicht, wenn sie vom Benutzer geschlossen wird. Wir lassen die Arbeit einfach vom SuspensionManager erledigen. Er ruft den SaveState-Ereignishandler auf jeder App-Seite auf und serialisiert alle Objekte, die wir im PageState-Objekt der einzelnen Seiten gespeichert haben. Sobald die App fortgesetzt wird, werden die Werte in den Seiten wiederhergestellt. Sie können den Code in „SuspensionManager.cpp“ überprüfen.

    Ersetzen Sie den vorhandenen OnSuspending-Funktionstext durch folgenden Code:

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

An dieser Stelle könnten wir die App starten und die Feeddaten herunterladen, es gibt jedoch keine Möglichkeit, sie dem Benutzer anzuzeigen. Dieses Problem werden wir jetzt angehen.

Teil 7: Hinzufügen der ersten Benutzeroberflächenseite – eine Feedliste

Sobald die App geöffnet wird, möchten wir dem Benutzer eine Auflistung der obersten Ebene aller heruntergeladenen Feeds anzeigen. Der Benutzer hat die Möglichkeit, ein Auflistungselement zu drücken oder anzuklicken, um zu einem bestimmten Feed zu navigieren, der eine Auflistung der Feedelemente oder -beiträge enthält. Die Seiten wurden bereits hinzugefügt. In der Windows-App wird die Seite „Elemente“ verwendet, um bei horizontaler Ausrichtung des Geräts eine GridView und bei vertikaler Ausrichtung eine ListView anzuzeigen. Phone-Projekte enthalten keine Seite „Elemente“, sodass wir eine Standardseite verwenden, der wir eine ListView manuell hinzufügen. Die Listenansicht richtet sich automatisch entsprechend der Geräteausrichtung aus.

Auf dieser und jeder anderen Seite sind in der Regel die gleichen Standardaufgaben auszuführen:

  • Hinzufügen von XAML-Markupcode, der die Benutzeroberfläche beschreibt und die Daten bindet
  • Hinzufügen von benutzerdefiniertem Code zu den Memberfunktionen LoadState und SaveState
  • Behandeln von Ereignissen, von denen in der Regel mindestens eines über Code verfügt, der zur nächsten Seite navigiert

Wir gehen diese Aufgaben der Reihe nach erst für das Windows-Projekt durch:

Hinzufügen von XAML-Markup (MainPage)

Die Hauptseite rendert jedes FeedData-Objekt in einem GridView-Steuerelement. Um das Erscheinungsbild der Daten zu beschreiben, erstellen wir eine DataTemplate, die als XAML-Struktur zum Rendern der einzelnen Elemente eingesetzt wird. Im Hinblick auf Layout, Schriftarten, Farben usw. sind den DataTemplates keine Grenzen gesetzt. Lassen Sie Ihrer Kreativität einfach freien Lauf. Auf dieser Seite verwenden wir eine einfache Vorlage, die nach dem Rendern wie folgt aussieht:

Feedelement

  1. Ein XAML-Stil ist vergleichbar mit einer Formatvorlage in Microsoft Word. Er bietet eine einfache Möglichkeit, eine Gruppe von Eigenschaftswerten für ein XAML-Element, den „TargetType“, zu gruppieren. Ein Style-Element kann auf einem anderen Stil basieren. Das x:Key-Attribut gibt den Namen an, von dem wir Gebrauch machen, um auf einen verwendeten Stil zu verweisen.

    Legen Sie diese Vorlage und die unterstützenden Stile im Knoten „Page.Resources“ der Datei „MainPage.xaml“ (Windows 8.1) ab. Diese werden nur in „MainPage“ verwendet.

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

    Sie sehen eine rote Wellenlinie unter GreenBlockBackgroundBrush, auf die wir ein paar Schritte später eingehen werden.

  2. Löschen Sie in „MainPage.xaml“ (Windows 8.1) das seitenlokale AppName-Element, sodass das globale, im App-Bereich hinzugefügte Element nicht ausgeblendet wird.

  3. Fügen Sie dem Knoten „Page.Resources“ ein CollectionViewSource-Element hinzu. Dieses Objekt verbindet unsere ListView mit dem Datenmodell:

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

    Beachten Sie, dass das Page-Element bereits über ein DataContext-Attribut verfügt, das für die MainPage-Klasse auf die DefaultViewModel-Eigenschaft festgelegt wurde. Diese Eigenschaft wird als FeedDataSource festgelegt, sodass CollectionViewSource nach einer Items-Auflistung sucht und diese auch findet.

  4. In „App.xaml“ fügen wir zusammen mit einigen zusätzlichen Ressourcen, auf die von mehreren App-Seiten verwiesen wird, eine globale Ressourcenzeichenfolge für den App-Namen hinzu. Indem wir Ressourcen hier hinzufügen, müssen wir sie nicht separat auf jeder Seite definieren. Fügen Sie diese Elemente dem Knoten „Resources“ in „App.xaml“ hinzu:

            <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 zeigt eine Feedliste an. Wenn das Gerät im Querformat ausgerichtet ist, verwenden wir eine GridView, die den horizontalen Bildlauf unterstützt. Im Hochformat verwenden wir eine ListView, die den vertikalen Bildlauf unterstützt. Der Benutzer soll in der Lage sein, die App mit beiden Ausrichtungen zu verwenden. Es ist relativ einfach, Unterstützung für eine geänderte Ausrichtung zu implementieren:

  • Fügen Sie der Seite beide Steuerelemente hinzu, und legen Sie ItemSource auf dieselbe CollectionViewSource fest. Legen Sie die Visibility-Eigenschaft für ListView auf Collapsed fest, sodass die Liste standardmäßig nicht sichtbar ist.
  • Erstellen Sie einen Satz von zwei VisualState-Objekten, von denen eines das Verhalten der Benutzeroberfläche für das Querformat und das andere das Verhalten für das Hochformat beschreibt.
  • Behandeln Sie das Window::SizeChanged-Ereignis, das ausgelöst wird, wenn sich die Ausrichtung ändert oder der Benutzer das Fenster verengt oder erweitert. Überprüfen Sie die Höhe und Breite des neuen Formats. Wenn das Format höher als breit ist, rufen Sie VisualState für das Hochformat auf. Rufen Sie andernfalls den Zustand für das Querformat auf.

Hh465045.wedge(de-de,WIN.10).gifHinzufügen von GridView und ListView

  1. Fügen Sie in „MainPage.xaml“ GridView, ListView und das Raster ein, das die Zurück-Schaltfläche und den Seitentitel enthält:

     <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. Beachten Sie, dass beide Steuerelemente die gleiche Memberfunktion für das ItemClick-Ereignis verwenden. Platzieren Sie die Einfügemarke auf einem der Steuerelemente, und drücken Sie F12, um den Ereignishandler-Stub automatisch zu generieren. Den zugehörigen Code fügen wir später hinzu.

  3. Fügen Sie die VisualStateGroups-Definition ein, sodass sie das letzte Element im Stammraster ist (wenn Sie es außerhalb des Rasters platzieren, funktioniert es nicht). Beachten Sie, dass es zwei Zustände gibt, von denen jedoch nur einer explizit definiert ist. Aus diesem Grund wurde der DefaultLayout-Zustand bereits im XAML-Code für diese Seite beschrieben.

    <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. Die Benutzeroberfläche ist jetzt vollständig definiert. Wir müssen die Seite nur noch anweisen, was beim Laden zu tun ist.

LoadState und SaveState (MainPage der Windows-App)

Die beiden primären Memberfunktionen, die wir auf jeder XAML-Seite im Auge behalten müssen, sind LoadState und (manchmal) SaveState. Wir verwenden LoadState zum Auffüllen mit Seitendaten und SaveState zum Speichern aller Daten, die erforderlich sind, um die Seite nach dem Anhalten und Fortsetzen der App wieder mit Daten aufzufüllen.

  • Ersetzen Sie die LoadState-Implementierung durch diesen Code, der die Feeddaten einfügt, die von der beim Start erstellten feedDataSource geladen wurden (bzw. immer noch geladen werden), und der die Daten in unser ViewModel für diese Seite einfügt.

    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 muss für MainPage nicht aufgerufen werden, da es für diese Seite nichts zu erinnern gibt. Es werden immer alle Feeds angezeigt.

Ereignishandler (MainPage der Windows-App)

Alle Seiten sind innerhalb eines Frames angelegt. Über diesen Frame navigieren wir zwischen den Seiten vor und zurück. Der zweite Parameter in einem Navigate-Funktionsaufruf wird verwendet, um Daten von einer Seite an eine andere zu übergeben. Alle hier übergebenen Objekte werden automatisch vom SuspensionManager gespeichert und serialisiert, sobald die App angehalten wird. Auf diese Weise können die Werte beim Fortsetzen der App wiederhergestellt werden. Vom Standard-SuspensionManager werden nur die integrierten Typen String und Guid unterstützt. Wenn Sie eine komplexere Serialisierung benötigen, können Sie einen benutzerdefinierten SuspensionManager erstellen. An dieser Stelle übergeben wir einen String-Wert, der von SplitPage für die Suche nach dem aktuellen Feed verwendet wird.

Hh465045.wedge(de-de,WIN.10).gifSo navigieren Sie bei Elementklicks

  1. Wenn der Benutzer auf ein Element im Raster klickt, ruft der Ereignishandler das Element ab, auf das geklickt wurde, legt es für den Fall, dass die App später angehalten wird, als „aktuellen Feed“ fest und navigiert dann zur nächsten Seite. Er übergibt den Feedtitel an die nächste Seite, damit diese Seite die Feeddaten nachschlagen kann. Der folgende Code wird eingefügt:

    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. Zum Kompilieren des vorangehenden Codes müssen wir am Anfang der aktuellen Datei „MainPage.xaml.cpp“ (Windows 8.1) die Anweisung „#include SplitPage.xaml.h“ einfügen:

    #include "SplitPage.xaml.h"
    

Hh465045.wedge(de-de,WIN.10).gifSo behandeln Sie das Page_SizeChanged-Ereignis

  • Fügen Sie in „MainPage.xaml“ einen Namen zum Stammelement hinzu, indem Sie x:Name="pageRoot" zu den Attributen der Stammseitenelemente hinzufügen und dann ein Attribut zu SizeChanged="pageRoot_SizeChanged" hinzufügen, um einen Ereignishandler zu erstellen. Ersetzen Sie die Handlerimplementierung in der CPP-Datei durch den folgenden Code:

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

    Fügen Sie die Deklaration dieser Funktion zur MainPage-Klasse in „MainPage.xaml.h“ hinzu.

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

    Der Code ist sehr übersichtlich. Wenn Sie die App jetzt im Simulator starten und das Gerät drehen, sehen Sie, wie sich die Benutzeroberfläche zwischen GridView und ListView ändert.

Hinzufügen von XAML (MainPage der Phone-App)

Als Nächstes bringen wir die Hauptseite der Phone-App ans Laufen. Dazu müssen wir deutlich weniger Code programmieren, weil wir den gesamten Code aus dem freigegebenen Projekt wiederverwenden. Darüber hinaus bieten Phone-Apps keine Unterstützung für GridView-Steuerelemente, weil die Bildschirme zu klein sind, um sie richtig darzustellen. Wir verwenden also ein ListView-Steuerelement, um die Ausrichtung im Querformat ohne VisualState-Änderungen automatisch anzupassen. Wir beginnen, indem wir dem Page-Element das DataContext-Attribut hinzufügen. Dieses Element wird auf einer Phone-Standardseite nicht automatisch generiert, wie dies bei einer ItemsPage oder SplitPage der Fall ist.

  1. Zum Implementieren der Seitennavigation benötigen Ihre Seiten NavigationHelper, was wiederum von RelayCommand abhängig ist. Fügen Sie das neue Element „RelayCommand.h“ hinzu, und kopieren Sie diesen Code hinein:

    //
    // 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. Fügen Sie „RelayCommand.cpp“ im Ordner „Allgemein“ hinzu, und kopieren Sie diesen Code hinein:

    //
    // 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. Fügen Sie im Ordner „Allgemein“ die Datei „NavigationHelper.h“ hinzu, und kopieren Sie diesen Code hinein:

    //
    // 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. Fügen Sie jetzt die Implementierungsdatei „NavigationHelper.cpp“ im gleichen Ordner mit folgendem Code hinzu:

    //
    // 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. Jetzt fügen Sie Code hinzu, um NavigationHelper in die Headerdatei „MainPage.xaml.h“ einzuschließen, sowie die DefaultViewModel-Eigenschaft, die später benötigt wird.

    //
    // 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. Fügen Sie in „MainPage.xaml.cpp“ die Implementierung von NavigationHelper und Stubs zum Laden und Speichern des Zustands sowie DefaultViewModel-Eigenschaften hinzu. Außerdem fügen Sie die erforderlichen using-Namespacedirektiven hinzu, sodass der Code dann folgendermaßen aussieht:

    //
    // 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. Während Sie sich noch in der Datei „MainPage.xaml“ (Windows Phone 8.1) befinden, führen Sie einen Bildlauf nach unten durch, suchen den TitlePanel-Kommentar und entfernen den gesamten StackPanel. Auf dem Smartphone benötigen wir zum Auflisten der Blogfeeds die Bildschirmfläche.

  8. Weiter unten auf der Seite sehen Sie ein Raster mit dem folgenden Kommentar: "TODO: Content should be placed within the following grid". Platzieren Sie die folgende ListView in diesem Raster:

        <!-- 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. Bewegen Sie den Mauszeiger dann über das ItemListView_ItemClick-Ereignis, und drücken Sie F12 (Gehe zu Definition). Visual Studio generiert eine leere Ereignishandlerfunktion für uns. Später fügen wir noch weiteren Code hinzu. Im Moment muss lediglich die Funktion generiert werden, damit die App kompiliert werden kann.

Teil 8: Auflisten der Beiträge und Anzeigen der Textansicht eines ausgewählten Beitrags

In diesem Teil werden der Phone-App zwei Seiten hinzugefügt: die Seite mit den Beiträgen und die Seite mit der Textversion eines ausgewählten Beitrags. In der Windows-App müssen wir nur eine einzelne Seite namens SplitPage hinzufügen, die auf der einen Seite die Liste und auf der anderen Seite den Text des ausgewählten Beitrags anzeigt. Wir beginnen mit den Phone-Seiten.

Hinzufügen von XAML-Markup (FeedPage der Phone-App)

Wir bleiben beim Phone-Projekt und arbeiten auf der FeedPage, die eine Liste der Beiträge in dem vom Benutzer ausgewählten Feed enthält.

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

  1. Fügen Sie dem Page-Element in „FeedPage.xaml“ (Windows Phone 8.1) einen Datenkontext hinzu:

    DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
    
  2. Fügen Sie dann nach dem öffnenden Page-Element das CollectionViewSource-Element hinzu:

    <Page.Resources>
        <!-- Collection of items displayed by this page -->
        <CollectionViewSource
        x:Name="itemsViewSource"
        Source="{Binding Items}"/>
    </Page.Resources>
    
  3. Fügen Sie im Grid-Element den folgenden StackPanel hinzu:

            <!-- TitlePanel -->
            <StackPanel Grid.Row="0" Margin="24,17,0,28">
                <TextBlock Text="{StaticResource AppName}" 
                           Style="{ThemeResource TitleTextBlockStyle}" 
                           Typography.Capitals="SmallCaps"/>
            </StackPanel>
    
  4. Als Nächstes fügen Sie ListView (unmittelbar nach dem öffnenden Element) im Raster ein:

                <!-- 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>
    

    Beachten Sie, dass die ItemsSource-Eigenschaft von ListView an die CollectionViewSource gebunden ist, die wiederum an unsere FeedData::Items-Eigenschaft gebunden ist, die wir in CodeBehind in die DefaultViewModel-Eigenschaft in LoadState einfügen (siehe unten).

  5. ListView verfügt über ein deklariertes ItemClick-Ereignis. Bewegen Sie den Cursor auf das Ereignis, und drücken Sie F12, um den Ereignishandler in CodeBehind zu generieren. Der Inhalt bleibt zunächst leer.

LoadState und SaveState (FeedPage der Phone-App)

In MainPage mussten wir uns nicht um das Speichern des Zustands kümmern, weil die Seite über die Internetverbindung immer vollständig neu initialisiert wird, sobald die App aus irgendeinem Grund neu gestartet wird. Die anderen Seiten müssen ihren Zustand speichern. Wird die App bei der Anzeige von FeedPage beispielsweise beendet (aus dem Speicher entladen), soll die App – wenn der Benutzer zur App zurücknavigiert – genau so aussehen, als wäre sie nie beendet worden. Wir müssen uns also merken, welcher Feed zuvor ausgewählt war. Diese Daten werden im lokalen AppData-Speicher gespeichert, wenn der Benutzer auf der MainPage darauf klickt.

Der einzige Haken besteht in der Frage, ob die Daten schon vollständig heruntergeladen wurden. Wenn wir von der MainPage über einen Mausklick zur FeedPage navigieren, können wir sicher sein, dass das ausgewählte FeedData-Objekt bereits vorhanden ist, da es andernfalls nicht in der MainPage-Liste enthalten wäre. Beim Fortsetzen der App wurde das zuletzt angezeigte FeedData-Objekt möglicherweise jedoch noch nicht vollständig geladen, wenn FeedPage versucht, eine Datenbindung vorzunehmen. Es muss also eine Möglichkeit geben, FeedPage (und andere Seiten) darüber zu informieren, wann FeedData verfügbar sind. Genau für diesen Zweck gibt es das concurrency::task_completion_event. Mithilfe dieses Ereignisses können wir das FeedData-Objekt sicher im gleichen Codepfad abrufen – ob wir die App nun fortsetzen oder von der MainPage dorthin navigieren. Auf der FeedPage rufen wir unseren Feed immer über GetCurrentFeedAsync auf. Wenn wir von MainPage zur App navigieren, war das Ereignis bereits festgelegt, als der Benutzer auf einen Feed geklickt hat. Die Methode gibt den Feed folglich sofort zurück. Wenn wir die App nach dem Anhalten fortsetzen, wird das Ereignis in der FeedDataSource::InitDataSource-Funktion festgelegt, sodass FeedPage in diesem Fall möglicherweise kurz warten muss, bis der Feed erneut geladen wurde. Warten ist jedoch besser als der Absturz der App. Dieser kleine Haken ist die Ursache für den komplizierten asynchronen Code in „FeedData.cpp“ und „App.xaml.cpp“. Wenn wir den Code jedoch näher betrachten, stellen wir fest, dass er nicht so kompliziert ist, wie es zunächst scheint.

  1. Fügen Sie in „FeedPage.xaml.cpp“ den folgenden Namespace hinzu, um Taskobjekte in den Bereich einzubinden:

    using namespace concurrency;
    
  2. Fügen Sie außerdem eine #include-Direktive für „TextViewerPage.xaml.h“ ein:

    #include "TextViewerPage.xaml.h"
    

    Die Definition der TextViewerPage-Klasse ist wie unten dargestellt für den Navigate-Aufruf erforderlich.

  3. Ersetzen Sie die LoadState-Methode durch den folgenden Code:

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

    Wenn wir von einer Seite weiter oben im Seitenstapel zur FeedPage zurücknavigieren, ist die Seite bereits initialisiert (DefaultViewModel weist also einen Wert für Feed auf), und der aktuelle Feed wurde bereits richtig festgelegt. Wenn wir jedoch von der MainPage in Vorwärtsrichtung navigieren oder die App fortsetzen, müssen wir den aktuellen Feed abrufen, um die Seite mit den richtigen Daten aufzufüllen. GetCurrentFeedAsync wartet ggf., bis die Feeddaten nach dem Fortsetzen verfügbar sind. Wir geben den use_current()-Kontext an, um den Task anzuweisen, zum Benutzeroberflächenthread zurückzukehren, bevor er auf die DefaultViewModel-Abhängigkeitseigenschaft zugreift. Hintergrundthreads können im Allgemeinen nicht direkt auf XAML-bezogene Objekte zugreifen.

    Auf dieser Seite führen wir keine Aktion für SaveState aus, weil wir unseren Zustand von der GetCurrentFeedAsync-Methode erhalten, sobald die Seite geladen wird.

  4. Fügen Sie die Deklaration von LoadState in der Headerdatei „FeedPage.xaml.h“ hinzu, fügen Sie eine include-Direktive für „Allgemein\NavigationHelper.h“ hinzu, und fügen Sie die NavigationHelper-Eigenschaft und die DefaultViewModel-Eigenschaft hinzu.

    //
    // 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. Fügen Sie die Implementierung dieser Eigenschaften in „FeedPage.xaml.cpp“ hinzu. Dies sieht nun wie folgt aus:

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

EventHandlers (FeedPage der Phone-App)

Auf FeedPage wird ein Ereignis behandelt: das ItemClick-Ereignis, das in Vorwärtsrichtung zu der Seite navigiert, auf der der Benutzer den Beitrag lesen kann. Sie haben bereits einen Stubhandler erstellt, als Sie im Ereignisnamen der XAML F12 gedrückt haben.

  1. Wir ersetzen die Implementierung jetzt durch den folgenden Code:

    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. Drücken Sie F5, um die Phone-App im Emulator zu erstellen und auszuführen. Wenn Sie ein Element von der MainPage aus auswählen, sollte die App jetzt zu FeedPage navigieren und eine Feedliste anzeigen. Der nächste Schritt besteht darin, den Text für einen ausgewählten Feed anzuzeigen.

Hh465045.wedge(de-de,WIN.10).gifHinzufügen von XAML-Markup (TextViewerPage der Phone-App)

  1. Ersetzen Sie im Phone-Projekt in „TextViewerPage.xaml“ den Titelbereich und das Inhaltsraster durch den folgenden Markupcode, der den App-Namen (unauffällig) und den Titel des aktuellen Beitrags zusammen mit einer einfachen Textdarstellung des Inhalts anzeigt:

     <!-- 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. Fügen Sie in „TextViewerPage.xaml.h“ die NavigationHelper-Eigenschaft und die DefaultViewItems-Eigenschaft hinzu. Fügen Sie außerdem den privaten Member „m_FeedItem“ zum Speichern eines Verweises auf das aktuelle Feedelement hinzu. Dies erfolgt, nachdem es zum ersten Mal mit der GetFeedItem-Funktion nachgeschlagen wurde, die im vorherigen Schritt der App-Klasse hinzugefügt wurde.

    Fügen Sie außerdem die RichTextHyperlinkClicked-Funktion hinzu. „TextViewerPage.xaml.h“ sollte nun wie folgt aussehen:

    //
    // 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(de-de,WIN.10).gifLoadState und SaveState (TextViewerPage der Phone-App)

  1. Fügen Sie in „TextViewerPage.xaml.cpp“ die folgende include-Direktive hinzu:

    #include "WebViewerPage.xaml.h"
    
  2. Fügen Sie die folgenden beiden Namespacedirektiven hinzu:

    using namespace concurrency;
    using namespace Windows::UI::Xaml::Documents;
    
  3. Fügen Sie den Code für NavigationHelper und DefaultViewModel hinzu.

    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. Ersetzen Sie dann die Implementierungen von LoadState und SaveState durch den folgenden Code:

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

    Da wir keine Bindung an RichTextBlock vornehmen können, erstellen wir den Inhalt mithilfe der TextHelper-Klasse manuell. Aus Gründen der Einfachheit verwenden wir die HtmlUtilities::ConvertToText-Funktion, die nur den Text aus dem Feed extrahiert. Zu Übungszwecken können Sie versuchen, den HTML- oder XML-Code selbst zu analysieren und die Bildlinks sowie den Text an die Blocks-Auflistung anzufügen. SyndicationClient verfügt über eine Funktion zum Analysieren von XML-Feeds. Einige Feeds verfügen über wohlgeformtes XML und andere nicht.

Hh465045.wedge(de-de,WIN.10).gifEreignishandler (TextViewerPage der Phone-App)

  1. In TextViewerPage navigieren wir mithilfe eines Hyperlink-Elements in RichText zur WebViewerPage. Das ist nicht die normale Methode zum Navigieren zwischen Seiten, scheint in diesem Fall jedoch geeignet zu sein und ermöglicht es uns, die Funktionsweise von Links nachzuvollziehen. Die Funktionssignatur wurde der Datei „TextViewerPage.xaml.h“ bereits hinzugefügt. Jetzt fügen wir die Implementierung der Datei „TextViewerPage.xaml.cpp“ hinzu:

    ///<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. Legen Sie das Phone-Projekt jetzt als Startprojekt fest, und drücken Sie F5. Sie sollten in der Lage sein, auf der Feedseite auf ein Element zu klicken und zur TextViewerPage zu navigieren, auf der der Blogbeitrag gelesen werden kann. Diese Blogs bieten allerlei interessante Informationen!

Hinzufügen von XAML (SplitPage der Windows-App)

Die Windows-App verhält sich in mancher Hinsicht anders als die Phone-App. Wir haben bereits gesehen, wie eine ItemsPage-Vorlage von der Datei „MainPage.xaml“ im Windows-Projekt verwendet wird. Diese Vorlage ist in Phone-Apps nicht verfügbar. Als Nächstes fügen wir eine SplitPage hinzu, die für die Phone-App ebenfalls nicht verfügbar ist. Wenn ein Gerät im Querformat ausgerichtet ist, verfügt die SplitPage in der Windows-App über einen rechten und linken Bereich. Wenn der Benutzer in unserer App zu der Seite navigiert, sieht er die Liste der Feedelemente im linken Bereich und eine Textdarstellung des momentan ausgewählten Feeds im rechten Bereich. Wenn das Gerät im Hochformat ausgerichtet ist, oder das Fenster keine volle Breite aufweist, verwendet die SplitPage das VisualStates-Element. Daraufhin verhält sie sich so, als würde sie aus zwei separaten Seiten bestehen. Dies wird in der Programmiersprache als „logische Seitennavigation“ bezeichnet.

  1. Wir beginnen mit dem folgenden Code. Dies ist der XAML-Code für eine einfache geteilte Seite, die die Standardvorlage in Windows 8-Projekten war.

    <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. Für die Standardseite wurden der Datenkontext und CollectionViewSource bereits festgelegt.

    Wir optimieren jetzt das titlePanel-Raster, sodass es sich über zwei Spalten erstreckt. So erstreckt sich der Feedtitel über die volle Bildschirmbreite:

    <Grid x:Name="titlePanel" Grid.ColumnSpan="2">
    
  3. Wir wechseln jetzt zum TextBlock-Element pageTitle im selben Grid und ändern Binding von Title in Feed.Title.

    Text="{Binding Feed.Title}"
    
  4. Suchen Sie dann den Kommentar „Vertical scrolling item list“ (Elementliste mit vertikalem Bildlauf), und ersetzen Sie das ListView-Standardelement durch:

            <!-- 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. Der Detailbereich einer SplitPage kann beliebige Informationen enthalten. In dieser App fügen wir einen RichTextBlock ein und zeigen eine einfache Textversion des Blogbeitrags an. Wir können eine von der Windows-API bereitgestellte Hilfsfunktion verwenden, um den HTML-Code aus FeedItem zu analysieren und eine Platform::String zurückzugeben. Anschließend verwenden wir unsere eigene Hilfsklasse, um die zurückgegebene Zeichenfolge in Abschnitte zu unterteilen und Rich-Text-Elemente zu erstellen. Diese Ansicht zeigt zwar keine Bilder an, wird jedoch schnell geladen. Wenn Sie die App später erweitern möchten, können Sie eine Option hinzufügen, über die der Benutzer die Schriftart und den Schriftgrad anpassen kann.

    Suchen Sie das ScrollViewer-Element unterhalb des Kommentars „Details for selected item“ (Details für das ausgewählte Element), und löschen Sie es. Fügen Sie dann den folgenden Markupcode ein:

            <!-- 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 und SaveState (SplitPage der Windows-App)

  1. Ersetzen Sie die SplitPage-Seite, die Sie erstellt haben, durch folgenden Code.

    „SplitPage.xaml.h“ sollte wie folgt aussehen:

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

    Verwenden Sie für „SplitPage.xaml.cpp“ den folgenden Code als Ausgangspunkt. Hiermit wird eine einfache geteilte Seite implementiert und dann die Unterstützung für NavigationHelper und SuspensionManager wie auf den anderen Seiten hinzugefügt. Außerdem wird der SizeChanged-Ereignishandler wie auf einer vorherigen Seite hinzugefügt.

    //
    // 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. Fügen Sie in „SplitPage.xaml.cpp“ die folgende using-Direktive hinzu:

    using namespace Windows::UI::Xaml::Documents;
    
  3. Ersetzen Sie LoadState und SaveState dann durch den folgenden Code:

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

    Beachten Sie, dass wir die GetCurrentFeedAsync-Methode verwenden, die wir dem freigegebenen Projekt zuvor hinzugefügt haben. Ein Unterschied zwischen dieser Seite und der Phone-Seite besteht darin, dass wir das ausgewählte Elemente jetzt nachverfolgen. In SaveState fügen wir das aktuell ausgewählte Element in das PageState-Objekt ein, damit es von SuspensionManager bei Bedarf dauerhaft gespeichert werden kann und uns über das PageState-Objekt wieder zur Verfügung steht, wenn wir LoadState aufrufen. Wir benötigen diese Zeichenfolge, um das aktuelle FeedItem im aktuellen Feed nachzuschlagen.

Ereignishandler (SplitPage der Windows-App)

Wenn sich das ausgewählte Element ändert, verwendet der Detailbereich die TextHelper-Klasse zum Rendern des Texts.

  1. Fügen Sie in „SplitPage.xaml.cpp“ die folgenden #include-Direktiven hinzu:

    #include "TextHelper.h"
    #include "WebViewerPage.xaml.h"
    
  2. Ersetzen Sie den standardmäßigen Ereignishandler-Stub SelectionChanged durch den folgenden Code:

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

    Diese Funktion gibt einen Rückruf an, der an ein im Rich-Text erstelltes Hyperlink-Element übergeben wird.

  3. Fügen Sie der Datei „SplitPage.xaml.h“ die folgende private Memberfunktion hinzu:

    void RichTextHyperlinkClicked(Windows::UI::Xaml::Documents::Hyperlink^ link, 
        Windows::UI::Xaml::Documents::HyperlinkClickEventArgs^ args);
    
  4. Fügen Sie der Datei „SplitPage.xaml.cpp“ die folgende Implementierung hinzu:

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

    Diese Funktion verweist wiederum auf die nächste Seite im Navigationsstapel. Jetzt können Sie F5 drücken, um den aktualisierten Text zu sehen, während sich die Auswahl ändert. Führen Sie den Code im Simulator aus, und drehen Sie das virtuelle Gerät. Sie stellen fest, dass die Ausrichtung im Hoch- und Querformat von den VisualState-Standardobjekten wie erwartet verarbeitet wird. Klicken Sie auf den Link-Text im Blogtext, und navigieren Sie zur WebViewerPage. Natürlich wird zum jetzigen Zeitpunkt noch kein Inhalt angezeigt. Das ändert sich, nachdem wir das Phone-Projekt auf den neuesten Stand gebracht haben.

Über die Rückwärtsnavigation

Ihnen ist vielleicht aufgefallen, dass die SplitPage der Windows-App eine Schaltfläche für die Rückwärtsnavigation enthält, die Sie zur MainPage zurückbringt, ohne dass Sie zusätzlichen Code programmieren müssen. Im Phone-Projekt wird diese Funktionalität über die Hardwaretaste „Zurück“ und nicht über Softwareschaltflächen bereitgestellt. Die Navigation über die Zurück-Taste des Smartphones wird von der NavigationHelper-Klasse im Ordner „Common“ verarbeitet. Suchen Sie in Ihrer Projektmappe nach BackPressed (STRG+UMSCHALT+F), um den entsprechenden Code zu finden. Auch hier müssen Sie keine zusätzlichen Schritte ausführen. Darauf können Sie sich verlassen!

Teil 9: Hinzufügen einer Webansicht des ausgewählten Beitrags

Die letzte Seite, die wir hinzufügen, zeigt den Blogbeitrag auf seiner ursprünglichen Webseite. Der Leser legt jedoch manchmal Wert darauf, auch die Bilder anzuzeigen. Der Nachteil bei der Anzeige von Webseiten besteht darin, dass der Text auf einem Smartphonebildschirm schwer lesbar sein kann, denn nicht alle Webseiten weisen ein für mobile Geräte geeignetes Format auf. Manchmal erstrecken sich die Ränder über den Bildschirmrand hinaus und erfordern einen horizontalen Bildlauf. Unsere WebViewerPage-Seite ist relativ einfach. Wir fügen der Seite lediglich ein WebView-Steuerelement hinzu, das die Arbeit für uns erledigt. Wir beginnen mit dem Phone-Projekt:

Hh465045.wedge(de-de,WIN.10).gifHinzufügen von XAML (WebViewerPage der Phone-App)

  • Fügen Sie in der Datei „WebViewerPage.xaml“ den Titelbereich und das contentRoot-Raster hinzu:

            <!-- 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(de-de,WIN.10).gifLoadState und SaveState (WebViewerPage der Phone-App)

  1. Starten Sie WebViewerPage wie alle anderen Seiten, indem Sie Unterstützung für NavigationHelper und DefaultItems in der Datei „WebViewerPage.xaml.h“ und die Implementierung in „WebViewerPage.xaml.cpp“ bereitstellen.

    „WebViewerPage.xaml.h“ sollte wie folgt beginnen:

    //
    // 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“ sollte wie folgt beginnen:

    //
    // 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. Fügen Sie „WebViewerPage.xaml.h“ die folgende private Membervariable hinzu:

    Windows::Foundation::Uri^ m_feedItemUri;
    
  3. Ersetzen Sie LoadState und SaveState in „WebViewerPage.xaml.cpp“ durch den folgenden Code:

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

    Beachten Sie die zusätzliche Animation am Anfang der Funktion. Weitere Informationen über Animationen erhalten Sie im Windows Developer Center. Beachten Sie, dass wir auch hier die beiden Möglichkeiten berücksichtigen müssen, wie der Benutzer zu der Seite gelangt. Wenn wir die App fortsetzen, müssen wir erst den Zustand nachschlagen.

Das war's. Drücken Sie F5, und navigieren Sie von der TextViewerPage zur WebViewerPage.

Wechseln Sie jetzt zurück zum Windows-Projekt. Diese Schritte stimmen weitestgehend mit den Schritten für das Phone-Projekt überein.

Hh465045.wedge(de-de,WIN.10).gifHinzufügen von XAML (WebViewerPage der Windows-App)

  1. Fügen Sie dem Page-Element in „WebViewerPage.xaml“ ein SizeChanged-Ereignis hinzu, und nennen Sie es „pageRoot_SizeChanged“. Bewegen Sie die Einfügemarke auf das Ereignis, und drücken Sie F12, um die CodeBehind-Datei zu generieren.

  2. Suchen Sie das Raster „Schaltfläche 'Zurück' und Seitentitel“, und löschen Sie „TextBlock“. Da der Seitentitel auf der Webseite angezeigt wird, muss er hier nicht extra aufgeführt werden.

  3. Fügen Sie jetzt unmittelbar nach dem Raster für die Zurück-Schaltfläche Border mit dem WebView-Steuerelement ein:

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

    Ein WebView-Steuerelement nimmt uns eine Menge Arbeit ab, hat jedoch einige Nachteile, die es in mancher Hinsicht von anderen XAML-Steuerelementen unterscheidet. Wenn Sie in einer App häufig Gebrauch davon machen, sollten Sie sich in jedem Fall eingehend darüber informieren.

Hh465045.wedge(de-de,WIN.10).gifHinzufügen von Membervariablen

  1. Fügen Sie in „WebViewerPage.xaml.h“ die folgende private Deklaration hinzu:

    Platform::String^ m_feedItemUri;
    

Hh465045.wedge(de-de,WIN.10).gifLoadState und SaveState (WebViewerPage der Windows-App)

  1. Ersetzen Sie die LoadState-Funktion und die SaveState-Funktion durch den folgenden Code, der dem Code der Phone-Seite weitestgehend entspricht:

    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. Legen Sie das Windows-Projekt als Startprojekt fest, und drücken Sie F5. Wenn Sie auf den Link auf der TextViewerPage klicken, sollten Sie zur WebViewerPage wechseln, und wenn Sie auf die Schaltfläche „Zurück“ auf der WebViewerPage klicken, sollten Sie zur TextViewerPage zurückwechseln.

Teil 10: Hinzufügen und Entfernen von Feeds

Die App funktioniert jetzt sowohl für Windows als auch für Phone gleichermaßen gut, wenn sich der Benutzer auf das Lesen der drei hardcodierten Feeds beschränkt. Bei unserem letzten Schritt orientieren wir uns jedoch an der Praxis und ermöglichen es dem Benutzer, Feeds seiner Wahl hinzuzufügen und zu löschen. Wir zeigen einige Standardfeeds an, damit der Bildschirm beim ersten Starten der App nicht leer ist. Anschließend fügen wir einige Schaltflächen hinzu, über die der Benutzer Feeds hinzufügen und löschen kann. Natürlich müssen wir die Liste der Benutzerfeeds auch speichern, damit sie von Sitzung zu Sitzung verfügbar sind. Dies ist ein guter Zeitpunkt, um mehr über lokale Daten in der App zu erfahren.

Im ersten Schritt müssen wir einige Standardfeeds speichern, die beim ersten Starten der App angezeigt werden. Anstatt diese jedoch hart zu codieren, können wir sie in einer Zeichenfolgen-Ressourcendatei ablegen, in der sie von ResourceLoader gesucht werden. Diese Ressourcen müssen sowohl in die Windows- als auch in die Phone-App kompiliert werden. Deshalb erstellen wir die RESW-Datei im freigegebenen Projekt.

Hh465045.wedge(de-de,WIN.10).gifFügen Sie Zeichenfolgenressourcen hinzu:

  1. Wählen Sie im Projektmappen-Explorer das freigegebene Projekt aus, und klicken Sie mit der rechten Maustaste, um ein neues Element hinzuzufügen. Wählen Sie im linken Bereich „Ressource“ und im mittleren Bereich „Ressourcendatei (.resw)“ aus. (Wählen Sie nicht die RC-Datei aus, da diese für Desktop-Apps reserviert ist.) Behalten Sie den Standardnamen bei, oder geben Sie ihr einen anderen Namen. Klicken Sie dann auf „Hinzufügen“.

  2. Fügen Sie die folgenden Name-Wert-Paare hinzu:

    Sobald Sie fertig sind, sollte der Ressourcen-Editor wie folgt aussehen.

    Zeichenfolgenressourcen

Hh465045.wedge(de-de,WIN.10).gifHinzufügen von freigegebenem Code zum Hinzufügen und Entfernen von Feeds

  1. Wir fügen der FeedDataSource-Klasse den Code zum Laden der URLs hinzu. In „feeddata.h“ fügen Sie FeedDataSource die folgende private Memberfunktion hinzu:

    concurrency::task<Windows::Foundation::Collections::IVector<Platform::String^>^> GetUserURLsAsync();
    
  2. Hinzufügen dieser Anweisungen zu „FeedData.cpp“

    using namespace Windows::Storage;
    using namespace Windows::Storage::Streams;
    
  3. Fügen Sie dann die folgende Implementierung hinzu:

    /// <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 stellt fest, ob die Datei „feeds.txt“ vorhanden ist. Wenn dies nicht der Fall ist, wird die Datei erstellt und die URLs aus den Zeichenfolgenressourcen werden hinzugefügt. Alle vom Benutzer hinzugefügten Dateien werden in der Datei „feeds.txt“ gespeichert. Da alle Schreibvorgänge für Dateien asynchron ablaufen, verwenden wir einen Task und eine then-Fortsetzung, um sicherzustellen, dass die asynchronen Vorgänge abgeschlossen sind, bevor wir auf die Dateidaten zugreifen.

  4. Ersetzen Sie jetzt die alte InitDataSource-Implementierung durch den folgenden Code, durch den GetUerURLsAsync aufgerufen wird:

    ///<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. Die Funktionen zum Hinzufügen und Entfernen von Feeds sind für Windows und Phone identisch, sodass wir sie in der App-Klasse bereitstellen. In „App.xaml.h“

  6. fügen Sie die folgenden internen Member hinzu:

    void AddFeed(Platform::String^ feedUri);
    void RemoveFeed(Platform::String^ feedUri);
    
  7. In „App.xaml.cpp“ fügen Sie den folgenden Namespace hinzu:

    using namespace Platform::Collections;
    
  8. In „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(de-de,WIN.10).gifHinzufügen von XAML-Markup für die Schaltflächen „Hinzufügen“ und „Entfernen“ (Windows 8.1)

  1. Die Schaltflächen zum Hinzufügen und Entfernen von Feeds gehören zur MainPage. Wir fügen die Schaltflächen in eine TopAppBar in der Windows-App und eine BottomAppBar in der Phone-App ein (Phone-Apps verfügen über keine obere App-Leiste). Im Windows-Projekt in der Datei „MainPage.xaml“: Fügen Sie TopAppBar direkt nach dem Knoten „Page.Resources“ hinzu:

    <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. Bewegen Sie den Cursor in jedem der vier Click-Ereignishandler (Add, Remove, Delete, Cancel) auf den Handlernamen, und drücken Sie F12, um die Funktionen in CodeBehind zu generieren.

  3. Fügen Sie die zweite VisualStateGroup im <VisualStateManager.VisualStateGroups>-Element hinzu:

    <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(de-de,WIN.10).gifFügen Sie die Ereignishandler zum Hinzufügen und Entfernen von Feeds hinzu (Windows 8.1):

  • Ersetzen Sie in „MainPage.xaml.cpp“ die vier Ereignishandler-Stubs durch den folgenden Code:

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

    Drücken Sie F5 mit dem Windows-Projekt als Startprojekt. Sie stellen fest, dass jede dieser Memberfunktionen die Visibility-Eigenschaft für die Schaltflächen auf den geeigneten Wert festlegt und dann in den normalen VisualState wechselt.

Hh465045.wedge(de-de,WIN.10).gifHinzufügen von XAML-Markup für die Schaltflächen „Hinzufügen“ und „Entfernen“ (Windows Phone 8.1)

  1. Fügen Sie die untere App-Leiste mit den Schaltflächen nach dem Knoten „Page.Resources“ hinzu:

     <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. Drücken Sie für jeden Click-Ereignisnamen F12, um CodeBehind zu generieren.

  3. Fügen Sie die Kontrollkästchen VisualStateGroup hinzu, damit der gesamte VisualStateGroups-Knoten wie folgt aussieht:

    <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(de-de,WIN.10).gifHinzufügen von Ereignishandlern für die Schaltflächen zum Hinzufügen und Entfernen von Feeds (Windows Phone 8.1)

  • Ersetzen Sie in „MainPage.xaml.cpp“ (Windows Phone 8.1) die gerade erstellten Stub-Ereignishandler durch den folgenden Code:

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

    Drücken Sie F5, und versuchen Sie, mithilfe der neuen Schaltflächen Feeds hinzuzufügen oder zu entfernen. Um einen Feed für Phone hinzuzufügen, klicken Sie auf einen RSS-Link auf einer Webseite, und wählen Sie „Speichern“ aus. Klicken Sie dann auf das Bearbeitungsfeld mit dem Namen der URL, und klicken Sie auf das Kopiersymbol. Navigieren Sie zurück zu der App, bewegen Sie die Einfügemarke in das Bearbeitungsfeld, und drücken Sie erneut das Kopiersymbol, um die URL einfügen. Der Feed sollte praktisch sofort in der Feedliste angezeigt werden.

    Die SimpleBlogReader-App ist jetzt bereits in einem brauchbaren Zustand. Sie kann jetzt auf Ihrem Windows-Gerät bereitgestellt werden.

Um die App auf Ihrem eigenen Smartphone bereitzustellen, müssen Sie sie zuerst wie unter Registrieren Ihres Windows Phone-Geräts beschrieben registrieren.

Hh465045.wedge(de-de,WIN.10).gifSo stellen Sie die App auf einem entsperrten Windows Phone bereit

  1. Erstellen Sie einen Versionsbuild.

    VS 2013-Versionsbuild C++

  2. Wählen Sie im Hauptmenü Projekt | Store | App-Pakete erstellen aus. In dieser Lektion stellen Sie die App NICHT im Store bereit. Übernehmen Sie im nächsten Bildschirm die Standardwerte, sofern keine Gründe dagegen sprechen.

  3. Wenn die Pakete erfolgreich erstellt wurden, werden Sie aufgefordert, das Zertifizierungskit für Windows-Apps (WACK) auszuführen. Durch Ausführen dieses Kits können Sie sicherstellen, dass die App keine versteckten Mängel aufweist, die ihre Zulassung für den Store verhindern. Da wir die App jedoch nicht im Store bereitstellen, ist der Schritt optional.

  4. Wählen Sie im Hauptmenü Extras | Windows Phone 8.1 | Anwendungsbereitstellung aus. Der Assistent für die Anwendungsbereitstellung wird angezeigt und sollte im ersten Bildschirm unter Ziel den Eintrag „Gerät“ haben. Klicken Sie auf die Schaltfläche Durchsuchen, um zum Ordner „AppPackages“ in der Projektstruktur zu navigieren, der sich auf derselben Ebene wie die Ordner „Debug“ und „Release“ befindet. Suchen Sie das neueste Paket in diesem Ordner (falls es mehrere gibt), und doppelklicken Sie auf das Paket. Klicken Sie dann auf die APPX- oder APPXBUNDLE-Datei im Paket.

  5. Stellen Sie sicher, dass Ihr Smartphone an den Computer angeschlossen ist und kein Sperrbildschirm angezeigt wird. Klicken Sie im Assistenten auf die Schaltfläche Bereitstellen, und warten Sie, bis die Bereitstellung abgeschlossen ist. Es sollte nur wenige Sekunden dauern, bis die Nachricht „Bereitstellung erfolgreich“ angezeigt wird. Suchen Sie die App in der App-Liste des Smartphones, und tippen Sie auf den Namen, um die App auszuführen.

    Hinweis: Das Hinzufügen neuer URLs ist zu Anfang nicht unbedingt intuitiv. Suchen Sie nach einer URL, die Sie hinzufügen möchten, und tippen Sie auf den Link. Geben Sie an der Eingabeaufforderung an, dass Sie die Datei öffnen möchten. Kopieren Sie die RSS-URL (z. B. http://feeds.bbci.co.uk/news/world/rss.xml) und NICHT die temporäre XML-Datei, deren Namen von Internet Explorer nach dem Öffnen der Datei angezeigt wird. Wenn die XML-Seite in Internet Explorer geöffnet wird, müssen Sie zum vorherigen Internet Explorer-Bildschirm zurücknavigieren, um die gewünschte URL aus der Adressleiste zu kopieren. Nachdem Sie die URL kopiert haben, navigieren Sie zurück zum einfachen Blogleser, fügen ihn in den Textblock „Feed hinzufügen“ ein und drücken die Schaltfläche „Feed hinzufügen“. Der vollständig initialisierte Feed wird sehr schnell auf der Hauptseite angezeigt. Übung für den Leser: Implementieren Sie einen Freigabe-Vertrag oder ein anderes Hilfsmittel, um das Hinzufügen neuer URLs zum SimpleBlogReader zu vereinfachen. Viel Spaß beim Lesen!

Nächste Schritte

In diesem Lernprogramm haben Sie erfahren, wie Sie mit den Seitenvorlagen in Microsoft Visual Studio Express 2012 für Windows 8 eine App mit mehreren Seiten erstellen und wie die Navigation und das Übergeben von Daten zwischen Seiten funktioniert. Außerdem haben Sie gelernt, wie die App mithilfe von Stilen und Vorlagen dem Erscheinungsbild der Website mit den Windows-Teamblogs angeglichen werden kann. Darüber hinaus sind Sie jetzt mit der Verwendung von Designanimationen und einer App-Leiste vertraut und können der App den Charakter einer Windows Store-App verleihen. Außerdem wissen Sie nun, wie Sie die App an verschiedene Layouts und Ausrichtungen anpassen können, um stets eine optimale Darstellung zu gewährleisten.

Unsere App ist fast bereit für den Windows Store. Weitere Informationen zum Einreichen einer App an den Windows Store finden Sie hier:

Verwandte Themen

Roadmap für Windows-Runtime-Apps mit C++