Créer une application de plateforme Windows universelle de lecteur de blog (C++)

De A à Z, voici comment utiliser les langages C++ et XAML pour développer une application de plateforme Windows universelle (UWP) à déployer sur Windows 10. Cette application lit des blogs à partir de flux RSS 2.0 ou Atom 1.0.

Ce didacticiel suppose que vous connaissiez déjà les concepts de la leçon Créer votre première application du Windows Store en C++.

Pour étudier la version finale de cette application, vous pouvez la télécharger à partir du site web de la Galerie de code MSDN.

Pour les besoins de ce didacticiel, nous allons utiliser le logiciel Visual Studio Community 2015 ou une version ultérieure. Si vous utilisez une autre édition du logiciel Visual Studio, il se peut que les commandes de menu soient légèrement différentes.

Pour suivre des didacticiels dans d’autres langages de programmation, voir :

Objectifs

Ce didacticiel est conçu pour vous aider à comprendre comment créer une application du Windows Store comportant plusieurs pages, et comment et quand utiliser les extensions des composants Visual C++ (C++/CX) pour simplifier le travail de codage par rapport au Windows Runtime. Vous apprendrez également à utiliser la classe concurrency::task pour consommer les API Windows Runtime asynchrones.

L’application SimpleBlogReader propose les fonctionnalités suivantes :

  • accès aux flux de données RSS et Atom via Internet ;
  • affichage d’une liste des flux et titres de flux ;
  • mise à disposition de deux méthodes de lecture d’un billet : en tant que texte simple ou en tant que page web ;
  • prise en charge de la gestion de la durée de vie des processus et enregistrement et rechargement corrects de son état si le système le ferme pendant qu’une autre tâche est au premier plan ;
  • adaptation à différentes tailles de fenêtre et orientations d’appareil (Portrait ou Paysage) ;
  • possibilité pour l’utilisateur d’ajouter ou de supprimer des flux.

Partie 1 : Configuration du projet

Pour commencer, utilisons le modèle C++ appelé Application vide (Windows universelle) pour créer un projet.

Hh465045.wedge(fr-fr,WIN.10).gifPour créer un projet

  • Dans le logiciel Visual Studio, choisissez Fichier > Nouveau > Projet, sélectionnez Installé > Visual C++ > Windows > Universel. Dans le volet central, sélectionnez le modèle Application vide (Windows universelle). Donnez le nom de « SimpleBlogReader » à la solution. Pour obtenir des instructions détaillées, voir Créer une application « Hello World » (C++).

Commençons par ajouter l’ensemble des pages. Il est plus facile de les ajouter en une seule fois. En effet, au fur et à mesure que le codage se poursuit, chaque page doit #inclure la page à laquelle elle accède.

Hh465045.wedge(fr-fr,WIN.10).gifAjout de pages d’applications Windows

  1. En fait, nous allons commencer par la destruction d’un élément. Cliquez avec le bouton droit de la souris sur le fichier MainPage.xaml, puis choisissez Enlever et cliquez sur Supprimer pour supprimer définitivement le fichier et ses fichiers code-behind. Il s’agit d’un type de page vierge qui ne propose pas la prise en charge de navigation dont nous avons besoin. Maintenant, cliquez avec le bouton droit de la souris sur le nœud du projet et choisissez Ajouter > Nouvel élément. Ajout d’un nouvel élément dans Visual C++
  2. Dans le volet gauche, choisissez XAML. Dans le volet central, choisissez Page Éléments. Donnez à la page le nom MainPage.xaml et cliquez sur OK. Une zone de message s’affiche, vous demandant si vous voulez ajouter de nouveaux fichiers au projet. Cliquez sur Oui. Dans notre code de démarrage, nous devons référencer les classes SuspensionManager et NavigationHelper définies dans ces fichiers, que Visual Studio place dans un nouveau dossier commun.
  3. Ajoutez une classe SplitPage et acceptez le nom par défaut.
  4. Ajoutez un objet BasicPage et nommez-le WebViewerPage.

Nous ajouterons ultérieurement les éléments de l’interface utilisateur à ces pages.

Hh465045.wedge(fr-fr,WIN.10).gifAjout de pages d’applications Windows Phone

  1. Dans l’Explorateur de solutions, développez le projet Windows Phone 8.1. Cliquez avec le bouton droit de la souris sur le fichier MainPage.xaml, choisissez Retirer > Supprimer définitivement.
  2. Ajoutez une nouvelle page de base au format XML et nommez-la MainPage.xaml. Cliquez sur Oui comme vous l’avez fait pour le projet Windows.
  3. Vous pouvez remarquer que la variété de modèles de page est plus limitée dans le projet Windows Phone. En effet, nous utilisons uniquement les pages de base de cette application. Ajoutez trois pages de base supplémentaires, en leur donnant les noms suivants : FeedPage, TextViewerPage et WebViewerPage.

Partie 2 : Création d’un modèle de données

Les applications du Windows Store basées sur des modèles Visual Studio reposent plus ou moins à une architecture de type MVVM (Model-View-ViewModel). Dans notre application, le modèle est composé de classes qui encapsulent des flux de blogs. Chaque page XAML de l’application représente une vue particulière de ces données, et chaque classe de page a son propre modèle d’affichage, qui correspond à une propriété appelée DefaultViewModel et présentant le type Map<String^,Object^>. Ce mappage stocke les données auxquelles sont liés les contrôles XAML sur la page ; il joue le rôle de contexte de données pour la page.

Notre modèle est composé de trois classes. La classe FeedData représente l’URI de niveau supérieur et les métadonnées d’un flux de blog. Le flux figurant à l’adresse https://blogs.windows.com/windows/b/buildingapps/rss.aspx est un exemple des éléments qu’une classe FeedData encapsule. Un flux présente une liste de billets de blog, que nous représentons sous la forme d’objets FeedItem. Chaque objet FeedItem correspond à un billet et inclut le titre, le contenu, l’URI et d’autres métadonnées. Le billet figurant à l’adresse https://blogs.windows.com/windows/b/buildingapps/archive/2014/05/28/using-the-windows-phone-emulator-for-testing-apps-with-geofencing.aspx est un exemple d’objet FeedItem. La première page de notre application affiche les flux, la deuxième page est une vue des objets FeedItem pour un seul flux ; les deux dernières pages fournissent différentes vues d’un billet unique (sous forme de texte brut ou de page web).

La classe FeedDataSource contient une collection d’éléments FeedData ainsi que les méthodes de téléchargement associées.

En résumé :

  • FeedData contient des informations sur un flux RSS ou Atom.

  • FeedItem contient des informations sur chaque billet de blog dans le flux.

  • FeedDataSource contient les méthodes pour télécharger les flux et initialiser nos classes de données.

Nous définissons ces classes en tant que classes public ref class pour activer la liaison de données ; les contrôles XAML ne peuvent pas interagir avec les classes C++ standard. Nous utilisons l’attribut Bindable pour indiquer au compilateur XAML que nous nous lions dynamiquement aux instances de ces types. Dans une classe ref publique, les membres de données publics sont exposés en tant que propriétés. Les propriétés dépourvues de logique spéciale n’exigent pas qu’un accesseur Get et une méthode setter soient spécifiés par l’utilisateur ; c’est le compilateur qui les fournit. Dans la classe FeedData, vous pouvez noter de quelle manière l’élément Windows::Foundation::Collections::IVector est utilisé pour exposer un type de collection publique. Nous utilisons en interne la classe Platform::Collections::Vector, en tant que type concret qui implémente IVector.

Les projets Windows et Windows Phone utilisent le même modèle de données. Nous allons donc placer les classes dans le projet partagé.

Hh465045.wedge(fr-fr,WIN.10).gifPour créer des classes de données personnalisées

  1. Dans l’Explorateur de solutions, dans le menu contextuel du nœud de projet SimpleBlogReader.Shared, choisissez Ajouter > Nouvel élément. Sélectionnez l’option Fichier d’en-tête (.h) et nommez-le FeedData.h.

  2. Ouvrez FeedData.h et collez-y le code suivant. Observez la directive #include pour l’élément pch.h. Il s’agit de l’en-tête précompilé. C’est dans ce dernier que doivent être placés les en-têtes système qui ne sont pas modifiés, ou très peu. Par défaut, pch.h inclut l’élément collection.h, qui est requis pour le type Platform::Collections::Vector et l’élément ppltasks.h, qui est requis pour le type concurrency::task et les types associés. Ces en-têtes comprennent les éléments <string> et <vector> requis par notre application. Nous n’avons donc pas besoin de les inclure de manière explicite.

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

    Les classes sont des classes ref, car les classes XAML Windows Runtime doivent pouvoir interagir avec elles pour lier les données à l’interface utilisateur. L’attribut [Bindable] associé à ces classes est également nécessaire pour la liaison de données. Le mécanisme de liaison ne les détectera pas sans cet attribut.

Partie 3 : Téléchargement des données

La classe FeedDataSource contient les méthodes permettant de télécharger les flux, ainsi que d’autres méthodes d’assistance. Elle comprend également la collection de flux téléchargés qui est ajoutée à la valeur Item de l’élément DefaultViewModel de la page d’application principale. L’élément FeedDataSource utilise la classe Windows::Web::Syndication::SyndicationClient pour effectuer le téléchargement. Étant donné que les opérations réseau peuvent prendre du temps, ces opérations sont asynchrones. Lorsqu’un flux de téléchargement est terminé, l’objet FeedData est initialisé et ajouté à la collection FeedDataSource::Feeds. Il s’agit d’un élément IObservable<T>, ce qui signifie que l’interface utilisateur est informée lorsqu’un élément est ajouté et l’affiche dans la page principale. Pour les opérations asynchrones, nous utilisons la classe concurrency::task ainsi que les méthodes et classes associées de l’élément ppltasks.h. La fonction create_task est utilisée pour envelopper l’élément IAsyncOperation et la fonction IAsyncAction appelle l’API Windows. La fonction membre task::then est utilisée pour exécuter du code qui doit attendre la fin de la tâche.

L’application propose une fonctionnalité intéressante : l’utilisateur n’a pas besoin d’attendre la fin du téléchargement de l’ensemble des flux. En effet, il peut cliquer sur un flux dès qu’il apparaît et accéder à une nouvelle page qui affiche tous les éléments de ce flux. Cet exemple illustre une interface utilisateur désormais « rapide et fluide », grâce aux nombreuses modifications apportées aux threads en arrière-plan. Nous allons voir cette interface en action une fois que nous aurons ajouté la page XAML principale.

Toutefois, les opérations asynchrones ajoutent de la complexité : « rapide et fluide » ne veut pas dire « gratuit ». Si vous avez lu les didacticiels précédents, vous savez qu’une application qui n’est pas active peut être arrêtée par le système afin de libérer de la mémoire, puis restaurée quand l’utilisateur y accède à nouveau. Dans notre application, nous n’enregistrons pas toutes les données de flux lorsque nous l’arrêtons, car cette opération monopolise une grande quantité d’espace de stockage et peut signifier l’apparition de données obsolètes. Nous téléchargeons toujours les flux au démarrage. Toutefois, cela signifie que nous devons prendre en compte le scénario dans lequel l’application reprend après un arrêt et essaie immédiatement d’afficher un objet FeedData dont le téléchargement n’est pas encore terminé. Nous devons éviter de tenter d’afficher les données tant qu’il n’est pas disponible. Dans ce cas, nous ne pouvons pas utiliser la méthode then, mais nous pouvons utiliser un élément task_completed_event. Cet événement empêche le code de tenter d’accéder à un objet FeedData tant qu’il n’est pas entièrement chargé.

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

  1. Ajoutez la classe FeedDataSource à l’élément FeedData.h., dans le cadre de l’espace de noms SimpleBlogReader :

        /// <summary>
        /// A FeedDataSource represents a collection of FeedData objects
        /// and provides the methods to retrieve the stores URLs and download 
        /// the source data from which FeedData and FeedItem objects are constructed.
        /// This class is instantiated at startup by this declaration in the 
        /// ResourceDictionary in app.xaml: <local:FeedDataSource x:Key="feedDataSource" /> 
        /// </summary>
        [Windows::UI::Xaml::Data::Bindable]
        public ref class FeedDataSource sealed
        {
        private:
            Platform::Collections::Vector<FeedData^>^ m_feeds;
            FeedData^ GetFeedData(Platform::String^ feedUri, WWS::SyndicationFeed^ feed);
            concurrency::task<WFC::IVector<Platform::String^>^> GetUserURLsAsync();
            void DeleteBadFeedHandler(Windows::UI::Popups::UICommand^ command);
    
        public:
            FeedDataSource();
            property Windows::Foundation::Collections::IObservableVector<FeedData^>^ Feeds
            {
                Windows::Foundation::Collections::IObservableVector<FeedData^>^ get()
                {
                    return this->m_feeds;
                }
            }
            property Platform::String^ CurrentFeedUri;
            void InitDataSource();        
    
        internal:
            // This is used to prevent SplitPage from prematurely loading the last viewed page on resume.
            concurrency::task_completion_event<FeedData^> m_LastViewedFeedEvent;
            concurrency::task<void> RetrieveFeedAndInitData(Platform::String^ url, WWS::SyndicationClient^ client);
        };
    
  2. Ensuite, créez un fichier nommé FeedData.cpp dans le projet partagé et collez le code suivant :

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

        #include "FeedData.h"
    
    • Dans le projet partagé, accédez au fichier App.xaml et ajoutez un nœud Application.Resources. Dans ce dernier, placez une référence à l’élément FeedDataSource afin que la page se présente comme suit :

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

      Ce balisage entraîne la création d’un objet FeedDataSource lors du démarrage de l’application ; l’objet est accessible à partir de n’importe quelle page de l’application. Lorsque l’événement OnLaunched est déclenché, l’objet d’application appelle l’élément InitDataSource, afin d’amener l’instance feedDataSource à lancer le téléchargement de toutes ses données.

      Le projet n’est pas encore généré, car nous devons ajouter des définitions de classe supplémentaires.

Partie 4 : Gestion de la synchronisation des données lors de la reprise sur arrêt

Lors du premier démarrage de l’application, lorsque l’utilisateur passe d’une page à une autre ou revient en arrière, aucune synchronisation de l’accès aux données n’est requis. Les flux apparaissent uniquement sur la première page une fois qu’ils sont initialisés ; les autres pages ne tentent pas d’accéder aux données tant que l’utilisateur n’a pas cliqué sur un flux visible. Après cela, l’accès est effectué uniquement en lecture seule : nous ne modifions jamais les données source. Toutefois, il existe un scénario qui nécessite une synchronisation : quand l’application est arrêtée alors qu’une page basée sur un flux particulier est active, cette page doit à nouveau relier ces données de flux au moment où l’application est relancée. Dans ce cas, il est possible qu’une page tente d’accéder aux données qui n’existent pas encore. Par conséquent, nous devons trouver un moyen de forcer la page à attendre que les données soient prêtes.

Les fonctions suivantes permettent à l’application de se rappeler quel flux elle surveillait. La méthode SetCurrentFeed assure simplement la persistance du flux dans les paramètres locaux, où il peut être récupéré lorsque l’application n’a plus de mémoire. La méthode GetCurrentFeedAsync est celle qui nous intéresse, car nous devons nous assurer que, lorsque nous voulons revenir en arrière et recharger le dernier flux, nous ne pouvons pas le faire tant que le flux n’a pas été rechargé. Nous étudierons ce code plus en détail un peu plus tard. Nous allons ajouter le code à la classe App, car nous allons l’appeler, tant à partir de l’application Windows que de l’application Windows Phone.

  1. Dans l’élément app.xaml.h, ajoutez les signatures de ces méthodes. L’accessibilité interne signifie que ces dernières peuvent être consommées uniquement à partir d’un autre code C++ dans le même espace de noms.

        internal:
        concurrency::task<FeedData^> GetCurrentFeedAsync();
        void SetCurrentFeed(FeedData^ feed); 
        FeedItem^ GetFeedItem(FeedData^ fd, Platform::String^ uri);
        void AddFeed(Platform::String^ feedUri);
        void RemoveFeeds(Platform::Collections::Vector<FeedData^>^ feedsToDelete);
        void DeleteUrlFromFeedFile(Platform::String^ s);
    
  2. Ensuite, en haut du fichier app.xaml.cpp, ajoutez les instructions using suivantes :

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

    Vous avez besoin de l’espace de noms concurrency pour la tâche, de l’espace de noms Platform::Collections pour l’élément Vector et de l’espace de noms Windows::Storage pour ApplicationData.

    Ajoutez également ces lignes en bas :

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

Partie 5: Conversion des données sous une forme utilisable

Toutes les données brutes ne se présentent pas forcément sous une forme utilisable. Un flux RSS ou Atom exprime sa date de publication sous la forme d’une valeur numérique de type RFC 822. Nous devons trouver une méthode permettant de convertir ces données dans un format texte significatif pour l’utilisateur. Pour ce faire, nous allons créer une classe personnalisée qui implémente l’interface IValueConverter et accepte une valeur RFC 833 en entrée, créant en sortie des chaînes pour chaque composant de la date. Plus tard, dans le code XAML qui affiche les données, nous allons créer une liaison à la sortie de notre classe DateConverter plutôt qu’au format de données brutes.

Hh465045.wedge(fr-fr,WIN.10).gifAjouter un convertisseur de date

  1. Dans le projet partagé, créez un fichier au format .h et ajoutez le code suivant :

    //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. Maintenant, associez-lui la directive #include dans le fichier App.xaml.h :

    #include "DateConverter.h"
    
  3. Créez une instance de cet élément dans le fichier App.xaml dans le nœud Application.Resources :

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

Le contenu des flux est fourni sur le réseau au format HTML, voire sous forme de texte XML mis en forme, dans certains cas. Pour afficher ce contenu dans un élément RichTextBlock, nous devons le convertir en texte enrichi. La classe suivante utilise la fonction HtmlUtilities de Windows pour analyser le code HTML, puis les fonctions < regex > pour le scinder en paragraphes, afin que nous puissions créer des objets de texte enrichi. Nous ne pouvons pas utiliser la liaison de données dans ce scénario ; la classe n’a donc pas besoin d’implémenter l’élément IValueConverter. Nous allons juste créer des instances locales de ce dernier dans les pages concernées.

Hh465045.wedge(fr-fr,WIN.10).gifAjouter un convertisseur de texte

  1. Dans le projet partagé, ajoutez un nouveau fichier .h, appelez-le TextHelper.h et ajoutez le code suivant :

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

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

    Notez que notre classe TextHelper personnalisée illustre quelques-unes des méthodes permettant d’utiliser ISO C++ (std::map, std::regex, std::wstring) en interne, dans une application CX/C++. Nous allons créer des instances de cette classe en local, dans les pages qui l’utilisent. Il nous suffit de l’inclure une seule fois dans le fichier App.xaml.h :

    #include "TextHelper.h"
    
  3. À présent, vous devez être en mesure de générer et d’exécuter l’application. Cependant, elle ne devrait pas être très active.

Partie 6 : Lancement, suspension et reprise de l’application

L’événement App::OnLaunched est déclenché lorsque l’utilisateur démarre l’application en appuyant ou en cliquant sur sa vignette, et lorsque l’utilisateur accède à nouveau à l’application, une fois le système arrêté, afin de libérer la mémoire pour d’autres applications. Dans les deux cas, nous devons toujours accéder à Internet et recharger les données en réponse à cet événement. Toutefois, il existe d’autres actions, qui doivent uniquement être appelées dans un cas ou dans l’autre. Nous pouvons déduire ces états en examinant l’élément rootFrame en combinaison avec l’argument LaunchActivatedEventArgs qui est passé à la fonction, puis effectuer l’action appropriée. Heureusement, la classe SuspensionManager qui a été ajoutée automatiquement avec l’élément MainPage effectue la majeure partie du travail pour enregistrer et restaurer l’état de l’application lorsque cette dernière est suspendue et relancée. Il nous suffit d’appeler ses méthodes.

  1. Ajoutez les fichiers de code SuspensionManager au projet dans le dossier Common. Ajoutez SuspensionManager.h et copiez-y le code suivant :

    //
    // SuspensionManager.h
    // Declaration of the SuspensionManager class
    //
    
    #pragma once
    
    namespace SimpleBlogReader
    {
        namespace Common
        {
            /// <summary>
            /// SuspensionManager captures global session state to simplify process lifetime management
            /// for an application.  Note that session state will be automatically cleared under a variety
            /// of conditions and should only be used to store information that would be convenient to
            /// carry across sessions, but that should be disacarded when an application crashes or is
            /// upgraded.
            /// </summary>
            class SuspensionManager sealed
            {
            public:
                static void RegisterFrame(Windows::UI::Xaml::Controls::Frame^ frame, Platform::String^ sessionStateKey, Platform::String^ sessionBaseKey = nullptr);
                static void UnregisterFrame(Windows::UI::Xaml::Controls::Frame^ frame);
                static concurrency::task<void> SaveAsync();
                static concurrency::task<void> RestoreAsync(Platform::String^ sessionBaseKey = nullptr);
                static Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ SessionState();
                static Windows::Foundation::Collections::IMap<Platform::String^, Platform::Object^>^ SessionStateForFrame(
                    Windows::UI::Xaml::Controls::Frame^ frame);
    
            private:
                static void RestoreFrameNavigationState(Windows::UI::Xaml::Controls::Frame^ frame);
                static void SaveFrameNavigationState(Windows::UI::Xaml::Controls::Frame^ frame);
    
                static Platform::Collections::Map<Platform::String^, Platform::Object^>^ _sessionState;
                static const wchar_t* sessionStateFilename;
    
                static std::vector<Platform::WeakReference> _registeredFrames;
                static Windows::UI::Xaml::DependencyProperty^ FrameSessionStateKeyProperty;
                static Windows::UI::Xaml::DependencyProperty^ FrameSessionBaseKeyProperty;
                static Windows::UI::Xaml::DependencyProperty^ FrameSessionStateProperty;
            };
        }
    }
    
  2. Ajoutez le fichier de code SuspensionManager.cpp et copiez-y le code suivant :

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

    #include "Common\SuspensionManager.h"
    
  4. Ajoutez la directive d’espace de noms :

    using namespace SimpleBlogReader::Common;
    
  5. Maintenant, remplacez la fonction existante par le code suivant :

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

    Notez que la classe App figure dans le projet partagé. Par conséquent, le code que nous écrivons ici s’exécute sur les applications Windows et Windows Phone, sauf si la macro WINAPI_FAMILY == WINAPI_FAMILY_PHONE_APP est définie.

  6. Le gestionnaire OnSuspending est plus simple. Il est appelé lorsque le système arrête l’application, et non lorsque l’utilisateur la ferme. Nous laissons la classe SuspensionManager agir. Elle appelle le gestionnaire d’événements SaveState sur chaque page de l’application, puis sérialise les objets que nous avons stockés dans l’objet PageState de chaque page, avant de restaurer les valeurs dans les pages lorsque l’application reprend son exécution. Consultez le fichier SuspensionManager.cpp si vous voulez examiner le code.

    Remplacez le corps de la fonction OnSuspending existante par le code suivant :

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

À ce stade, nous pouvons lancer l’application et télécharger les données de flux, mais nous n’avons aucun moyen de les afficher à l’utilisateur. Il est temps d’y remédier !

Partie 7 : Ajout de la première page d’interface web, une liste de flux

Lorsque l’application s’ouvre, nous voulons afficher à l’utilisateur une collection de niveau supérieur, qui regroupe tous les flux téléchargés. Ils peuvent cliquer ou appuyer sur un élément dans la collection, afin d’accéder à un flux particulier qui contient une collection de billets ou d’éléments de flux. Nous avons déjà ajouté les pages. Dans l’application Windows, il s’agit d’une page d’éléments, qui affiche un contrôle GridView lorsque l’appareil est à l’horizontale, et un contrôle ListView lorsque le périphérique est à la verticale. Les projets Windows Phone ne présentent pas de page Éléments. Nous disposons donc d’une page de base, à laquelle nous ajoutons un contrôle ListView manuellement. La vue Liste s’ajuste automatiquement lorsque l’orientation du périphérique change.

Sur n’importe quelle page, les mêmes tâches de base doivent généralement être effectuées :

  • ajout d’un balisage XAML qui décrit l’interface utilisateur et la lie aux données ;
  • ajout de code personnalisé aux fonctions membres LoadState et SaveState ;
  • gestion des événements, l’un d’entre eux (au minimum) présentant du code qui accède à la page suivante.

Nous allons effectuer ces opérations dans l’ordre, tout d’abord dans le projet Windows :

Ajouter le balisage XAML (page MainPage)

La page principale effectue le rendu de chaque objet FeedData dans un contrôle GridView. Pour décrire l’aspect attendu des données, nous créons un DataTemplate, qui correspond à une arborescence XAML, laquelle sera utilisée pour afficher chaque élément. Les possibilités des éléments DataTemplate en termes de dispositions, polices, couleurs, etc., sont uniquement limitées par votre imagination et votre conception du style ! Sur cette page, nous allons utiliser un modèle simple, qui ressemblera à ce qui suit lors du rendu :

Élément de flux

  1. Un style XAML s’apparente à un style dans Microsoft Word ; c’est un moyen pratique de regrouper un ensemble de valeurs de propriété sur un élément XAML, l’élément « TargetType ». Un style peut être basé sur un autre style. L’attribut « x:Key » spécifie le nom que nous utilisons pour faire référence au style lorsque nous l’appliquons.

    Placez ce modèle et les styles qui l’accompagnent dans le nœud Page.Resources du fichier MainPage.xaml (Windows 8.1). Ils sont uniquement utilisés dans la classe MainPage.

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

    Un trait rouge ondulé doit apparaître sous GreenBlockBackgroundBrush. Nous nous en occuperons, en quelques étapes.

  2. Dans le fichier MainPage.xaml (Windows 8.1), supprimez l’élément AppName page-local afin qu’il ne masque pas l’élément global que nous allons ajouter à l’étendue de l’application.

  3. Ajoutez un contrôle CollectionViewSource au nœud Page.Resources. Cet objet connecte notre contrôle ListView au modèle de données :

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

    Notez que l’élément Page a déjà défini un attribut DataContext sur la propriété DefaultViewModel pour la classe MainPage. Nous définissons cette propriété sur un élément FeedDataSource. De ce fait, CollectionViewSource y recherche (et y trouve) une collection d’éléments.

  4. Dans le fichier App.xaml, ajoutons une chaîne de ressource globale pour le nom de l’application, ainsi que des ressources supplémentaires qui seront référencées à partir de plusieurs pages dans l’application. En plaçant les ressources à cet endroit, nous n’avons pas besoin de les définir séparément, sur chaque page. Ajoutez ces éléments au nœud Resources dans le fichier App.xaml :

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

La classe MainPage affiche la liste des flux. Lorsque le périphérique présente l’orientation paysage, nous utilisons un contrôle GridView, qui prend en charge le défilement horizontal. Dans l’orientation paysage, nous allons utiliser un contrôle ListView, qui prend en charge le défilement vertical. Nous voulons que l’utilisateur puisse manipuler l’application dans les deux orientations. L’implémentation de la prise en charge des changements d’orientation est relativement simple :

  • Ajoutez les deux contrôles à la page et définissez l’élément ItemSource sur la même valeur collectionViewSource. Définissez la propriété Visibility du contrôle ListView sur Collapsed, afin que ce contrôle ne soit pas visible, par défaut.
  • Créez un ensemble de deux objets VisualState, l’un décrivant le comportement de l’interface utilisateur pour l’orientation paysage et l’autre, son comportement pour l’orientation portrait.
  • Gérez l’événement Window::SizeChanged, qui est déclenché lorsque l’orientation est modifiée, ou lorsque l’utilisateur réduit ou agrandit la fenêtre. Examinez les valeurs de hauteur et de largeur de la nouvelle taille. Si la hauteur est supérieure à la largeur, appelez l’état VisualState pour l’orientation portrait. Sinon, appelez l’état du mode paysage.

Hh465045.wedge(fr-fr,WIN.10).gifAjouter des contrôles GridView et ListView

  1. Dans le fichier MainPage.xaml, ajoutez ces contrôles GridView et ListView, ainsi que la grille qui contient le titre de page et le bouton Précédent :

     <Grid Style="{StaticResource WindowsBlogLayoutRootStyle}">
            <Grid.ChildrenTransitions>
                <TransitionCollection>
                    <EntranceThemeTransition/>
                </TransitionCollection>
            </Grid.ChildrenTransitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="140"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
    
            <!-- Horizontal scrolling grid -->
            <GridView
                x:Name="ItemGridView"
                AutomationProperties.AutomationId="ItemsGridView"
                AutomationProperties.Name="Items"
                TabIndex="1"
                Grid.RowSpan="2"
                Padding="116,136,116,46"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                SelectionMode="None"
                ItemTemplate="{StaticResource DefaultGridItemTemplate}"
                IsItemClickEnabled="true"
                IsSwipeEnabled="false"
                ItemClick="ItemGridView_ItemClick"  Margin="0,-10,0,10">
            </GridView>
    
            <!-- Vertical scrolling list -->
            <ListView
                x:Name="ItemListView"
                Visibility="Collapsed"            
                AutomationProperties.AutomationId="ItemsListView"
                AutomationProperties.Name="Items"
                TabIndex="1" Grid.Row="1" Margin="-10,-10,0,0"      
                IsItemClickEnabled="True"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                IsSwipeEnabled="False"            
                ItemClick="ItemGridView_ItemClick"
                ItemTemplate="{StaticResource ListItemTemplate}">
    
                <ListView.ItemContainerStyle>
                    <Style TargetType="FrameworkElement">
                        <Setter Property="Margin" Value="2,0,0,2"/>
                    </Style>
                </ListView.ItemContainerStyle>
            </ListView>
    
            <!-- Back button and page title -->
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="120"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Button x:Name="backButton" Margin="39,59,39,0" 
                        Command="{Binding NavigationHelper.GoBackCommand, ElementName=pageRoot}"
                        Style="{StaticResource NavigationBackButtonNormalStyle}"
                        VerticalAlignment="Top"
                        AutomationProperties.Name="Back"
                        AutomationProperties.AutomationId="BackButton"
                        AutomationProperties.ItemType="Navigation Button"/>
                <TextBlock x:Name="pageTitle" Text="{StaticResource AppName}" 
                        Style="{StaticResource HeaderTextBlockStyle}" Grid.Column="1" 
                        IsHitTestVisible="false" TextWrapping="NoWrap" 
                        VerticalAlignment="Bottom" Margin="0,0,30,40"/>
            </Grid>
    
  2. Notez que les deux contrôles utilisent la même fonction membre pour l’événement ItemClick. Placez le point d’insertion sur l’un de ces contrôles et appuyez sur F12 pour générer automatiquement le stub de gestionnaire d’événements. Nous ajouterons le code associé ultérieurement.

  3. Collez la définition VisualStateGroups de sorte qu’elle constitue le dernier élément à l’intérieur de l’élément grid racine (ne la mettez pas en dehors de la grille, car elle ne fonctionnera pas). Notez qu’il existe deux états, mais un seul d’entre eux est défini de manière explicite. En effet, l’état DefaultLayout est déjà décrit dans le code XAML de cette page.

    <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. Désormais, l’interface utilisateur est entièrement définie. Il ne nous reste plus qu’à indiquer à la page ce qu’elle doit faire lorsqu’elle est chargée.

LoadState et SaveState (page MainPage de l’application Windows)

Sur une page XAML, les deux principales fonctions membres que nous devons prendre en considération sont LoadState et SaveState (dans certains cas). Dans la fonction LoadState, nous remplissons les données de la page ; dans la fonction SaveState, nous enregistrons les données qui seront nécessaires pour remplir la page, pour le cas où elle serait suspendue, puis redémarrée.

  • Remplacez l’implémentation de la fonction LoadState par ce code, qui insère les données de flux qui ont été chargées (ou sont toujours en cours de chargement) par l’élément feedDataSource que nous avons créé au démarrage, puis place les données dans notre ViewModel pour cette page.

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

    Nous n’avons pas besoin d’appeler la fonction SaveState pour la page MainPage, car il n’existe aucun élément que cette page doit retenir. Elle affiche toujours l’ensemble des flux.

Gestionnaires d’événements (page MainPage de l’application Windows)

D’un point de vue conceptuel, toutes les pages se trouvent à l’intérieur d’une trame. Il s’agit de la trame qui nous permet de passer d’une page à une autre, d’avant en arrière. Le second paramètre d’une fonction Navigate permet de passer les données d’une page à une autre. Les objets que nous passons ici sont automatiquement stockés et sérialisés par la classe SuspensionManager chaque fois que l’application est suspendue, afin que les valeurs puissent être restaurées lors de la reprise de l’application. La classe SuspensionManager par défaut n’accepte que les types String et Guid intégrés. Si vous avez besoin d’une sérialisation plus sophistiquée, vous pouvez créer une classe SuspensionManager personnalisée. Ici, nous passons une chaîne, qui permet à la classe SplitPage de rechercher le flux en cours.

Hh465045.wedge(fr-fr,WIN.10).gifPour naviguer via des clics d’élément

  1. Lorsque l’utilisateur clique sur un élément de la grille, le gestionnaire d’événements obtient l’élément ayant fait l’objet du clic, le définit en tant que « flux en cours » (pour le cas où l’application serait plus tard suspendue) et accède à la page suivante. Il passe le titre du flux à la page suivante, afin que cette dernière puisse rechercher les données de ce flux. Voici le code à coller :

    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. Pour permettre la compilation du code précédent, nous devons utiliser la directive « #include SplitPage.xaml.h » pour inclure le fichier SplitPage.xaml.h au début du fichier en cours, à savoir MainPage.xaml.cpp (Windows 8.1) :

    #include "SplitPage.xaml.h"
    

Hh465045.wedge(fr-fr,WIN.10).gifPour gérer l’événement Page_SizeChanged

  • Dans le fichier MainPage.xaml, ajoutez un nom à l’élément racine en ajoutant x:Name="pageRoot" aux attributs de l’élément Page racine, puis ajoutez un attribut SizeChanged="pageRoot_SizeChanged" pour créer un gestionnaire d’événements. Dans le fichier cpp, remplacez l’implémentation du gestionnaire d’événements par le code suivant :

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

    Ensuite, ajoutez la déclaration de cette fonction à la classe MainPage dans MainPage.xaml.h.

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

    Le code est simple. Si vous exécutez l’application dans le simulateur et faites pivoter l’appareil, vous verrez l’interface utilisateur passer du contrôle GridView au contrôle ListView.

Ajout de code XAML (page MainPage de l’application Windows Phone)

Maintenant, nous allons activer la page principale de l’application Windows Phone. Cela va nécessiter beaucoup moins de code, car nous allons utiliser celui que nous avons inséré dans le projet partagé. En outre, les applications Windows Phone ne prennent pas en charge les contrôles GridView, car les écrans sont trop petits pour qu’elles puissent fonctionner correctement. Nous allons utiliser un contrôle ListView qui s’adapte automatiquement à l’orientation paysage. Nous n’avons pas besoin de modifier VisualState. Nous allons commencer en ajoutant l’attribut DataContext à l’élément Page. Cet élément n’est pas généré automatiquement dans une page Windows Phone de base comme il le serait dans une page ItemsPage ou SplitPage.

  1. Pour implémenter une navigation entre les pages, vos pages ont besoin de la classe NavigationHelper qui, à son tour, dépend du fichier RelayCommand. Ajoutez le nouvel élément RelayCommand.h et copiez-y le code suivant :

    //
    // RelayCommand.h
    // Declaration of the RelayCommand and associated classes
    //
    
    #pragma once
    
    // <summary>
    // A command whose sole purpose is to relay its functionality 
    // to other objects by invoking delegates. 
    // The default return value for the CanExecute method is 'true'.
    // <see cref="RaiseCanExecuteChanged"/> needs to be called whenever
    // <see cref="CanExecute"/> is expected to return a different value.
    // </summary>
    
    
    namespace SimpleBlogReader
    {
        namespace Common
        {
            [Windows::Foundation::Metadata::WebHostHidden]
            public ref class RelayCommand sealed :[Windows::Foundation::Metadata::Default] Windows::UI::Xaml::Input::ICommand
            {
            public:
                virtual event Windows::Foundation::EventHandler<Object^>^ CanExecuteChanged;
                virtual bool CanExecute(Object^ parameter);
                virtual void Execute(Object^ parameter);
                virtual ~RelayCommand();
    
            internal:
                RelayCommand(std::function<bool(Platform::Object^)> canExecuteCallback,
                    std::function<void(Platform::Object^)> executeCallback);
                void RaiseCanExecuteChanged();
    
            private:
                std::function<bool(Platform::Object^)> _canExecuteCallback;
                std::function<void(Platform::Object^)> _executeCallback;
            };
        }
    }
    
  2. Dans le dossier Common, ajoutez RelayCommand.cpp et copiez-y le code suivant :

    //
    // RelayCommand.cpp
    // Implementation of the RelayCommand and associated classes
    //
    
    #include "pch.h"
    #include "RelayCommand.h"
    #include "NavigationHelper.h"
    
    using namespace SimpleBlogReader::Common;
    
    using namespace Platform;
    using namespace Platform::Collections;
    using namespace Windows::Foundation;
    using namespace Windows::Foundation::Collections;
    using namespace Windows::System;
    using namespace Windows::UI::Core;
    using namespace Windows::UI::ViewManagement;
    using namespace Windows::UI::Xaml;
    using namespace Windows::UI::Xaml::Controls;
    using namespace Windows::UI::Xaml::Input;
    using namespace Windows::UI::Xaml::Navigation;
    
    /// <summary>
    /// Determines whether this <see cref="RelayCommand"/> can execute in its current state.
    /// </summary>
    /// <param name="parameter">
    /// Data used by the command. If the command does not require data to be passed, this object can be set to null.
    /// </param>
    /// <returns>true if this command can be executed; otherwise, false.</returns>
    bool RelayCommand::CanExecute(Object^ parameter)
    {
        return (_canExecuteCallback) (parameter);
    }
    
    /// <summary>
    /// Executes the <see cref="RelayCommand"/> on the current command target.
    /// </summary>
    /// <param name="parameter">
    /// Data used by the command. If the command does not require data to be passed, this object can be set to null.
    /// </param>
    void RelayCommand::Execute(Object^ parameter)
    {
        (_executeCallback) (parameter);
    }
    
    /// <summary>
    /// Method used to raise the <see cref="CanExecuteChanged"/> event
    /// to indicate that the return value of the <see cref="CanExecute"/>
    /// method has changed.
    /// </summary>
    void RelayCommand::RaiseCanExecuteChanged()
    {
        CanExecuteChanged(this, nullptr);
    }
    
    /// <summary>
    /// RelayCommand Class Destructor.
    /// </summary>
    RelayCommand::~RelayCommand()
    {
        _canExecuteCallback = nullptr;
        _executeCallback = nullptr;
    };
    
    /// <summary>
    /// Creates a new command that can always execute.
    /// </summary>
    /// <param name="canExecuteCallback">The execution status logic.</param>
    /// <param name="executeCallback">The execution logic.</param>
    RelayCommand::RelayCommand(std::function<bool(Platform::Object^)> canExecuteCallback,
        std::function<void(Platform::Object^)> executeCallback) :
        _canExecuteCallback(canExecuteCallback),
        _executeCallback(executeCallback)
        {
        }
    
  3. Dans le dossier Common, ajoutez un fichier NavigationHelper.h et copiez-y le code suivant :

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

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

    //
    // MainPage.xaml.h
    // Declaration of the MainPage class
    //
    
    #pragma once
    
    #include "MainPage.g.h"
    #include "Common\NavigationHelper.h"
    
    namespace SimpleBlogReader
    {
    
        namespace WFC = Windows::Foundation::Collections;
        namespace WUIX = Windows::UI::Xaml;
        namespace WUIXNav = Windows::UI::Xaml::Navigation;
        namespace WUIXControls = Windows::UI::Xaml::Controls;
    
        /// <summary>
        /// A basic page that provides characteristics common to most applications.
        /// </summary>
        [Windows::Foundation::Metadata::WebHostHidden]
        public ref class MainPage sealed
        {
        public:
            MainPage();
    
            /// <summary>
            /// Gets the view model for this <see cref="Page"/>. 
            /// This can be changed to a strongly typed view model.
            /// </summary>
            property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel
            {
                WFC::IObservableMap<Platform::String^, Platform::Object^>^  get();
            }
    
            /// <summary>
            /// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>.
            /// </summary>
            property Common::NavigationHelper^ NavigationHelper
            {
                Common::NavigationHelper^ get();
            }
    
        protected:
            virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override;
            virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override;
    
        private:
            void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e);
            void SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e);
    
            static WUIX::DependencyProperty^ _defaultViewModelProperty;
            static WUIX::DependencyProperty^ _navigationHelperProperty;
    
        };
    
    }
    
  6. Dans MainPage.xaml.cpp, ajoutez l’implémentation de NavigationHelper et des stubs pour le chargement et l’enregistrement de l’état, ainsi que les propriétés DefaultViewModel. Vous ajouterez également les directives d’espace de noms using requises, afin que le code final ressemble à ceci :

    //
    // MainPage.xaml.cpp
    // Implementation of the MainPage class
    //
    
    #include "pch.h"
    #include "MainPage.xaml.h"
    
    using namespace SimpleBlogReader;
    using namespace Platform;
    using namespace Platform::Collections;
    using namespace Windows::Foundation;
    using namespace Windows::Foundation::Collections;
    using namespace Windows::UI::Xaml;
    using namespace Windows::UI::Xaml::Controls;
    using namespace Windows::UI::Xaml::Controls::Primitives;
    using namespace Windows::UI::Xaml::Data;
    using namespace Windows::UI::Xaml::Input;
    using namespace Windows::UI::Xaml::Media;
    using namespace Windows::UI::Xaml::Navigation;
    using namespace Windows::UI::Xaml::Interop;
    
    // The Basic Page item template is documented at https://go.microsoft.com/fwlink/?LinkID=390556
    
    MainPage::MainPage()
    {
        InitializeComponent();
        SetValue(_defaultViewModelProperty, 
            ref new Platform::Collections::Map<String^, Object^>(std::less<String^>()));
        auto navigationHelper = ref new Common::NavigationHelper(this);
        SetValue(_navigationHelperProperty, navigationHelper);
        navigationHelper->LoadState += 
            ref new Common::LoadStateEventHandler(this, &MainPage::LoadState);
        navigationHelper->SaveState += 
            ref new Common::SaveStateEventHandler(this, &MainPage::SaveState);
    }
    
    DependencyProperty^ MainPage::_defaultViewModelProperty =
    DependencyProperty::Register("DefaultViewModel",
    TypeName(IObservableMap<String^, Object^>::typeid), TypeName(MainPage::typeid), nullptr);
    
    /// <summary>
    /// Used as a trivial view model.
    /// </summary>
    IObservableMap<String^, Object^>^ MainPage::DefaultViewModel::get()
    {
        return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty));
    }
    
    DependencyProperty^ MainPage::_navigationHelperProperty =
    DependencyProperty::Register("NavigationHelper",
    TypeName(Common::NavigationHelper::typeid), TypeName(MainPage::typeid), nullptr);
    
    /// <summary>
    /// Gets an implementation of <see cref="NavigationHelper"/> designed to be
    /// used as a trivial view model.
    /// </summary>
    Common::NavigationHelper^ MainPage::NavigationHelper::get()
    {
        return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty));
    }
    
    #pragma region Navigation support
    
    /// The methods provided in this section are simply used to allow
    /// NavigationHelper to respond to the page's navigation methods.
    /// 
    /// Page specific logic should be placed in event handlers for the  
    /// <see cref="NavigationHelper::LoadState"/>
    /// and <see cref="NavigationHelper::SaveState"/>.
    /// The navigation parameter is available in the LoadState method 
    /// in addition to page state preserved during an earlier session.
    
    void MainPage::OnNavigatedTo(NavigationEventArgs^ e)
    {
        NavigationHelper->OnNavigatedTo(e);
    }
    
    void MainPage::OnNavigatedFrom(NavigationEventArgs^ e)
    {
        NavigationHelper->OnNavigatedFrom(e);
    }
    
    #pragma endregion
    
    /// <summary>
    /// Populates the page with content passed during navigation. Any saved state is also
    /// provided when recreating a page from a prior session.
    /// </summary>
    /// <param name="sender">
    /// The source of the event; typically <see cref="NavigationHelper"/>
    /// </param>
    /// <param name="e">Event data that provides both the navigation parameter passed to
    /// <see cref="Frame::Navigate(Type, Object)"/> when this page was initially requested and
    /// a dictionary of state preserved by this page during an earlier
    /// session. The state will be null the first time a page is visited.</param>
    void MainPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e)
    {
     (void) sender; // Unused parameter
        (void) e; // Unused parameter
    }
    
    /// <summary>
    /// Preserves state associated with this page in case the application is suspended or the
    /// page is discarded from the navigation cache.  Values must conform to the serialization
    /// requirements of <see cref="SuspensionManager::SessionState"/>.
    /// </summary>
    /// <param name="sender">The source of the event; typically <see cref="NavigationHelper"/></param>
    /// <param name="e">Event data that provides an empty dictionary to be populated with
    /// serializable state.</param>
    void MainPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e)
    {
        (void) sender;  // Unused parameter
        (void) e; // Unused parameter
    }
    
  7. Dans le fichier MainPage.xaml (Windows Phone 8.1), accédez au bas de la page, recherchez le commentaire relatif au panneau titre et supprimez l’ensemble du StackPanel. Sur le téléphone, nous avons besoin de la surface occupée de l’écran pour répertorier les flux de blog.

  8. Plus bas dans la page s’affiche une grille incluant ce commentaire : "TODO: Content should be placed within the following grid". Placez ce contrôle ListView dans cette grille :

        <!-- Vertical scrolling item list -->
         <ListView
            x:Name="itemListView"           
            AutomationProperties.AutomationId="itemListView"
            AutomationProperties.Name="Items"
            TabIndex="1" 
            IsItemClickEnabled="True"
            ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
            IsSwipeEnabled="False"
            ItemClick="ItemListView_ItemClick"
            SelectionMode="Single"
            ItemTemplate="{StaticResource ListItemTemplate}">
    
            <ListView.ItemContainerStyle>
                <Style TargetType="FrameworkElement">
                    <Setter Property="Margin" Value="2,0,0,2"/>
                </Style>
            </ListView.ItemContainerStyle>
        </ListView>
    
  9. Placez maintenant le curseur sur l’événement ItemListView_ItemClick et appuyez sur F12 (Atteindre la définition). Visual Studio génère une fonction de gestionnaire d’événements vide. Nous lui ajouterons du code plus tard. Pour le moment, nous devons simplement générer la fonction pour que l’application soit compilée.

Partie 8 : Liste des billets et affichage du texte du billet sélectionné

Dans cette partie, nous allons ajouter deux pages à l’application Windows Phone : la page qui répertorie les billets et celle qui indique la version du texte du billet sélectionné. Dans l’application Windows, nous devons juste ajouter une seule page, appelée SplitPage, qui affiche la liste d’un côté et le texte du billet sélectionné de l’autre. Tout d’abord, penchons-nous sur les pages Windows Phone.

Ajoutez le balisage XAML (page FeedPage de l’application Windows Phone)

Restons dans le projet Windows Phone et étudions la page FeedPage, qui répertorie les billets du flux que l’utilisateur sélectionne.

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

  1. Dans le fichier FeedPage.xaml (Windows Phone 8.1), ajoutez un contexte de données à l’élément Page :

    DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
    
  2. Ajoutez maintenant l’élément CollectionViewSource après l’élément Page d’ouverture :

    <Page.Resources>
        <!-- Collection of items displayed by this page -->
        <CollectionViewSource
        x:Name="itemsViewSource"
        Source="{Binding Items}"/>
    </Page.Resources>
    
  3. Dans l’élément de grille, ajoutez ce StackPanel :

            <!-- TitlePanel -->
            <StackPanel Grid.Row="0" Margin="24,17,0,28">
                <TextBlock Text="{StaticResource AppName}" 
                           Style="{ThemeResource TitleTextBlockStyle}" 
                           Typography.Capitals="SmallCaps"/>
            </StackPanel>
    
  4. Ensuite, ajoutez le contrôle ListView dans la grille (juste après l’élément d’ouverture) :

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

    Notez que la propriété ItemsSource du contrôle ListView se lie à l’élément CollectionViewSource, lequel est lié à notre propriété FeedData::Items, que nous insérons dans la propriété DefaultViewModel de l’élément LoadState dans le code-behind (voir ci-dessous).

  5. Il existe un événement ItemClick déclaré dans le contrôle ListView. Placez le curseur dessus, puis appuyez sur F12 pour générer le gestionnaire d’événements dans le code-behind. Ce gestionnaire est vide ; laissons-le ainsi pour le moment.

LoadState et SaveState (page FeedPage de l’application Windows Phone)

Dans MainPage, nous n’avions pas à nous soucier du stockage de l’état, car la page effectue systématiquement une réinitialisation complète à partir d’Internet à chaque démarrage de l’application, pour une raison quelconque. Les autres pages doivent se rappeler leur état. Par exemple, si l’application est arrêtée (déchargée de la mémoire) alors que la page FeedPage s’affiche, lorsque l’utilisateur retourne à cette dernière, nous voulons donner l’impression que l’application n’a jamais disparu. Nous devons donc nous rappeler quel flux a été sélectionné. L’emplacement de stockage de ces données se trouve dans le stockage AppData local. Lorsque l’utilisateur clique dessus dans la page MainPage, c’est le bon moment pour stocker ces données.

Un seul problème se pose : les données existent-elles déjà ? Si vous naviguez de la page MainPage à la page FeedPage via un clic d’utilisateur, nous savons avec certitude que l’objet FeedData sélectionné existe déjà. En effet, si tel n’était pas le cas, il n’apparaîtrait pas dans la liste MainPage. Toutefois, si l’application reprend son exécution, il se peut que le dernier objet FeedData consulté ne soit pas encore chargé lorsque FeedPage tente de se lier à lui. De ce fait, la page FeedPage (et d’autres pages) doit savoir comment déterminer que FeedData est disponible. L’événement concurrency::task_completion_event est tout spécifiquement conçu pour ce genre de situation. Grâce à lui, nous pouvons placer l’objet FeedData en toute sécurité dans le même chemin de code, que nous reprenions la navigation ou la démarrions depuis MainPage. Depuis FeedPage, nous obtenons toujours notre flux en appelant GetCurrentFeedAsync. Si nous naviguons à partir de MainPage, l’événement a déjà été défini lorsque l’utilisateur a cliqué sur un flux. Ainsi, la méthode renvoie immédiatement le flux. Si nous reprenons l’exécution après une interruption, l’événement est défini dans la fonction FeedDataSource::InitDataSource. Dans ce cas, FeedPage devra peut-être attendre quelque temps que le flux soit rechargé. En effet, l’attente est préférable à un blocage. À cause de ce léger problème, une grande partie du code asynchrone peut sembler complexe dans les fichiers FeedData.cpp et App.xaml.cpp. Cependant, si vous le regardez de près, vous constaterez qu’il n’est pas si compliqué.

  1. Dans le fichier FeedPage.xaml.cpp, ajoutez cet espace de noms pour inclure les objets de tâche dans la portée :

    using namespace concurrency;
    
  2. Ajoutez une directive #include pour le fichier TextViewerPage.xaml.h :

    #include "TextViewerPage.xaml.h"
    

    La définition de classe TextViewerPage est requise dans l’appel à Navigate, illustré ci-dessous.

  3. Remplacez la méthode LoadState par le code suivant :

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

    Si vous revenez à la page FeedPage depuis une page située plus haut dans la pile de pages, alors cette page est déjà initialisée (c’est-à-dire que DefaultViewModel présente une valeur pour « Feed ») et le flux en cours est déjà correctement défini. Cependant, si vous avancez vers une autre page depuis MainPage, ou si vous reprenez la navigation, nous devons obtenir le flux en cours afin d’insérer les données correctes dans la page. Si nécessaire, GetCurrentFeedAsync attend que les données lui parviennent après la reprise. Nous spécifions le contexte use_current() pour indiquer à la tâche qu’elle doit revenir au thread d’interface utilisateur avant d’essayer d’accéder à la propriété de dépendance DefaultViewModel. En général, il est impossible d’accéder directement aux objets liés à XAML à partir de threads d’arrière-plan.

    Dans cette page, nous ne touchons pas à SaveState, car nous obtenons l’état à partir de la méthode GetCurrentFeedAsync chaque fois que la page est chargée.

  4. Ajoutez la déclaration de LoadState dans le fichier d’en-tête FeedPage.xaml.h, ajoutez une directive include pour « Common\NavigationHelper.h », puis ajoutez les propriétés NavigationHelper et DefaultViewModel.

    //
    // FeedPage.xaml.h
    // Declaration of the FeedPage class
    //
    
    #pragma once
    
    #include "FeedPage.g.h"
    #include "Common\NavigationHelper.h"
    
    namespace SimpleBlogReader
    {
    
        namespace WFC = Windows::Foundation::Collections;
        namespace WUIX = Windows::UI::Xaml;
        namespace WUIXNav = Windows::UI::Xaml::Navigation;
        namespace WUIXControls = Windows::UI::Xaml::Controls;
    
                    /// <summary>
                    /// A basic page that provides characteristics common to most applications.
                    /// </summary>
                    [Windows::Foundation::Metadata::WebHostHidden]
                    public ref class FeedPage sealed
                    {
                    public:
                                    FeedPage();
    
                                    /// <summary>
                                    /// Gets the view model for this <see cref="Page"/>. 
                                    /// This can be changed to a strongly typed view model.
                                    /// </summary>
                                    property WFC::IObservableMap<Platform::String^, Platform::Object^>^ DefaultViewModel
                                    {
                                                    WFC::IObservableMap<Platform::String^, Platform::Object^>^  get();
                                    }
    
                                    /// <summary>
                                    /// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>.
                                    /// </summary>
                                    property Common::NavigationHelper^ NavigationHelper
                                    {
                                                    Common::NavigationHelper^ get();
                                    }
    
                    protected:
                                    virtual void OnNavigatedTo(WUIXNav::NavigationEventArgs^ e) override;
                                    virtual void OnNavigatedFrom(WUIXNav::NavigationEventArgs^ e) override;
    
                    private:
                                    void LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e);
                                    void SaveState(Platform::Object^ sender, Common::SaveStateEventArgs^ e);
    
                                    static Windows::UI::Xaml::DependencyProperty^ _defaultViewModelProperty;
                                    static Windows::UI::Xaml::DependencyProperty^ _navigationHelperProperty;
            void ItemListView_ItemClick(Platform::Object^ sender, WUIXControls::ItemClickEventArgs^ e);
        };
    
    }
    
  5. Ajoutez l’implémentation de ces propriétés dans FeedPage.xaml.cpp, qui se présente maintenant comme suit :

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

Gestionnaires d’événements (page FeedPage de l’application Windows Phone)

Nous gérons un événement sur FeedPage : l’événement ItemClick qui avance vers la page dans laquelle l’utilisateur peut lire le billet. Vous avez déjà créé un gestionnaire stub lorsque vous avez appuyé sur F12 en cliquant sur le nom de l’événement dans le code XAML.

  1. À présent, remplaçons l’implémentation par ce 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. Appuyez sur la touche F5 pour générer et exécuter l’application Windows Phone dans l’émulateur. Désormais, lorsque vous sélectionnez un élément à partir de MainPage, l’application doit accéder à FeedPage et afficher une liste des flux. L’étape suivante consiste à afficher le texte d’un flux sélectionné.

Hh465045.wedge(fr-fr,WIN.10).gifAjoutez de balisage XAML (page TextViewerPage de l’application Windows Phone)

  1. Dans le projet Windows Phone, accédez au fichier TextViewerPage.xaml et remplacez le panneau titre et la grille de contenu par ce balisage, qui affiche le nom de l’application (de manière transparente) et le titre du billet actuel, ainsi qu’un rendu simple du contenu, au format texte :

     <!-- 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. Dans le fichier TextViewerPage.xaml.h, ajoutez les propriétés NavigationHelper et DefaultViewItems, ainsi qu’un membre privé m_FeedItem pour stocker une référence à l’élément de flux actif lorsque nous le rechercherons pour la première fois à l’aide de la fonction GetFeedItem que nous avons ajoutée à la classe App au cours de l’étape précédente.

    Ajoutez également une fonction RichTextHyperlinkClicked. Le fichier TextViewerPage.xaml.h doit maintenant ressembler à ceci :

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

Hh465045.wedge(fr-fr,WIN.10).gifLoadState et SaveState (page TextViewerPage de l’application Windows Phone)

  1. Dans le fichier TextViewerPage.xaml.cpp, ajoutez cette directive include :

    #include "WebViewerPage.xaml.h"
    
  2. Ajoutez ces deux directives d’espace de noms :

    using namespace concurrency;
    using namespace Windows::UI::Xaml::Documents;
    
  3. Ajoutez le code pour NavigationHelper et DefaultViewModel.

    TextViewerPage::TextViewerPage()
    {
        InitializeComponent();
        SetValue(_defaultViewModelProperty, 
            ref new Platform::Collections::Map<String^, Object^>(std::less<String^>()));
        auto navigationHelper = ref new Common::NavigationHelper(this);
        SetValue(_navigationHelperProperty, navigationHelper);
        navigationHelper->LoadState += 
            ref new Common::LoadStateEventHandler(this, &TextViewerPage::LoadState);
        navigationHelper->SaveState += 
            ref new Common::SaveStateEventHandler(this, &TextViewerPage::SaveState);
    
      //  this->DataContext = DefaultViewModel;
    
    }
    
    DependencyProperty^ TextViewerPage::_defaultViewModelProperty =
    DependencyProperty::Register("DefaultViewModel",
    TypeName(IObservableMap<String^, Object^>::typeid), TypeName(TextViewerPage::typeid), nullptr);
    
    /// <summary>
    /// Used as a trivial view model.
    /// </summary>
    IObservableMap<String^, Object^>^ TextViewerPage::DefaultViewModel::get()
    {
        return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty));
    }
    
    DependencyProperty^ TextViewerPage::_navigationHelperProperty =
    DependencyProperty::Register("NavigationHelper",
    TypeName(Common::NavigationHelper::typeid), TypeName(TextViewerPage::typeid), nullptr);
    
    /// <summary>
    /// Gets an implementation of <see cref="NavigationHelper"/> designed to be
    /// used as a trivial view model.
    /// </summary>
    Common::NavigationHelper^ TextViewerPage::NavigationHelper::get()
    {
        return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty));
    }
    
    #pragma region Navigation support
    
    /// The methods provided in this section are simply used to allow
    /// NavigationHelper to respond to the page's navigation methods.
    /// 
    /// Page specific logic should be placed in event handlers for the  
    /// <see cref="NavigationHelper::LoadState"/>
    /// and <see cref="NavigationHelper::SaveState"/>.
    /// The navigation parameter is available in the LoadState method 
    /// in addition to page state preserved during an earlier session.
    
    void TextViewerPage::OnNavigatedTo(NavigationEventArgs^ e)
    {
        NavigationHelper->OnNavigatedTo(e);
    }
    
    void TextViewerPage::OnNavigatedFrom(NavigationEventArgs^ e)
    {
        NavigationHelper->OnNavigatedFrom(e);
    }
    
    #pragma endregion
    
  4. Remplaçons maintenant les implémentations des éléments LoadState et SaveState par ce 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);
    }
    

    Comme nous ne pouvons pas effectuer de liaison à un RichTextBlock, nous créons son contenu manuellement, au moyen de la classe TextHelper. Par souci de simplicité, nous utilisons la fonction HtmlUtilities::ConvertToText, qui extrait uniquement le texte du flux. En guise d’exercice, vous pouvez essayer d’analyser le code HTML ou XML vous-même et d’ajouter les liens d’image, ainsi que le texte, à la collection Blocks. L’élément SyndicationClient inclut une fonction pour l’analyse des flux XML. Certains flux sont constitués d’un code XML bien formé, d’autres non.

Hh465045.wedge(fr-fr,WIN.10).gifGestionnaires d’événements (page TextViewerPage de l’application Windows Phone)

  1. Dans TextViewerPage, vous accédez à la page WebViewerPage au moyen d’un lien hypertexte (Hyperlink) figurant dans le texte enrichi. Ce n’est généralement pas la méthode à suivre pour passer d’une page à une autre, mais elle semble appropriée dans ce cas : elle nous permet d’explorer le fonctionnement des liens hypertexte. Nous avons déjà ajouté la signature de fonction au fichier TextViewerPage.xaml.h. À présent, ajoutez l’implémentation dans le fichier TextViewerPage.xaml.cpp :

    ///<summary>
    /// Invoked when the user clicks on the "Link" text at the top of the rich text 
    /// view of the feed. This navigates to the web page. Identical action to using
    /// the App bar "forward" button.
    ///</summary>
    void TextViewerPage::RichTextHyperlinkClicked(Hyperlink^ hyperLink, 
        HyperlinkClickEventArgs^ args)
    {
        this->Frame->Navigate(WebViewerPage::typeid, m_feedItem->Link->AbsoluteUri);
    }
    
  2. Ensuite, définissez le projet Windows Phone en tant que projet de démarrage et appuyez sur F5. Vous devez être en mesure de cliquer sur un élément de la page de flux et d’accéder à la page TextViewerPage sur laquelle vous pouvez lire le billet de blog. Il y a des éléments intéressants dans ces blogs !

Ajout du code XAML (page SplitPage de l’application Windows)

L’application Windows se comporte différemment de l’application Windows Phone et ce, de plusieurs façons. Nous avons vu comment le fichier MainPage.xaml du projet Windows utilise un modèle ItemsPage, qui n’est pas disponible dans les applications Windows Phone. À présent, nous allons ajouter un SplitPage, qui n’est pas disponible dans les applications Windows Phone, lui non plus. Lorsqu’un périphérique est en orientation paysage, le SplitPage de l’application Windows présente un volet droit et un volet gauche. Lorsque l’utilisateur accède à la page de notre application, il voit apparaître la liste des éléments de flux dans le volet gauche, ainsi qu’un rendu au format texte du flux sélectionné dans le volet droit. Lorsque le périphérique se trouve en orientation portrait, ou si la fenêtre n’est pas affichée en pleine largeur, l’élément SplitPage utilise VisualStates pour se comporter comme s’il était constitué de deux pages distinctes. On parle de « navigation entre pages logiques » dans le code.

  1. Commençons par le code suivant. Il s’agit du code XAML d’une page fractionnée élémentaire utilisé comme modèle par défaut dans les projets Windows 8.

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

    Ajustons la grille titlePanel, afin qu’elle s’étende sur deux colonnes. Ainsi, le titre du flux s’affiche sur toute la largeur de l’écran :

    <Grid x:Name="titlePanel" Grid.ColumnSpan="2">
    
  3. À présent, recherchez l’élément TextBlock pageTitle dans la même grille et modifiez la liaison entre Title et Feed.Title.

    Text="{Binding Feed.Title}"
    
  4. Maintenant, recherchez le commentaire « Liste d’éléments à défilement vertical » et remplacez le contrôle ListView par défaut par celui-ci :

            <!-- Vertical scrolling item list -->
            <ListView
                x:Name="itemListView"
                AutomationProperties.AutomationId="ItemsListView"
                AutomationProperties.Name="Items"
                TabIndex="1"
                Grid.Row="1"
                Margin="10,10,0,0"
                Padding="10,0,0,60"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                IsSwipeEnabled="False"
                SelectionChanged="ItemListView_SelectionChanged"
                ItemTemplate="{StaticResource ListItemTemplate}">
    
                <ListView.ItemContainerStyle>
                    <Style TargetType="FrameworkElement">
                        <Setter Property="Margin" Value="0,0,0,10"/>
                    </Style>
                </ListView.ItemContainerStyle>
            </ListView>
    
  5. Le volet de détails d’un SplitPage peut contenir ce que vous souhaitez. Dans cette application, nous y placerons un élément RichTextBlock et afficherons une version en texte simple du billet de blog. Nous pouvons utiliser une fonction utilitaire fournie par l’API Windows pour analyser le code HTML à partir de l’élément FeedItem et renvoyer un élément Platform::String, avant d’utiliser notre propre classe utilitaire pour fractionner la chaîne retournée en paragraphes et créer des éléments de texte enrichi. Cette vue n’affiche aucune image, mais elle se charge rapidement. Si vous voulez étendre cette application, vous pouvez ajouter ultérieurement une option permettant à l’utilisateur de choisir la police et de régler sa taille.

    Recherchez l’élément ScrollViewer sous le commentaire « Détails de l’élément sélectionné » et supprimez-le. Ensuite, collez le balisage suivant :

            <!-- 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 et SaveState (page SplitPage de l’application Windows)

  1. Remplacez la page SplitPage que vous avez créée par le code suivant.

    Le fichier SplitPage.xaml.h doit ressembler à ceci :

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

    Pour le fichier SplitPage.xaml.cpp, utilisez le code suivant en tant que point de départ. Cela met en œuvre une page fractionnée élémentaire et ajoute la prise en charge de NavigationHelper et SuspensionManager comme vous l’avez ajoutée aux autres pages, ainsi que le gestionnaire d’événements SizeChanged comme dans une page précédente.

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

    using namespace Windows::UI::Xaml::Documents;
    
  3. Ensuite, remplacez LoadState et SaveState par le code suivant :

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

    Remarque : nous utilisons la méthode GetCurrentFeedAsync que nous avons préalablement ajoutée au projet partagé. Une différence entre cette page et la page Windows Phone est le fait que nous pouvons désormais effectuer un suivi de l’élément sélectionné. Après avoir accédé à SaveState, nous insérons l’élément actuellement sélectionné dans l’objet PageState, afin que SuspensionManager le rende persistant, si besoin est. Il sera à nouveau disponible pour nous dans l’objet PageState au moment de l’appel de LoadState. Nous avons besoin de cette chaîne pour rechercher l’élément FeedItem actuel dans le flux en cours.

Gestionnaires d’événements (page SplitPage de l’application Windows)

Lorsque l’élément sélectionné est modifié, le volet de détails utilise la classe TextHelper pour effectuer le rendu du texte.

  1. Dans le fichier SplitPage.xaml.cpp, ajoutez ces directives #include :

    #include "TextHelper.h"
    #include "WebViewerPage.xaml.h"
    
  2. Remplacez le stub de gestionnaire d’événements SelectionChanged par défaut par ce qui suit :

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

    Cette fonction spécifie un rappel qui sera transmis à un lien hypertexte, que nous créons dans le texte enrichi.

  3. Ajoutez cette fonction membre privée dans le fichier SplitPlage.xaml.h :

    void RichTextHyperlinkClicked(Windows::UI::Xaml::Documents::Hyperlink^ link, 
        Windows::UI::Xaml::Documents::HyperlinkClickEventArgs^ args);
    
  4. Ajoutez également l’implémentation suivante dans le fichier SplitPage.xaml.cpp :

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

    À son tour, cette fonction fait référence à la page suivante dans la pile de navigation. Désormais, vous pouvez appuyer sur F5 et voir le texte se mettre à jour lorsque la sélection change. Exécutez le simulateur et faites pivoter le périphérique virtuel pour constater que les objets VisualState par défaut gèrent les orientations portrait et paysage exactement comme prévu. Cliquez sur le texte Link figurant dans le texte du blog et accédez à la page WebViewerPage. Bien entendu, elle n’inclut aucun contenu pour l’instant ; nous verrons cela lorsque nous mettrons à jour le projet Windows Phone.

À propos de la navigation vers l’arrière

Vous avez peut-être remarqué que, dans l’application Windows, le SplitPage fournit un bouton de navigation vers l’arrière, qui vous permet de revenir à la page MainPage sans aucun codage supplémentaire de votre part. Sur le téléphone, la fonctionnalité du bouton Précédent est fournie par le bouton Précédent matériel, et non par le bouton Précédent logiciel. La navigation via le bouton Précédent du téléphone est gérée par la classe NavigationHelper dans le dossier Common. Recherchez « BackPressed » (Ctrl + Maj + F) dans votre solution pour afficher le code approprié. Là encore, vous n’avez rien de plus à faire à ce niveau : ça marche, tout simplement !

Partie 9 : Ajout d’un affichage web du billet sélectionné

La dernière page que nous allons ajouter affiche le billet de blog dans sa page web d’origine. Après tout, le lecteur peut souhaiter voir les images également ! L’affichage des pages web présente un inconvénient : le fait que le texte peut être difficile à lire sur un écran de téléphone, car les pages web ne sont pas toutes mises en forme correctement pour les périphériques mobiles. Parfois, les marges dépassent les bords de l’écran ; l’utilisateur doit alors faire défiler le texte à l’horizontale. Notre page WebViewerPage est relativement simple. Il nous suffit d’ajouter un contrôle WebView dans la page et de le laisser faire ! Commençons par le le projet Windows Phone :

Hh465045.wedge(fr-fr,WIN.10).gifAjout de balisage XAML (page WebViewerPage de l’application Windows Phone)

  • Dans le fichier WebViewerPage.xaml, ajoutez le panneau titre et une grille contentRoot :

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

Hh465045.wedge(fr-fr,WIN.10).gifLoadState et SaveState (page WebViewerPage de l’application Windows Phone)

  1. Commencez la page WebViewerPage comme toutes les autres pages, en fournissant la prise en charge de NavigationHelper et DefaultItems dans le fichier WebViewerPage.xaml.h et l’implémentation dans le fichier WebViewerPage.xaml.cpp.

    Le fichier WebViewerPage.xaml.h doit commencer comme suit :

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

    Le fichier WebViewerPage.xaml.cpp doit commencer comme suit :

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

    Windows::Foundation::Uri^ m_feedItemUri;
    
  3. Dans le fichier WebViewerPage.xaml.cpp, remplacez LoadState et SaveState par le code suivant :

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

    Notez l’animation gratuite au début de la fonction. Vous pouvez en savoir plus sur les animations en consultant le Centre de développement Windows. Une fois de plus, nous devons faire face aux deux méthodes possibles pour arriver à cette page. Si nous réactivons le système, nous devons consulter l’état.

C’est tout ! Appuyez sur la touche F5. Vous pouvez constater qu’il est possible de partir de la page TextViewerPage pour atteindre la page WebViewerPage !

Revenons à présent au projet Windows. La procédure que nous allons suivre sera très semblable à celle que nous avons exécutée pour le téléphone.

Hh465045.wedge(fr-fr,WIN.10).gifAjout de balisage XAML (page WebViewerPage de l’application Windows)

  1. Dans le fichier WebViewerPage.xaml, ajoutez un événement SizeChanged à l’élément Page et nommez-le « pageRoot_SizeChanged ». Placez le point d’insertion sur cet événement, puis appuyez sur F12 pour générer le code-behind.

  2. Recherchez la grille « Bouton Précédent et titre de la page » et supprimez le contrôle TextBlock. Le titre de la page s’affiche sur la page web ; il n’est pas nécessaire qu’il occupe de l’espace ici.

  3. Immédiatement après cette grille de bouton Précédent, ajoutez l’élément Border avec le contrôle WebView :

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

    Un contrôle WebView effectue une grande partie du travail gratuitement, mais il présente des particularités, qui le différencient des autres contrôles XAML et ce, de diverses manières. Vous devez vous informer sur ce contrôle si vous souhaitez l’utiliser souvent dans une application.

Hh465045.wedge(fr-fr,WIN.10).gifAjouter une variable membre

  1. Ajoutez la déclaration privée suivante dans le fichier WebViewerPage.xaml.h :

    Platform::String^ m_feedItemUri;
    

Hh465045.wedge(fr-fr,WIN.10).gifLoadState et SaveState (page WebViewerPage de l’application Windows)

  1. Remplacez les fonctions LoadState et SaveState par le code suivant, qui est très similaire à la page Windows Phone :

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

    \

  2. Définissez le projet Windows en tant que projet de démarrage et appuyez sur F5. Lorsque vous cliquez sur le lien dans la page TextViewerPage, vous devez aboutir à la page WebViewerPage ; lorsque vous cliquez sur le bouton Précédent de la page WebViewerPage, vous devez revenir à la page TextViewerPage.

Partie 10 : Ajout et suppression de flux

L’application fonctionne parfaitement sur Windows et sur Windows Phone, si l’on part du principe que l’utilisateur ne voudra lire que les trois flux que nous avons codés de manière irréversible dans cette application. Cependant, à titre d’étape finale, nous allons revenir sur Terre et permettre à l’utilisateur et de supprimer les flux de son choix. Nous allons lui montrer certains flux par défaut, afin que l’écran ne soit pas vierge lors du démarrage initial de l’application. Ensuite, nous allons ajouter des boutons pour lui permettre d’ajouter et de supprimer des flux. Bien entendu, nous devrons stocker la liste des flux de l’utilisateur, afin d’assurer leur persistance de session en session. C’est le bon moment pour en savoir plus sur les données d’application locales.

Dans un premier temps, nous devons stocker des flux par défaut lorsque l’application démarre pour la première fois. Or, au lieu de les coder de manière irréversible, nous pouvons les placer dans un fichier de ressources de chaînes, dans lequel ResourceLoader peut les trouver. Il faut que ces ressources soient compilées dans l’application Windows Phone et dans l’application Windows. Pour cela, nous allons créer le fichier .resw dans le projet partagé.

Hh465045.wedge(fr-fr,WIN.10).gifAjoutez des ressources de type chaîne :

  1. Dans l’Explorateur de solutions, sélectionnez le projet partagé, puis cliquez avec le bouton droit de la souris et ajoutez un nouvel élément. Dans le volet gauche, choisissez Ressource ; dans le volet central, choisissez Fichier de ressources (.resw). (Ne choisissez pas le fichier .rc, car il concerne les applications de bureau). Conservez le nom par défaut ou modifiez-le. Ensuite, cliquez sur Ajouter.

  2. Ajoutez les paires Nom/Valeur suivantes :

    Une fois que vous avez terminé, l’éditeur de ressources doit ressembler à ce qui suit.

    Ressources de type chaîne

Hh465045.wedge(fr-fr,WIN.10).gifAjoutez le code partagé pour insérer et supprimer des flux.

  1. À présent, nous allons ajouter le code permettant de charger les URL pointant vers la classe FeedDataSource. Dans le fichier feeddata.h, ajoutez cette fonction membre privée à FeedDataSource :

    concurrency::task<Windows::Foundation::Collections::IVector<Platform::String^>^> GetUserURLsAsync();
    
  2. Ajoutez ces instructions dans le fichier FeedData.cpp.

    using namespace Windows::Storage;
    using namespace Windows::Storage::Streams;
    
  3. Ensuite, ajoutez l’implémentation :

    /// <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 cherche à savoir si le fichier feeds.txt existe. Si tel n’est pas le cas, il crée et ajoute les URL à partir de ressources de chaînes. Les fichiers que l’utilisateur ajoute seront insérés dans le fichier feeds.txt. Toutes les opérations d’écriture de fichier étant asynchrones, nous utilisons une tâche et une continuation .then pour veiller à ce que le travail asynchrone soit effectué avant de tenter d’accéder aux données du fichier.

  4. Maintenant, remplacez l’ancienne implémentation InitDataSource par le code suivant, qui appelle GetUserURLsAsync :

    ///<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. Les fonctions permettant d’ajouter et de supprimer des flux sont les mêmes sur l’application Windows et l’application Windows Phone. Pour cela, nous allons les placer dans la classe App. Dans le fichier App.xaml.h,

  6. ajoutez ces membres internes :

    void AddFeed(Platform::String^ feedUri);
    void RemoveFeed(Platform::String^ feedUri);
    
  7. Dans le fichier App.xaml.cpp, ajoutez cet espace de noms :

    using namespace Platform::Collections;
    
  8. Dans le fichier App.xaml.cpp, procédez comme suit :

    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(fr-fr,WIN.10).gifAjouter le balisage XAML pour des boutons d’ajout et de suppression (Windows 8.1)

  1. Les boutons d’ajout et suppression de flux font partie de la page MainPage. Nous allons placer les boutons dans un élément TopAppBar dans l’application Windows, et dans un élément BottomAppBar dans l’application Windows Phone (ce type d’application n’inclut pas de barre d’application supérieure). Dans le projet Windows, accédez au fichier MainPage.xaml, puis ajoutez l’élément TopAppBar juste après le nœud Page.Resources :

    <Page.TopAppBar>
            <CommandBar x:Name="cmdBar" IsSticky="False" Padding="10,0,10,0">
    
                <AppBarButton x:Name="addButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Icon="Add">
                    <Button.Flyout>
                        <Flyout Placement="Top">
                            <Grid>
                                <StackPanel>
                                    <TextBox x:Name="tbNewFeed" Width="400"/>
                                    <Button Click="AddFeed_Click">Add feed</Button>
                                </StackPanel>
                            </Grid>
                        </Flyout>
                    </Button.Flyout>
                </AppBarButton>
    
                <AppBarButton x:Name="removeButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Icon="Remove"
                              Click="removeFeed_Click"/>
    
                <!--These buttons appear when the user clicks the remove button to 
                signal that they want to remove a feed. Delete removes the feed(s)  
                and returns to the normal visual state and cancel just returns 
                to the normal state. -->
                <AppBarButton x:Name="deleteButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Visibility="Collapsed"
                              Icon="Delete" Click="deleteButton_Click"/>
    
                <AppBarButton x:Name="cancelButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Visibility="Collapsed"
                              Icon="Cancel"
                              Click="cancelButton_Click"/>
            </CommandBar>
        </Page.TopAppBar>
    
  2. Dans le nom de chacun des quatre gestionnaires d’événement de clic (ajout, suppression, retrait et annulation), placez le curseur sur le nom du gestionnaire et appuyez sur F12 pour générer les fonctions dans le code-behind.

  3. Ajoutez ce deuxième VisualStateGroup dans l’élément <VisualStateManager.VisualStateGroups> :

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

Hh465045.wedge(fr-fr,WIN.10).gifAjoutez les gestionnaires d’événements pour l’ajout et la suppression de flux (Windows 8.1) :

  • Dans le fichier MainPage.xaml.cpp, remplacez quatre des stubs de gestionnaire d’événements par le code suivant :

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

    Appuyez sur la touche F5 en affectant le projet Windows en tant que projet de démarrage Windows. Vous pouvez constater que chacune de ces fonctions membres définit la propriété de visibilité associée aux boutons sur la valeur appropriée, puis accède ensuite à l’état visuel Normal.

Hh465045.wedge(fr-fr,WIN.10).gifAjouter le balisage XAML pour des boutons d’ajout et de suppression (Windows Phone 8.1)

  1. Ajoutez la barre d’application inférieure incluant les boutons après le nœud Page.Resources :

     <Page.BottomAppBar>
    
            <CommandBar x:Name="cmdBar" Padding="10,0,10,0">
    
                <AppBarButton x:Name="addButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Icon="Add"
                              >
                    <Button.Flyout>
                        <Flyout Placement="Top">
                            <Grid Background="Black">
                                <StackPanel>
                                    <TextBox x:Name="tbNewFeed" Width="400"/>
                                    <Button Click="AddFeed_Click">Add feed</Button>
                                </StackPanel>
                            </Grid>
                        </Flyout>
                    </Button.Flyout>
    
                </AppBarButton>
                <AppBarButton x:Name="removeButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Icon="Remove"
                              Click="removeFeed_Click"/>
    
    
                <!--These buttons appear when the user clicks the remove button to 
                signal that they want to remove a feed. Delete removes the feed(s)  
                and returns to the normal visual state. Cancel just returns to the normal state. -->
                <AppBarButton x:Name="deleteButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Visibility="Collapsed"
                              Icon="Delete" Click="deleteButton_Click"/>
    
    
                <AppBarButton x:Name="cancelButton" Height="95" Margin="20,0,20,0"
                              HorizontalAlignment="Right"
                              Visibility="Collapsed"
                              Icon="Cancel"
                              Click="cancelButton_Click"/>
            </CommandBar>
        </Page.BottomAppBar>
    
  2. Appuyez sur la touche F12 sur chacun des noms d’événement de clic pour générer le code-behind.

  3. Ajoutez l’élément VisualStateGroup « Cases à cocher » afin que le nœud VisualStateGroups se présente comme suit dans son ensemble :

    <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(fr-fr,WIN.10).gifAjouter des gestionnaires d’événement pour des boutons d’ajout et de suppression de flux (Windows Phone 8.1)

  • Dans le fichier MainPage.xaml.cpp (Windows Phone 8.1), remplacez les gestionnaires d’événements stub que vous venez de créer par le code suivant :

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

    Appuyez sur la touche F5 et tentez d’utiliser ces nouveaux boutons pour ajouter ou supprimer des flux ! Pour ajouter un flux sur le téléphone, cliquez sur un lien RSS d’une page web, puis cliquez sur Enregistrer. Ensuite, appuyez sur la zone d’édition qui porte le nom de l’URL, puis appuyez sur l’icône Copier. Revenez à l’application et placez le point d’insertion dans la zone d’édition, puis appuyez à nouveau sur l’icône Copier pour coller l’URL. Le flux doit s’afficher dans la liste des flux de manière quasi immédiate.

    L’application SimpleBlogReader présente désormais un état utilisable. Elle est prête pour un déploiement sur votre périphérique Windows.

Pour déployer l’application sur votre téléphone, vous devez d’abord l’enregistrer comme indiqué dans la section relative à l’inscription de votre Windows Phone.

Hh465045.wedge(fr-fr,WIN.10).gifPour déployer l’application sur un Windows Phone déverrouillé

  1. Créez une version Release.

    VS 2013 - Version Release Build - C++

  2. Dans le menu principal, sélectionnez Projet | Store| Créer des packages d’application. Vous ne souhaitez PAS déployer l’application sur le Windows Store dans cet exercice. Acceptez les valeurs par défaut dans l’écran suivant, sauf si vous avez une raison de les modifier.

  3. Si les packages ont été correctement créés, vous serez invité à exécuter le Kit de certification des applications Windows. Il peut être avisé de l’exécuter, afin de vérifier que l’application ne présente aucun défaut masqué qui risquerait d’empêcher son acceptation pas Windows Store. Cependant, comme nous ne déployons pas l’application sur Windows Store, cette étape est facultative.

  4. Dans le menu principal, sélectionnez Outils | Windows Phone 8.1 | Déploiement d’application. L’Assistant Déploiement d’application apparaît et dans le premier écran, le paramètre Cible doit indiquer « Périphérique ». Cliquez sur le bouton Parcourir pour accéder au dossier AppPackages figurant dans l’arborescence de votre projet, au même niveau que les dossiers Debug et Release. Accédez au dernier package de ce dossier (s’il en existe plusieurs) et double-cliquez dessus, puis cliquez sur le fichier appx ou appxbundle qu’il inclut.

  5. Assurez-vous que votre téléphone est branché à votre ordinateur et qu’il n’est pas bloqué par l’écran de verrouillage. Appuyez sur le bouton Déployer de l’Assistant et attendez la fin du déploiement. Cela ne devrait prendre que quelques secondes. Ensuite, un message doit s’afficher, indiquant que le déploiement a abouti. Recherchez l’application dans la liste Applications du téléphone et appuyez sur cette dernière pour l’exécuter.

    Remarque : l’ajout de nouvelles URL peut sembler un peu complexe de prime abord. Recherchez l’URL que vous voulez ajouter, puis appuyez sur le lien. À l’invite, indiquez que vous voulez l’ouvrir. Copiez l’URL du flux RSS (par exemple : ttp://feeds.bbci.co.uk/news/world/rss.xml), ET NON le nom de fichier XML temporaire qui s’affiche quand Internet Explorer ouvre le fichier. Si la page XML s’ouvre dans Internet Explorer, vous devez revenir à l’écran précédent de ce navigateur pour saisir l’URL voulue à partir de la barre d’adresses. Une fois que vous l’avez copiée, revenez au lecteur de blog simple (SimpleBlogReader) et collez-la dans la zone de texte Ajouter un flux, puis appuyez sur le bouton du même nom. Très rapidement, vous verrez apparaître le flux entièrement initialisé dans votre page principale. Voici un exercice pour vous, lecteur : implémentez un contrat de partage ou autre pour simplifier l’ajout de nouvelles URL à SimpleBlogReader. Bonne lecture !

Et après ?

Ce didacticiel explique comment utiliser les modèles de page intégrés de Microsoft Visual Studio Express 2012 pour Windows 8 afin de générer une application de plusieurs pages et comment naviguer et passer des données entre les pages. Nous avons appris à utiliser des styles et des modèles afin que notre application corresponde à l’apparence du site des blogs de l’équipe Windows. Nous avons également appris à utiliser des animations de thème et une barre de l’application pour que notre application corresponde à une application du Windows Store. Enfin, nous avons appris à adapter notre application aux diverses dispositions et orientations pour obtenir un résultat esthétique optimal.

Notre application est pratiquement en mesure d’être proposée au Windows Store. Pour plus d’informations sur la façon d’envoyer une application au Windows Store, voir :

Rubriques associées

Feuille de route pour les applications Windows Runtime en C++