Windows Dev Center

Erstellen einer Blogleser-Store-App (C++)

Hier zeigen wir Ihnen Schritt für Schritt, wie Sie mit C++ und XAML eine Store-App entwickeln, die Sie unter Windows 8.1 oder Windows Phone 8.1 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 den Lektionen 1 bis 4 unter 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 Express 2013 mit Update 2 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:

Lernziele

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.

In diesem Lernprogramm wird nicht erläutert, wie Sie die Benutzeroberfläche mit Expression Blend erstellen. Stattdessen wird direkt die Verwendung von XAML-Markup beschrieben.

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 Apps), 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, und wählen Sie Installiert > Visual C++ > Store-Apps > Universelle Apps. Wählen Sie dann im mittleren Bereich die Vorlage Leere App (Universelle Apps). Geben Sie der Projektmappe den Namen „SimpleBlogReader“. Eine ausführliche Anleitung finden Sie unter Hello World in C++.

    Nachdem das Projekt erstellt wurde, verfügen Sie über eine Projektmappe mit drei Projekten: das Windows 8.1-Projekt, das Windows Phone 8.1-Projekt und das freigegebene Projekt. Letzteres ist eigentlich nur ein Ordner mit Dateien, die in den Kompilierungsprozess anderer Projekte einbezogen werden. Dieses Projekt erstellt keine Binärdateien. Wir arbeiten mehr oder weniger gleichzeitig an allen drei Projekten, um die Unterschiede und Parallelen zwischen den beiden Projekttypen aufzuzeigen. Wenn Sie diesem Ansatz nicht folgen möchten, können Sie auch erst alle Schritte für das eine oder das andere Projekt ausführen.

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 im Windows 8.1-Projekt 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 http://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 http://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 nicht vollständig geladen wurde.

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

  1. Fügen Sie der Datei „FeedData.h“ die FeedDataSource-Klasse 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);
            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();
            void RetrieveFeedAndInitData(Platform::String^ url, WWS::SyndicationClient^ client);
    
        internal:
            // This is used to prevent SplitPage from prematurely loading the last viewed page on resume.
            concurrency::task_completion_event<FeedData^> m_LastViewedFeedEvent;
        };
    
    
    
  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 initialize 
    /// the app's data structures.
    ///</summary>
    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.
        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!)
                // This logic is not completely robust because it doesn't address
                // the case of one of our default URLs in the resources going stale.
                SyndicationErrorStatus status = SyndicationError::GetStatus(e->HResult);
                if (status == SyndicationErrorStatus::InvalidXml) {
    
                    auto handler = ref new Windows::UI::Popups::UICommandInvokedHandler(
                        [this, url](Windows::UI::Popups::IUICommand^ command)
                    {
                        auto app = safe_cast<App^>(App::Current);
                        auto title = app->GetTitleFromUri(url);
                        app->RemoveFeed(title);
                    });
                    
                    auto msg = ref new Windows::UI::Popups::MessageDialog(
                        "An invalid XML exception was thrown in " + url + 
                        ". Please make sure to use a URI that points to a RSS or Atom feed.");
                    auto cmd = ref new Windows::UI::Popups::UICommand(
                        ref new String(L"Delete Invalid Feed"), handler, 1);
                    msg->Commands->Append(cmd);
                    msg->ShowAsync();
                }
    
                if (status == SyndicationErrorStatus::Unknown) 
                {
                    Windows::Web::WebErrorStatus webError = 
                        Windows::Web::WebError::GetStatus(e->HResult);
                    // TODO: Provide a user friendly message, if there is something 
                    // they can do to fix the problem.               
                }
            }
        }); //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"http://blogs.windows.com/windows/b/bloggingwindows/rss.aspx");
        urls->Append(L"http://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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://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 von der Windows-App als auch von 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);
    
    
    
  2. Führen Sie dann in „app.xaml.cpp“ die folgenden Schritte aus:

    
    
    ///<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:
            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. Sie sollten jetzt in der Lage sein, sowohl die Windows-App als auch die Phone-App zu erstellen und auszuführen. Zum jetzigen Zeitpunkt ist die App jedoch noch nicht sehr leistungsfähig. Zur Auswahl des Projekts, das beim Drücken von F5 ausgeführt wird, klicken Sie mit der rechten Maustaste auf den Projektknoten und wählen Als Startprojekt festlegen aus.

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

    
    #include "Common\SuspensionManager.h"
    
    
  2. Fügen Sie die folgenden namespace-Direktiven hinzu:

    
    
    using namespace concurrency;
    using namespace SimpleBlogReader::Common;
    using namespace Windows::Storage;
    
    
  3. 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.

  4. 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 und die Datenbindungen beschreibt
  • 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 der Windows-App)

Die Hauptseite der Windows-App 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 (Windows)

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

    
     <!-- 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>
    
    
    
  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 direkt nach dem ListView-Steuerelement ein, sodass sie das letzte Element im Stammraster ist (wenn sie es außerhalb des Rasters platzieren, ist es nicht funktionsfähig). 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 (Windows 8.1)

  • Wählen Sie im Fenster Dokumentgliederung (STRG+ALT+D) das Element pageRoot aus, und drücken Sie ALT+EINGABE, um den Bereich Eigenschaften anzuzeigen. Klicken Sie oben rechts auf die Ereignisschaltfläche, und führen Sie einen Bildlauf nach unten bis zum SizeChanged-Ereignis durch. Setzen Sie den Cursor in das Textfeld, und drücken Sie die EINGABETASTE, um einen Ereignishandler mit dem Standardnamen pageRoot_SizeChanged 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);
        }
    } 
    
    
    

    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. Fügen Sie in der Datei „MainPage.xaml“ (Windows Phone 8.1) im öffnenden Page-Element unmittelbar nach dem x:Name-Attribut das DataContext-Attribut hinzu:

    
    DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
    
    
  2. Während Sie sich noch in der Datei „MainPage.xaml“ (Windows Phone 8.1) befinden, fügen Sie unmittelbar nach dem öffnenden Page-Element einen Page.Resources-Knoten hinzu, der eine CollectionViewSource enthält, die an die Items-Auflistung von FeedDataSource gebunden wird:

    
    <Page.Resources>
        <!-- Collection of items displayed by this page -->
        <CollectionViewSource x:Name="itemsViewSource" Source="{Binding Items}"/>
    </Page.Resources>
    
    
    
  3. 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.

  4. 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>
    
    
    
  5. 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.

LoadState und SaveState (MainPage der Phone-App)

  1. Fügen Sie zunächst am Anfang der Datei „MainPage.xaml.cpp“ (Windows Phone 8.1) eine #include-Anweisung hinzu:
    
    #include "FeedPage.xaml.h"
    
    
  2. Ersetzen Sie in „MainPage.xamp.cpp“ die LoadState-Funktionsimplementierung durch den folgenden Code (der Ihnen von der Datei „MainPage.xaml.cpp“ des Windows Phone-Projekts vertraut sein sollte):


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


Wie schon bei der Windows-App sind für SaveState an dieser Stelle keine Schritte erforderlich.

Ereignishandler (MainPage der Phone-App)

Wenn der Benutzer auf einen Feed in MainPage klickt, navigiert die App zur FeedPage, die alle Feedbeiträge auflistet. Bei der Navigation wird der ausgewählte Feed an die FeedPage übergeben, die den Feed in der LoadState-Funktion empfängt. Darüber hinaus wird der Feed gespeichert, sodass wir ihn später bei Bedarf ansehen können. Darauf gehen wir im nächsten Schritt ausführlich ein.

  • Ersetzen Sie den leeren Ereignishandler-Stub durch den folgenden Code:

    
    void MainPage::ItemListView_ItemClick(Platform::Object^ sender, ItemClickEventArgs^ e)
    {
        // We must manually cast from Object^ to FeedData^.
        auto feedData = safe_cast<FeedData^>(e->ClickedItem);
    
        auto app = safe_cast<App^>(App::Current);
        app->SetCurrentFeed(feedData);
    
        // Navigate to FeedPage and pass the title of the selected feed.
        // FeedPage will receive this in its LoadState method in the navigationParamter.
        this->Frame->Navigate(FeedPage::typeid, feedData->Uri);
    }
    
    

Damit ist die erste Seite programmiert. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf den Knoten des WindowsPhone 8.1-Projekts, und wählen Sie Als Startprojekt festlegen aus. Drücken Sie jetzt F5, um die App im Phone-Emulator zu starten. Daraufhin sollte die Anzeige in etwa wie folgt aussehen (die Reihenfolge kann unterschiedlich sein):

Phone-Hauptseite

Die Phone-App verwendet offensichtlich denselben Startcode in FeedDataSource, auf den von „app.xaml“ verwiesen wird. Das Erscheinungsbild der Listenelemente wird durch die ListItemTemplate definiert, die der Datei „app.xaml“ im vorangehenden Schritt hinzugefügt wurde. In dieser Vorlage wird die DateBlockTemplate eingebettet, die das markante grüne Quadrat zur Anzeige des Feeddatums erzeugt. Im Vorlagencode können Sie erkennen, wie die Vorlage an die von der DateConverter-Klasse erzeugten Textwerte gebunden wird. Diese Vorlage veranschaulicht auch, wie Textelemente innerhalb eines eingeschränkten Bereichs präzise positioniert werden. Probieren Sie verschiedene Positionswerte aus, um die Darstellung der Quadrate zu verbessern.

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. Ersetzen Sie den Titelbereich durch den folgenden StackPanel:

    
            <!-- 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) in das ContentRoot-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 selben 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 eine u. U. beängstigend große Menge asynchronen Codes in „FeedData.cpp“ und „App.xaml.cpp“. Wenn wir den Code jedoch näher betrachten, stellen wir fest, dass überhaupt nicht so viel dahinter steckt. Mit diesem zusätzlichen Code müssen wir in unserer asynchronen Welt leben.

  1. Fügen Sie in „FeedPage.xaml.cpp“ (Windows Phone 8.1) 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.

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“ einen privaten Member hinzu. Wir verwenden diesen Code zum Speichern eines Verweises auf den aktuellen Feed, nachdem wir den Feed erstmalig mithilfe der GetFeedItem-Funktion nachgeschlagen haben, die wir der App-Klasse im vorherigen Schritt hinzugefügt haben.

    
    FeedItem^ m_feedItem;
    
    
  3. Fügen Sie diese private Funktion hinzu, die aufgerufen wird, wenn der Benutzer auf den Link im Rich-Text klickt:

    
    void RichTextHyperlinkClicked(Windows::UI::Xaml::Documents::Hyperlink^ link, 
             Windows::UI::Xaml::Documents::HyperlinkClickEventArgs^ args);
    
    

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. 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. Als Nächstes wechseln wir zu „SplitPage.xaml“ (Windows 8.1). 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">
    
    
  2. Wir wechseln jetzt zum TextBlock-Element pageTitle im selben Grid und ändern Binding von Title in Feed.Title.

    
    Text="{Binding Feed.Title}"
    
    
  3. 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>
    
    
    
  4. 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. Fügen Sie in „SplitPage.xaml.cpp“ die folgende using-Direktive hinzu:

    
    using namespace Windows::UI::Xaml::Documents;
    
    
    
  2. 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. Fügen Sie „WebViewerPage.xaml.h“ die folgende private Membervariable hinzu:

    
    Windows::Foundation::Uri^ m_feedItemUri;
    
    
  2. 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-Funktionen 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);
    }
    
    
    

    Die WebViewerPage beider Projekte ist so ähnlich, dass wir wahrscheinlich denselben XAML-Code für beide Seiten verwenden und im freigegebenen Projekt einfügen können. Diesen Schritt überlassen wir Ihnen.

  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:

    • URL_1 http://sxp.microsoft.com/feeds/3.0/devblogs
    • URL_2 http://blogs.windows.com/windows/b/bloggingwindows/rss.aspx
    • URL_3 http://azure.microsoft.com/blog/feed

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

 

 

Anzeigen:
© 2015 Microsoft