Crear una aplicación de la Tienda para la lectura de blogs (C++)

Aquí tienes una explicación completa de cómo utilizar C++ y XAML para desarrollar una aplicación de la Tienda que se puede implementar en Windows 8.1 o Windows Phone 8.1. La aplicación puede leer blogs de fuentes RSS 2.0 o Atom 1.0.

Este tutorial asume que ya estás familiarizado con los conceptos de las lecciones 1-4 en Crear la primera aplicación de la Tienda Windows con C++.

Para estudiar la versión acabada de esta aplicación, puedes descargarla del sitio web de la Galería de código de MSDN.

En este tutorial usaremos Visual Studio Express 2013 con Update 2 o posterior. Si utilizas otra edición de Visual Studio, los comandos de menú podrían ser ligeramente diferentes.

Consulta los siguientes temas para acceder a tutoriales en otros lenguajes de programación:

Objetivos

Este tutorial está diseñado para que aprendas cómo crear aplicaciones de la Tienda Windows de varias páginas, y cómo y cuándo usar las extensiones de componente de Visual C++ (C++/CX) para simplificar el trabajo de codificación en función de Windows en tiempo de ejecución. También aprenderás a usar la clase concurrency::task para consumir API de Windows en tiempo de ejecución asincrónicas.

El tutorial no muestra cómo usar Expression Blend para crear la interfaz de usuario, En su lugar muestra el marcado XAML directamente.

La aplicación SimpleBlogReader tiene estas características:

  • Accede a datos de fuentes RSS y Atom a través de Internet.
  • Muestra una lista de fuentes y títulos de fuentes.
  • Proporciona dos formas de leer una publicación: como texto simple o como página web.
  • Admite la Administración del ciclo de vida de los procesos (PLM), y guarda y vuelve a cargar correctamente su estado si el sistema la apaga mientras hay otra tarea en primer plano.
  • Se adapta a los diferentes tamaños de ventana y orientaciones de dispositivo (horizontal o vertical).
  • Los usuarios pueden agregar y quitar las fuentes.

Parte 1: Configurar el proyecto

Para empezar, usaremos la plantilla de aplicación vacía (aplicaciones universales) de C++ para crear un proyecto.

Hh465045.wedge(es-es,WIN.10).gifPara crear un proyecto

  • En Visual Studio, elige Archivo > Nuevo > Proyecto, selecciona Instalado > Visual C++ > Aplicaciones de la Tienda > Aplicaciones universales. En el panel central, elige y luego selecciona la plantilla Aplicación vacía (aplicaciones universales). Pon el nombre “SimpleBlogReader” a la solución. Para obtener instrucciones más detalladas, consulta Hello World en C++.

    Una vez creado el proyecto, tendrás una solución con tres proyectos: el proyecto de Windows 8.1, el proyecto de Windows Phone 8.1 y un proyecto compartido. En realidad, este último consta de una sola carpeta con archivos pertenecientes al proceso de compilación de los otros proyectos y, además, no genera binarios. Iremos trabajando en los tres proyectos de forma más o menos paralela para poner de relieve las diferencias y similitudes entre los dos tipos de proyectos. Si no te gusta este método, puedes realizar primero todos los pasos de uno u otro proyecto.

Empecemos por agregar todas las páginas. Es más fácil agregarlas todas a la vez ya que, al empezar a escribir el código, cada página debe tener una directiva #include con la página a la que navega.

Hh465045.wedge(es-es,WIN.10).gifAgregar las páginas de la aplicación para Windows

  1. En realidad, el primer paso es destruir. En el proyecto de Windows 8.1, haz clic con el botón secundario en MainPage.xaml y elige Quitar. A continuación, haz clic en Eliminar para eliminar permanentemente el archivo y los archivos de código subyacente. Se trata de un tipo de página en blanco que no admite la navegación que necesitamos. Ahora haz clic con el botón secundario en el nodo de proyecto y elige Agregar > Nuevo elemento. Agregar un elemento nuevo en Visual C++
  2. En el panel izquierdo elige XAML y en el panel central elige Página de elementos. Llámalo MainPage.xaml y haz clic en Aceptar. Verás un cuadro de mensaje en el que se te pregunta si estás de acuerdo en agregar archivos nuevos al proyecto. Haz clic en . En el código de inicio debemos hacer referencia a las clases SuspensionManager y NavigationHelper que están definidas en esos archivos, y que Visual Studio pone en una nueva carpeta común.
  3. Agrega un SplitPage y acepta el nombre predeterminado.
  4. Agrega un BasicPage y llámalo WebViewerPage.

Más tarde agregaremos los elementos de interfaz de usuario a esas páginas.

Hh465045.wedge(es-es,WIN.10).gifAgregar las páginas de la aplicación para Windows Phone

  1. En el Explorador de soluciones, expande el proyecto de Windows Phone 8.1. Haz clic con el botón secundario en MainPage.xaml, elige Quitar > Eliminar permanentemente.
  2. Agrega un nuevo XAML de Página básica y llámalo MainPage.xaml. Haz clic en al igual que en el proyecto de Windows.
  3. Observarás que la variedad de plantillas de página es más limitada en el proyecto del teléfono; esto se debe a que en esta aplicación solamente utilizamos las páginas básicas. Agrega otras tres páginas básicas y llámalas FeedPage, TextViewerPage y WebViewerPage.

Parte 2: Crear un modelo de datos

Las aplicaciones de la Tienda basadas en plantillas de Visual Studio encarnan en cierto modo una arquitectura MVVM. En nuestra aplicación, el modelo se compone de clases que encapsulan las fuentes del blog. Cada página XAML de la aplicación representa una vista particular de los datos. Asimismo, cada clase de página tiene su propio modelo de vista, que es una propiedad denominada DefaultViewModel y de tipo Map<String^,Object^>. Esta asignación almacena los datos a los que están enlazados los controles XAML de la página y actúa como contexto de datos de la página.

Nuestro modelo consta de tres clases. La clase FeedData representa el URI de nivel superior y los metadatos para una fuente de blog. La fuente de http://blogs.windows.com/windows/b/buildingapps/rss.aspx es un ejemplo de lo que se encapsula en un FeedData. Una fuente tiene una lista de entradas de blog, representadas como objetos FeedItem. Cada FeedItem representa una entrada y contiene el título, el contenido, el URI y otros metadatos. La entrada que aparece en http://blogs.windows.com/windows/b/buildingapps/archive/2014/05/28/using-the-windows-phone-emulator-for-testing-apps-with-geofencing.aspx es un ejemplo de FeedItem. La primera página de nuestra aplicación es una vista de las fuentes, la segunda página es una vista de los FeedItems de una única fuente y las dos últimas páginas ofrecen vistas diferentes de una sola entrada: como texto sin formato o como página web.

La clase FeedDataSource contiene una colección de elementos FeedData junto con métodos para descargarlos.

En resumen:

  • FeedData contiene información sobre una fuente Atom o RSS.

  • FeedItem contiene información sobre las entradas de blog individuales en la fuente.

  • FeedDataSource contiene métodos para descargar las fuentes e inicializar nuestras clases de datos.

Definimos estas clases como clases de referencia pública (public ref class) que habilitan el enlace de datos, ya que los controles XAML no pueden interactuar con clases C++ estándar. El atributo Bindable lo utilizamos para indicarle al compilador XAML que queremos enlazar de forma dinámica con instancias de esos tipos. En una clase de referencia pública, los miembros de datos públicos se exponen como propiedades. Las propiedades que no tienen lógica especial no necesitan un getter ni un setter especificado por el usuario; el compilador los proporcionará. En la clase FeedData, observe cómo se utiliza Windows::Foundation::Collections::IVector para exponer un tipo de colección pública. Usamos la clase Platform::Collections::Vector de forma interna como el tipo determinado que implementa IVector.

Tanto el proyecto de Windows como el de Windows Phone utilizarán el mismo modelo de datos, así que pondremos las clases en el proyecto compartido.

Hh465045.wedge(es-es,WIN.10).gifPara crear clases de datos personalizadas

  1. En el Explorador de soluciones, accede al menú contextual del nodo de proyecto SimpleBlogReader.Shared y elige Agregar > Nuevo elemento. Selecciona la opción Archivo de encabezado (.h) y asígnale el nombre FeedData.h.

  2. Abre FeedData.h y pega en su interior el siguiente código. Observa la directiva #include de "pch.h": es nuestro encabezado precompilado y ahí pondremos los encabezados de sistema que prácticamente no cambian. De forma predeterminada, pch.h incluye collection.h, que es necesario para el tipo Platform::Collections::Vector, y ppltasks.h, que es necesario para concurrency::task y los tipos relacionados. Estos encabezados incluyen los <string> y <vector> necesarios para la aplicación, así que no tenemos que incluirlos explícitamente.

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

    Las clases son ref porque las clases XAML de Windows en tiempo de ejecución necesitan interactuar con ellas para enlazar datos a la interfaz de usuario. El atributo [Bindable] de esas clases también es necesario para el enlace de datos. Sin ese atributo, el mecanismo de enlace no las verá.

Parte 3: Descargar los datos

La clase FeedDataSource contiene los métodos que descargan las fuentes y tiene además otros métodos auxiliares. También contiene la colección de las fuentes descargadas que se agrega al valor "Items" en el DefaultViewModel de la página de la aplicación principal. FeedDataSource utiliza la clase Windows::Web::Syndication::SyndicationClient para realizar la descarga. Dado que las operaciones de red pueden llevar bastante tiempo, estas operaciones son asincrónicas. Cuando haya finalizado la descarga de una fuente, el objeto FeedData se inicializa y se agrega a la colección FeedDataSource::Feeds. Este es un IObservable<T>, lo que significa que la interfaz de usuario recibirá una notificación cuando se agregue un elemento y mostrará dicho elemento en la página principal. Para las operaciones asincrónicas utilizamos la clase concurrency::task, las clases relacionadas y los métodos de ppltasks.h. La función create_task se utiliza para encapsular las llamadas de función IAsyncOperation e IAsyncAction en la API de Windows. La función miembro task::then se utiliza para ejecutar código que debe esperar hasta que se complete la tarea.

Una característica interesante de la aplicación es que los usuarios no tienen que esperar a que todas las fuentes se descarguen. Pueden hacer clic en una fuente tan pronto como aparece e ir a una nueva página que muestra todos los elementos de esa fuente. Este ejemplo de interfaz de usuario "rápida y fluida" es posible gracias a que una gran parte del trabajo se realiza en subprocesos en segundo plano. La veremos en acción después de agregar la página XAML principal.

No obstante, las operaciones asincrónicas son más complejas: la rapidez y la fluidez no son "gratuitas". Si has leído los tutoriales anteriores, sabrás que el sistema puede finalizar una aplicación que no está activa con el fin de liberar memoria y luego restaurarla cuando el usuario vuelve a ella. Cuando cerramos nuestra aplicación no guardamos todos los datos de fuentes, ya que esto ocuparía mucho almacenamiento y podrían acumularse datos obsoletos. Por ello, siempre descargamos las fuentes al iniciar. Sin embargo, eso nos obliga a tener en cuenta el escenario en el que la aplicación se reanuda e intenta mostrar de inmediato un objeto FeedData que todavía no ha acabado de descargarse. Tenemos que asegurarnos que no se intentan mostrar datos hasta que no están disponibles. En este caso no podemos usar el método then, pero sí un task_completed_event. Este evento evitará que el código trate de acceder a un objeto FeedData hasta que ese objeto no se haya cargado por completo.

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

  1. Agrega la clase FeedDataSource a FeedData.h:

    
        /// <summary>
        /// A FeedDataSource represents a collection of FeedData objects
        /// and provides the methods to retrieve the stores URLs and download 
        /// the source data from which FeedData and FeedItem objects are constructed.
        /// This class is instantiated at startup by this declaration in the 
        /// ResourceDictionary in app.xaml: <local:FeedDataSource x:Key="feedDataSource" /> 
        /// </summary>
        [Windows::UI::Xaml::Data::Bindable]
        public ref class FeedDataSource sealed
        {
        private:
            Platform::Collections::Vector<FeedData^>^ m_feeds;
            FeedData^ GetFeedData(Platform::String^ feedUri, WWS::SyndicationFeed^ feed);
            void DeleteBadFeedHandler(Windows::UI::Popups::UICommand^ command);
    
        public:
            FeedDataSource();
            property Windows::Foundation::Collections::IObservableVector<FeedData^>^ Feeds
            {
                Windows::Foundation::Collections::IObservableVector<FeedData^>^ get()
                {
                    return this->m_feeds;
                }
            }
            property Platform::String^ CurrentFeedUri;
    
            void InitDataSource();
            void RetrieveFeedAndInitData(Platform::String^ url, WWS::SyndicationClient^ client);
    
        internal:
            // This is used to prevent SplitPage from prematurely loading the last viewed page on resume.
            concurrency::task_completion_event<FeedData^> m_LastViewedFeedEvent;
        };
    
    
    
  2. Ahora crea un archivo llamado FeedData.cpp en el proyecto compartido y pégalo en este código:

    
    #include "pch.h"
    #include "FeedData.h"
    
    using namespace std;
    using namespace concurrency;
    using namespace SimpleBlogReader;
    using namespace Platform;
    using namespace Platform::Collections;
    using namespace Windows::Foundation;
    using namespace Windows::Foundation::Collections;
    using namespace Windows::Web::Syndication;
    using namespace Windows::Storage;
    using namespace Windows::Storage::Streams;
    
    FeedDataSource::FeedDataSource()
    {
        m_feeds = ref new Vector<FeedData^>();
        CurrentFeedUri = "";
    }
    
    ///<summary>
    /// Uses SyndicationClient to get the top-level feed object, then initialize 
    /// the app's data structures.
    ///</summary>
    void FeedDataSource::RetrieveFeedAndInitData(String^ url, SyndicationClient^ client)
    {
        // Create the async operation. feedOp is an 
        // IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^
        auto feedUri = ref new Uri(url);
        auto feedOp = client->RetrieveFeedAsync(feedUri);
    
        // Create the task object and pass it the async operation.
        // SyndicationFeed^ is the type of the return value that the feedOp 
        // operation will pass to the continuation. The continuation can run
        // on any thread.
        create_task(feedOp).then([this, url](SyndicationFeed^ feed) -> FeedData^
        {
            return GetFeedData(url, feed);
        }, concurrency::task_continuation_context::use_arbitrary())
    
            // Append the initialized FeedData object to the items collection.
            // This has to happen on the UI thread. By default, a .then
            // continuation runs in the same apartment that it was called on.
            // We can append safely to the Vector from multiple threads
            // without taking an explicit lock.
            .then([this, url](FeedData^ fd)
        {
            if (fd->Uri == CurrentFeedUri)
            {
                // By setting the event we tell the resuming SplitPage the data
                // is ready to be consumed.
                m_LastViewedFeedEvent.set(fd);
            }
    
            m_feeds->Append(fd);
    
        })
    
            // The last continuation serves as an error handler.
            // get() will surface any unhandled exceptions in this task chain.
            .then([this, url](task<void> t)
        {
            try
            {
                t.get();
            }
    
            catch (Platform::Exception^ e)
            {
                // Sometimes a feed URL changes (I'm talking to you, Windows blogs!)
                // This logic is not completely robust because it doesn't address
                // the case of one of our default URLs in the resources going stale.
                SyndicationErrorStatus status = SyndicationError::GetStatus(e->HResult);
                if (status == SyndicationErrorStatus::InvalidXml) {
    
                    auto handler = ref new Windows::UI::Popups::UICommandInvokedHandler(
                        [this, url](Windows::UI::Popups::IUICommand^ command)
                    {
                        auto app = safe_cast<App^>(App::Current);
                        auto title = app->GetTitleFromUri(url);
                        app->RemoveFeed(title);
                    });
                    
                    auto msg = ref new Windows::UI::Popups::MessageDialog(
                        "An invalid XML exception was thrown in " + url + 
                        ". Please make sure to use a URI that points to a RSS or Atom feed.");
                    auto cmd = ref new Windows::UI::Popups::UICommand(
                        ref new String(L"Delete Invalid Feed"), handler, 1);
                    msg->Commands->Append(cmd);
                    msg->ShowAsync();
                }
    
                if (status == SyndicationErrorStatus::Unknown) 
                {
                    Windows::Web::WebErrorStatus webError = 
                        Windows::Web::WebError::GetStatus(e->HResult);
                    // TODO: Provide a user friendly message, if there is something 
                    // they can do to fix the problem.               
                }
            }
        }); //end task chain
    }
    
    
    ///<summary>
    /// Retrieve the data for each atom or rss feed and put it into our custom data structures.
    ///</summary>
    void FeedDataSource::InitDataSource()
    {
        // Hard code some feeds for now. Later in the tutorial we'll improve this.
        auto urls = ref new Vector<String^>();
        urls->Append(L"http://sxp.microsoft.com/feeds/3.0/devblogs");
        urls->Append(L"http://blogs.windows.com/windows/b/bloggingwindows/rss.aspx");
        urls->Append(L"http://azure.microsoft.com/blog/feed");
    
        // Populate the list of feeds.
        SyndicationClient^ client = ref new SyndicationClient();
        for (auto url : urls)
        {
            RetrieveFeedAndInitData(url, client);
        }
    }
    
    ///<summary>
    /// Creates our app-specific representation of a FeedData.
    ///</summary>
    FeedData^ FeedDataSource::GetFeedData(String^ feedUri, SyndicationFeed^ feed)
    {
        FeedData^ feedData = ref new FeedData();
    
        // Store the Uri now in order to map completion_events 
        // when resuming from termination.
        feedData->Uri = feedUri;
    
        // Get the title of the feed (not the individual posts).
        // auto app = safe_cast<App^>(App::Current);
        TextHelper^ helper = ref new TextHelper();
    
        feedData->Title = helper->UnescapeText(feed->Title->Text);
        if (feed->Subtitle != nullptr)
        {
            feedData->Description = helper->UnescapeText(feed->Subtitle->Text);
        }
    
        // Occasionally a feed might have no posts, so we guard against that here.
        if (feed->Items->Size > 0)
        {
            // Use the date of the latest post as the last updated date.
            feedData->PubDate = feed->Items->GetAt(0)->PublishedDate;
    
            for (auto item : feed->Items)
            {
                FeedItem^ feedItem;
                feedItem = ref new FeedItem();
                feedItem->Title = helper->UnescapeText(item->Title->Text);
                feedItem->PubDate = item->PublishedDate;
    
                //Only get first author in case of multiple entries.
                item->Authors->Size > 0 ? feedItem->Author = 
                    item->Authors->GetAt(0)->Name : feedItem->Author = L"";
    
                if (feed->SourceFormat == SyndicationFormat::Atom10)
                {
                    // Sometimes a post has only the link to the web page
                    if (item->Content != nullptr)
                    {
                        feedItem->Content = helper->UnescapeText(item->Content->Text);
                    }
                    feedItem->Link = ref new Uri(item->Id);
                }
                else
                {
                    feedItem->Content = item->Summary->Text;
                    feedItem->Link = item->Links->GetAt(0)->Uri;
                }
                feedData->Items->Append(feedItem);
            };
        }
        else
        {
            feedData->Description = "NO ITEMS AVAILABLE." + feedData->Description;
        }
    
        return feedData;
    
    } //end GetFeedData
    
    
    
    
    
    
  3. Ahora pongamos una instancia de FeedDataSource en nuestra aplicación. En app.xaml.h, agrega una directiva #include para feedData.h para que los tipos sean visibles.

    
        #include "FeedData.h"
    
    
    
    • En el proyecto compartido, en App.xaml, agrega un nodo Application.Resources e incluye en él una referencia a FeedDataSource de forma que la página tenga esta apariencia:

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

      Este marcado hará que, al iniciarse la aplicación, se cree un objeto FeedDataSource al que se puede acceder desde cualquier página de la aplicación. Cuando se genere el evento OnLaunched, el objeto de aplicación llamará a InitDataSource para que la instancia de feedDataSource comience a descargar todos sus datos.

      El proyecto no se compilará todavía porque necesitamos agregar algunas definiciones de clase adicionales.

Parte 4: Controlar la sincronización de datos al reanudar la aplicación tras una finalización

Cuando la aplicación se inicia por vez primera y mientras el usuario navega por las páginas, no es necesario sincronizar el acceso a datos. Las fuentes solo aparecen en la primera página una vez que se inicializan y las otras páginas nunca intentan acceder a los datos hasta que el usuario hace clic en una fuente visible. Después de eso, todo el acceso es de solo lectura y nunca modificamos los datos de origen. Sin embargo, hay un escenario en el que se requiere sincronización: si la aplicación finaliza mientras está activa una página basada en una fuente concreta, esa página tendrá que volver a enlazar a los datos de la fuente cuando se reanude la aplicación. En este caso es posible que una página intente acceder a datos que aún no existen. Por lo tanto, necesitamos forzar de algún modo la página para que espere a que los datos estén listos.

Las siguientes funciones permiten a la aplicación recordar la fuente a la que estaba mirando. El método SetCurrentFeed almacena la fuente en la configuración local, de donde se puede recuperar incluso después de que la aplicación salga de la memoria. El método GetCurrentFeedAsync es el que nos interesa, porque tenemos que asegurarnos de que cuando volvemos y queremos volver a cargar la última fuente, no tratemos de hacerlo antes de esa fuente se haya recargado. Más adelante hablaremos de este código. Vamos a agregar el código a la clase App porque la llamaremos desde ambas aplicaciones, la de Windows y la del teléfono.

  1. En app.xaml.h agrega estas signaturas de método. La accesibilidad interna significa que solo se pueden consumir desde otro código C++ en el mismo espacio de nombres.

    
        internal:
        concurrency::task<FeedData^> GetCurrentFeedAsync();
        void SetCurrentFeed(FeedData^ feed); 
        FeedItem^ GetFeedItem(FeedData^ fd, Platform::String^ uri);
    
    
    
  2. A continuación, en app.xaml.cpp:

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

Parte 5: Convertir los datos en formularios que se puedan usar

Los datos sin procesar no tienen por qué estar necesariamente en un formulario utilizable. Una fuente RSS o Atom expresa su fecha de publicación como un valor numérico de RFC 822. Necesitamos una forma de convertir eso en texto que tenga sentido para el usuario. Para ello, vamos a crear una clase personalizada que implementa IValueConverter y acepta un valor de RFC833 como cadenas de entrada y salida para cada componente de la fecha. Más tarde, en el código XAML que muestra los datos, estableceremos un enlace al resultado de la clase DateConverter en vez de al formato de datos sin procesar.

Hh465045.wedge(es-es,WIN.10).gifAgregar un convertidor de fecha

  1. En el proyecto común, crea un nuevo archivo .h y agrega este código:

    
    
    //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. Ahora inclúyelo en una directiva #include en App.xaml.h:

    
    
    #include "DateConverter.h"
    
    
  3. Y crea una instancia de él en App.xaml en el nodo Application.Resources:

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

El contenido de la fuente se transmite por la red como texto en formato HTML o, en algunos casos, en formato XML. Para visualizar este contenido en un RichTextBlock, tenemos que convertirlo en texto enriquecido. La clase siguiente utiliza la función de Windows HtmlUtilities para analizar el código HTML y, a continuación, usa funciones <regex> para dividirlo en párrafos con el fin de que podamos compilar objetos de texto enriquecido. No podemos usar el enlace de datos en este escenario, así que no hay necesidad de que la clase implemente IValueConverter. Simplemente crearemos instancias locales del mismo en las páginas donde lo necesitamos.

Hh465045.wedge(es-es,WIN.10).gifAgregar un convertidor de texto

  1. En el proyecto compartido, agrega un nuevo archivo .h, asígnale el nombre TextHelper.h y agrega este código:

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

    Observa que nuestra clase TextHelper personalizada muestra algunas de las formas en que puedes utilizar ISO C++ (std::map, std::regex, std::wstring) de manera interna en una aplicación C++/CX. Vamos a crear instancias de esta clase de forma local en las páginas que la utilizan. Tan solo tenemos que incluirla una vez, en App.xaml.h:

    
    #include "TextHelper.h"
    
    
  3. Ahora deberías poder compilar y ejecutar ambas aplicaciones, la de Windows y la del teléfono, pero sin demasiadas expectativas por el momento. Para seleccionar el proyecto que se ejecuta cuando presionas F5, haz clic con el botón secundario en el nodo del proyecto y elige Establecer como proyecto de inicio.

Parte 6: Iniciar, suspender y reanudar la aplicación

El evento App::OnLaunched se desencadena cuando el usuario inicia la aplicación al presionar o hacer clic en su icono, y también cuando el usuario vuelve a la aplicación después de que el sistema la haya finalizado para liberar y asignar memoria a otras aplicaciones. En cualquier caso, siempre acudimos a Internet y recargamos los datos en respuesta a este evento. Sin embargo, hay otras acciones que solo necesitan invocarse en un caso u otro. Podemos deducir estos estados si observamos el rootFrame en combinación con el argumento LaunchActivatedEventArgs que se pasa a la función y luego hacemos lo correcto. Afortunadamente, la clase SuspensionManager que se agregó automáticamente con MainPage realiza la mayor parte del trabajo de guardar y restaurar el estado de la aplicación cuando esta se suspende y se reinicia. Nosotros simplemente tenemos que llamar a sus métodos.

  1. En app.xaml.cpp agrega esta directiva include:

    
    #include "Common\SuspensionManager.h"
    
    
  2. Agrega estas directivas de espacio de nombres:

    
    
    using namespace concurrency;
    using namespace SimpleBlogReader::Common;
    using namespace Windows::Storage;
    
    
  3. Ahora reemplaza la función existente con este código:

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

    Ten en cuenta que la clase App está en el proyecto compartido, así que el código que escribimos aquí se ejecutará tanto en la aplicación de Windows como en la de teléfono, salvo si se ha definido la macro WINAPI_FAMILY == WINAPI_FAMILY_PHONE_APP.

  4. El controlador OnSuspending es más simple. Se llama cuando el sistema cierra la aplicación, no cuando la cierra el usuario. Simplemente dejamos que SuspensionManager se encargue. Llamará al controlador de eventos SaveState en cada página de la aplicación y serializará todos los objetos que tengamos almacenados en el objeto PageState de cada. Luego, cuando se reanude la aplicación, restaurará los valores en las páginas. Mira SuspensionManager.cpp si quieres ver el código.

    Reemplaza el cuerpo actual de la función OnSuspending con este código:

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

En este punto podríamos iniciar la aplicación y descargar los datos de fuentes, pero no podríamos mostrarlos al usuario. Hagamos algo al respecto.

Parte 7: Agregar la primera página de interfaz de usuario (una lista de fuentes)

Cuando se abre la aplicación, queremos mostrar al usuario una colección de nivel superior de todas las fuentes que se han descargado. Los usuarios pueden hacer clic o presionar sobre un elemento de la colección para navegar a una fuente en particular que contendrá una colección de las entradas o los elementos de fuente. Ya hemos agregado las páginas. En la aplicación para Windows es una página de elementos que muestra un control GridView cuando el dispositivo está en horizontal y un control ListView cuando el dispositivo está en vertical. Los proyectos de teléfono no tienen una página de elementos, así que disponemos de una página básica a la que le agregamos un control ListView manualmente. La vista de lista se ajustará automáticamente cuando cambie la orientación del dispositivo.

Generalmente, en todas las páginas hay que realizar las mismas tareas básicas:

  • Agregar el marcado XAML que describe la interfaz de usuario y enlaza los datos
  • Agregar código personalizado para las funciones miembro LoadState y SaveState.
  • Controlar los eventos (al menos uno de ellos suele tener código que navega a la página siguiente)

Lo haremos por orden, empezando por el proyecto de Windows:

Agregar el marcado XAML (MainPage de la aplicación para Windows)

La página principal de la aplicación para Windows presenta cada uno de los objetos FeedData en un control GridView. Para describir la apariencia que deben tener los datos creamos un DataTemplate, que es un árbol XAML que se utilizará para presentar cada elemento. La única limitación a las posibilidades de DataTemplates en cuanto a diseños, fuentes, colores, etc. son tu imaginación y sensibilidad estilística. En esta página usaremos una plantilla sencilla que, presentada, tendrá la apariencia siguiente:

Elemento de fuente
  1. Un estilo XAML es como un estilo en Microsoft Word: una manera práctica de agrupar un conjunto de valores de propiedad en un elemento XAML, el "TargetType". Un estilo puede basarse en otro estilo. El atributo "x: Key" especifica el nombre que utilizamos para referirnos al estilo cuando lo utilizamos.

    Coloca esta plantilla y sus estilos auxiliares en el nodo Page.Resources de MainPage.xaml (Windows 8.1). Solo se utilizan en 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>
    
    
    

    Verás una línea ondulada roja debajo de GreenBlockBackgroundBrush de la que nos ocuparemos más tarde.

  2. Todavía en MainPage.xaml (Windows 8.1), elimina el elemento AppName local de la página para que no oculte el elemento global que vamos a agregar en el ámbito App.

  3. Agrega un CollectionViewSource en el nodo Page.Resources. Este objeto conecta nuestro control ListView con el modelo de datos:

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

    Ten en cuenta que el elemento Page ya tiene un atributo DataContext establecido en la propiedad DefaultViewModel de la clase MainPage. Esa propiedad la establecemos para que sea un FeedDataSource, con lo que CollectionViewSource busca y encuentra allí una colección Items.

  4. En App.xaml vamos a agregar una cadena de recurso global para el nombre de la aplicación, junto con algunos recursos adicionales a los que se hará referencia desde varias páginas de la aplicación. Al colocar aquí los recursos, no necesitamos definirlos por separado en cada página. Agrega estos elementos al nodo Resources en App.xaml:

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

MainPage muestra una lista de las fuentes. Cuando el dispositivo esté orientado en horizontal usaremos un control GridView, que permite el desplazamiento horizontal. En orientación vertical usaremos un control ListView, que permite el desplazamiento vertical. Queremos que el usuario pueda utilizar la aplicación en cualquier orientación. Es relativamente fácil implementar compatibilidad para cambios de orientación:

  • Agrega ambos controles a la página y establece ItemSource en el mismo collectionViewSource. Establece la propiedad Visibility de ListView en Collapsed para que no esté visible de forma predeterminada.
  • Crea un conjunto de dos objetos VisualState, uno que describa el comportamiento de la interfaz de usuario para la orientación horizontal y otro que describa el comportamiento para la orientación vertical.
  • Controla el evento Window::SizeChanged, que se desencadena cuando la orientación cambia o el usuario estrecha o ensancha la ventana. Examina el alto y el ancho del nuevo tamaño. Si el alto es mayor que el ancho, invoca el VisualState de orientación vertical. De lo contrario, invoca el estado de orientación horizontal.

Hh465045.wedge(es-es,WIN.10).gifAgregar GridView y ListView (Windows)

  1. En MainPage.xaml (Windows 8.1), agrega estos controles GridView y ListView justo después de la cuadrícula que contiene el botón de retroceso y el título de la página:

    
     <!-- Horizontal scrolling grid -->
            <GridView
                x:Name="itemGridView"
                AutomationProperties.AutomationId="ItemsGridView"
                AutomationProperties.Name="Items"
                TabIndex="1"
                Grid.RowSpan="2"
                Padding="116,136,116,46"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                SelectionMode="None"
                ItemTemplate="{StaticResource DefaultGridItemTemplate}"
                IsItemClickEnabled="true"
                IsSwipeEnabled="false"
                ItemClick="ItemGridView_ItemClick"  
                Margin="0,-10,0,10">
            </GridView>
    
            <!-- Vertical scrolling list -->
            <ListView
                x:Name="itemListView"
                Visibility="Collapsed"            
                AutomationProperties.AutomationId="ItemsListView"
                AutomationProperties.Name="Items"
                TabIndex="1"
                Grid.Row="1"
                Margin="-10,-10,0,0"      
                IsItemClickEnabled="True"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                IsSwipeEnabled="False"            
                ItemClick="ItemGridView_ItemClick"
                ItemTemplate="{StaticResource ListItemTemplate}">
    
                <ListView.ItemContainerStyle>
                    <Style TargetType="FrameworkElement">
                        <Setter Property="Margin" Value="2,0,0,2"/>
                    </Style>
                </ListView.ItemContainerStyle>
            </ListView>
    
    
    
  2. Ten en cuenta que ambos controles utilizan la misma función miembro para el evento ItemClick. Coloca el punto de inserción en uno de ellos y presiona F12 para generar automáticamente el stub del controlador de eventos. Más adelante agregaremos el código para él.

  3. Pega la definición de VisualStateGroups justo después de ListView, de modo que este sea el último elemento dentro de la cuadrícula raíz (no lo pongas fuera de la cuadrícula o no funcionará). Tenga en cuenta que hay dos estados, pero que solo uno se define de manera explícita. Esto se debe a que el estado DefaultLayout ya está descrito en el código XAML correspondiente a esta página.

    
    <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. Ahora ya tenemos la interfaz de usuario totalmente definida y simplemente tenemos que indicarle a la página lo que debe hacer cuando se cargue.

LoadState y SaveState (MainPage de la aplicación para Windows)

Las dos funciones miembro principales a las que debemos prestar atención en una página XAML son LoadState y (a veces) SaveState. En LoadState rellenamos los datos de la página y en SaveState guardamos los datos que serán necesarios para volver a rellenar la página en caso de que se suspenda la aplicación y se inicie de nuevo.

  • Reemplaza la implementación de LoadState con este código, que inserta los datos de fuente que cargó (o está cargando) el feedDataSource que creamos en el inicio y pone esos datos en nuestro ViewModel para esta página.

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

    No tenemos que llamar a SaveState para MainPage porque no hay nada que esta página deba recordar. Siempre muestra todas las fuentes.

Controladores de eventos (MainPage de aplicación para Windows)

Desde un punto de vista conceptual, todas las páginas residen dentro de Frame (marco). Es el Frame que utilizamos para navegar por las páginas. El segundo parámetro de una llamada de función Navigate se utiliza para pasar datos de una página a otra. Siempre que se suspende la aplicación, SuspensionManager almacena y serializa automáticamente todos los objetos que pasemos aquí para que los valores se puedan restaurar cuando se reanude la aplicación. El SuspensionManager predeterminado solo admite los tipos integrados String y Guid. Si necesitas una serialización más sofisticada, puedes crear un SuspensionManager personalizado. Aquí pasamos una cadena String que SplitPage utilizará para buscar la fuente actual.

Hh465045.wedge(es-es,WIN.10).gifPara navegar por clics en elementos

  1. Cuando el usuario hace clic en un elemento de la cuadrícula, el controlador de eventos recibe ese elemento, lo establece como "fuente actual" por si la aplicación se suspende más adelante y luego navega a la siguiente página. El título de la fuente se pasa a la página siguiente para que esa página pueda buscar los datos de la fuente. Este es el código que se debe pegar:

    
    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. Para que el código anterior se compile, necesitamos colocar la directiva #include SplitPage.xaml.h en la parte superior del archivo actual, MainPage.xaml.cpp (Windows 8.1):

    
    #include "SplitPage.xaml.h"
    
    

Hh465045.wedge(es-es,WIN.10).gifPara controlar el evento Page_SizeChanged (Windows 8.1)

  • En la ventana Esquema del documento (Ctrl + Alt + D), selecciona pageRoot y luego presiona Alt + Entrar para mostrar el panel Propiedades. Haz clic en el botón de evento en la parte superior derecha y luego baja hasta encontrar el evento SizeChanged. Coloca el cursor en el cuadro de texto y presiona Entrar para crear un controlador de eventos con el nombre predeterminado pageRoot_SizeChanged. Reemplaza la implementación del controlador en el archivo cpp por este código:

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

    El código es sencillo. Si ahora ejecutas la aplicación en el simulador y giras el dispositivo, verás que la interfaz de usuario cambia entre GridView y ListView.

Agregar XAML (MainPage de la aplicación para teléfono)

Ahora pondremos en funcionamiento la página principal de la aplicación para teléfono. Aquí habrá mucho menos código, ya que utilizaremos todo el código que pusimos en el proyecto compartido. Además, las aplicaciones para teléfono no admiten controles GridView porque las pantallas son demasiado pequeñas para que funcione bien. Por lo tanto, vamos a usar un control ListView que se ajustará automáticamente a la orientación horizontal sin necesidad de ningún cambio en VisualState. Empezaremos por agregar el atributo DataContext en el elemento Page. En una página básica de teléfono esto no se genera de manera automática como en un ItemsPage o SplitPage.

  1. En MainPage.xaml (Windows Phone 8.1), en el elemento Page de apertura y justo después del atributo x:Name, agrega el atributo DataContext:

    
    DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
    
    
  2. Sin salir de MainPage.xaml (Windows Phone 8.1), justo después del elemento Page de apertura, agrega un nodo Page.Resources que contenga un CollectionViewSource que enlazará a la colección Items de FeedDataSource:

    
    <Page.Resources>
        <!-- Collection of items displayed by this page -->
        <CollectionViewSource x:Name="itemsViewSource" Source="{Binding Items}"/>
    </Page.Resources>
    
    
    
  3. Todavía en MainPage.xaml (Windows Phone 8.1), baja por la página, busca el "Panel de título" y quita todo el elemento StackPanel. En el teléfono, necesitamos que las fuentes de blog se enumeren en el estado real la pantalla.

  4. Más abajo en la página verás un Grid con este comentario: "TODO: Content should be placed within the following grid". Pon este ListView dentro de ese Grid:

    
        <!-- Vertical scrolling item list -->
         <ListView
            x:Name="itemListView"           
            AutomationProperties.AutomationId="itemListView"
            AutomationProperties.Name="Items"
            TabIndex="1" 
            IsItemClickEnabled="True"
            ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
            IsSwipeEnabled="False"
            ItemClick="ItemListView_ItemClick"
            SelectionMode="Single"
            ItemTemplate="{StaticResource ListItemTemplate}">
    
            <ListView.ItemContainerStyle>
                <Style TargetType="FrameworkElement">
                    <Setter Property="Margin" Value="2,0,0,2"/>
                </Style>
            </ListView.ItemContainerStyle>
        </ListView>
    
    
    
  5. Ahora coloca el cursor sobre el evento ItemListView_ItemClick y presiona F12 (ir a definición). Visual Studio generará una función de controlador de eventos vacía a la que agregaremos algo de código más tarde. Por ahora solo necesitamos que se genere la función para que la aplicación se compile.

LoadState y SaveState (MainPage de la aplicación para teléfono)

  1. En primer lugar, en la parte superior de MainPage.xaml.cpp (Windows Phone 8.1), agrega una instrucción #include:
    
    #include "FeedPage.xaml.h"
    
    
  2. Sin salir de MainPage.xamp.cpp, reemplaza la implementación de la función LoadState por este código (que debería sonarnos del archivo MainPage.xaml.cpp de Windows):


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


Al igual que en la aplicación para Windows, no haremos nada con SaveState.

Controladores de eventos (MainPage de la aplicación para teléfono)

Cuando el usuario hace clic en una fuente de MainPage, la aplicación navega a FeedPage, donde se enumeran todas las publicaciones de esa fuente. Cuando navegamos, pasamos la fuente seleccionada a FeedPage, que la recibe en su función LoadState. También guardamos la fuente para poder verla más tarde si es necesario; volveremos a tratar esto en el paso siguiente.

  • Reemplaza el stub del controlador de eventos vacío por este código:

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

Ya hemos acabado con la primera página. En el Explorador de soluciones, haz clic con el botón secundario en el nodo del proyecto de Windows Phone 8.1 y elige Establecer como proyecto de inicio. Ahora presiona F5 para iniciar la aplicación en el emulador de teléfono; deberías ver algo parecido a esto (el orden puede ser diferente):

Página principal de teléfono

Obviamente, la aplicación para teléfono está utilizando el mismo código de inicio en FeedDataSource al que se hace referencia en app.xaml. La apariencia de los elementos de la lista se obtiene del ListItemTemplate que agregamos a app.xaml en un paso anterior. Esa plantilla inserta el DateBlockTemplate para generar el cuadrado verde distintivo que muestra la fecha de la fuente. Si te fijas en el código de plantilla, verás cómo está enlazado a los valores de texto que genera la clase DateConverter. Esa plantilla también muestra cómo realizar un posicionamiento preciso de los elementos de texto dentro de un área restringida. Juega con los valores de posición para mejorar la apariencia de los cuadrados.

Parte 8: Enumerar las publicaciones y mostrar el texto de una publicación seleccionada

En esta parte agregamos dos páginas a la aplicación del teléfono: la página en la que se enumeran las publicaciones y la página que muestra la versión en texto de una publicación seleccionada. En la aplicación para Windows, solo tenemos que agregar una página llamada SplitPage que mostrará la lista en un lado y el texto de la publicación seleccionada en el otro. Primero las páginas de teléfono.

Agrega el marcado XAML (FeedPage de la aplicación para teléfono)

Sigamos con el proyecto del teléfono. Ahora trabajaremos en el FeedPage, que enumera las publicaciones de la fuente que el usuario selecciona.

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

  1. En FeedPage.xaml (Windows Phone 8.1), agrega un contexto de datos al elemento Page:

    
    DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
    
    
  2. Ahora agrega CollectionViewSource después del elemento Page de apertura:

    
    <Page.Resources>
        <!-- Collection of items displayed by this page -->
        <CollectionViewSource
        x:Name="itemsViewSource"
        Source="{Binding Items}"/>
    </Page.Resources>
    
    
    
  3. Reemplaza el panel título por este StackPanel:

    
            <!-- TitlePanel -->
            <StackPanel Grid.Row="0" Margin="24,17,0,28">
                <TextBlock Text="{StaticResource AppName}" 
                           Style="{ThemeResource TitleTextBlockStyle}" 
                           Typography.Capitals="SmallCaps"/>
            </StackPanel>
    
    
    
  4. A continuación, agrega el ListView dentro de la cuadrícula ContentRoot (justo después del elemento de apertura):

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

    Ten en cuenta que la propiedad ItemsSource de ListView enlaza con CollectionViewSource. Este, a su vez, enlaza con la propiedad FeedData::Items que insertamos en la propiedad DefaultViewModel de LoadState en el código subyacente (véase más abajo).

  5. Hay un evento ItemClick declarado en el ListView. Pon el cursor sobre él y presiona F12 para generar el controlador de eventos en el código subyacente. Por el momento lo dejaremos en blanco.

LoadState y SaveState (FeedPage de la aplicación para teléfono)

En MainPage no tuvimos que preocuparnos por almacenar el estado, ya que la página se reinicializa por completo desde Internet siempre que la aplicación se inicia por cualquier motivo. En cambio, las otras páginas necesitan recordar su estado. Por ejemplo, si la aplicación finaliza (se descarga de la memoria) mientras se muestra FeedPage, queremos que, cuando el usuario vuelva a ella, tenga la impresión de que nunca se cerró. Por eso debemos recordar la fuente que estaba seleccionada. El lugar para guardar estos datos es el almacenamiento AppData local, y un buen momento para hacerlo es cuando el usuario hace clic en la fuente desde MainPage.

Solo hay un problema: ¿realmente ya existen los datos? Si navegamos a FeedPage desde MainPage mediante un clic de usuario, sabremos con seguridad que el objeto FeedData seleccionado ya existe, porque de lo contrario no aparecerá en la lista de MainPage. Sin embargo, si la aplicación se está reanudando, puede que el último objeto FeedData visto aún no se haya cargado cuando FeedPage intenta enlazar con él. Por tanto, FeedPage (y otras páginas) necesita de saber de algún modo cuándo está disponible el FeedData. concurrency::task_completion_event está diseñado para tal situación. Con él podemos obtener el objeto FeedData de forma segura y en la misma ruta de código, independientemente de si estamos reanudando o navegando por vez primera desde MainPage. Desde FeedPage siempre obtenemos nuestra fuente con una llamada a GetCurrentFeedAsync. Si navegamos desde MainPage, el evento ya se estableció cuando el usuario hizo clic en una fuente, por lo que el método devolverá la fuente de inmediato. Si reanudamos tras una suspensión, el evento se establece en la función FeedDataSource::InitDataSource, y en ese caso FeedPage quizá tenga que esperar un poco a que la fuente se recargue. En este caso, es mejor esperar y no que se produzca un bloqueo. Esta pequeña complicación es el motivo de que en FeedData.cpp y App.xaml.cpp se incluya mucho código asincrónico que posiblemente tenga un aspecto aterrador, pero, si se examina detenidamente, se verá que tampoco es tan complicado. El mundo asincrónico tiene estas cosas.

  1. En FeedPage.xaml.cpp (Windows Phone 8.1), agrega este espacio de nombres para incluir los objetos de tarea:

    
    using namespace concurrency;
    
    
  2. Y una directiva #include para TextViewerPage.xaml.h:

    
    #include "TextViewerPage.xaml.h"
    
    

    La definición de la clase TextViewerPage es necesaria en la llamada a Navigate que se muestra a continuación.

  3. Reemplaza el método LoadState por este código:

    
    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 navegamos de vuelta a FeedPage desde una página situada más arriba en la pila de páginas, la página ya estará inicializada (es decir, DefaultViewModel tendrá un valor para "Feed") y la fuente actual estará establecida correctamente. Pero si navegamos hacia delante desde MainPage o estamos reanudando, tendremos que obtener la fuente actual para rellenar la página con los datos correctos. Si es necesario, GetCurrentFeedAsync esperará a que lleguen los datos de fuente después de reanudar. El contexto use_current() se especifica para indicarle a la tarea que vuelva al subproceso de interfaz de usuario antes de intentar acceder a la propiedad de dependencia DefaultViewModel. Por lo general no es posible acceder a los objetos relacionados con XAML directamente desde subprocesos en segundo plano.

    En esta página no hacemos nada con SaveState porque el estado se consigue con el método GetCurrentFeedAsync cada vez que se carga la página.

EventHandlers (FeedPage de la aplicación para teléfono)

En FeedPage controlamos el evento ItemClick, que avanza hasta la página donde el usuario puede leer la publicación. Ya creaste un controlador de stub al presionar F12 en el nombre del evento en el código XAML.

  1. Reemplaza ahora la implementación por este código.
    
    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. Presiona F5 para compilar y ejecutar la aplicación para teléfono en el emulador. Ahora, si seleccionas un elemento de MainPage, la aplicación debería navegar a FeedPage y mostrar una lista de las fuentes. El siguiente paso es mostrar el texto de una fuente seleccionada.

Hh465045.wedge(es-es,WIN.10).gifAgregar el marcado XAML (TextViewerPage de aplicación para teléfono)

  1. En el proyecto del teléfono, en TextViewerPage.xaml, reemplaza el panel de título y la cuadrícula de contenido por este marcado que mostrará el nombre de la aplicación (discretamente) y el título de la publicación actual, junto con una representación en texto simple del contenido:

    
     <!-- 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. En TextViewerPage.xaml.h, agrega un miembro privado. Lo usaremos para almacenar una referencia al elemento de fuente actual después de buscarlo por primero vez con la función GetFeedItem que agregamos a la clase App en el paso anterior.

    
    FeedItem^ m_feedItem;
    
    
  3. Agrega esta función privada que se invocará cuando el usuario hace clic en el vínculo del texto enriquecido:

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

Hh465045.wedge(es-es,WIN.10).gifLoadState y SaveState (TextViewerPage de aplicación para teléfono)

  1. En TextViewerPage.xaml.cpp, agrega esta directiva include:

    
    #include "WebViewerPage.xaml.h"
    
    
  2. Agrega estas dos directivas de espacio de nombres:

    
    using namespace concurrency;
    using namespace Windows::UI::Xaml::Documents;
    
    
    
  3. Ahora reemplaza las implementaciones de LoadState y SaveState con este código:

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

    Como no podemos enlazar a un RichTextBlock, construiremos su contenido manualmente con la clase TextHelper. Por simplicidad utilizamos la función HtmlUtilities::ConvertToText, que solo extrae el texto de la fuente. Como ejercicio, puedes intentar analizar el html o xml, y anexar los vínculos de imágenes y el texto a la colección Blocks. SyndicationClient tiene una función para analizar fuentes XML. Algunas fuentes tienen un formato XML correcto y otras no.

Hh465045.wedge(es-es,WIN.10).gifControladores de eventos (TextViewerPage de aplicación para teléfono)

  1. En TextViewerPage, navegamos a WebViewerPage por medio de un Hyperlink en RichText. Esto no es la forma habitual de navegar entre páginas, pero parece apropiada en este caso y nos permite explorar el funcionamiento de los hipervínculos. Ya hemos agregado la signatura de la función a TextViewerPage.xaml.h. Ahora agrega la implementación en 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. Ahora establece el proyecto de teléfono como proyecto de inicio y presiona F5. Deberías poder hacer clic en un elemento de la página de la fuente y navegar a TextViewerPage, donde puedes leer la entrada de blog. ¡No están nada mal esos blogs!

Agregar XAML (SplitPage de la aplicación para Windows)

El comportamiento de la aplicación para Windows y el de la aplicación para teléfono difiere en varios aspectos. Ya hemos visto que el MainPage.xaml del proyecto de Windows utiliza una plantilla de ItemsPage que no está disponible en aplicaciones para teléfono. Ahora vamos a agregar un SplitPage, que tampoco está disponible en el teléfono. Cuando un dispositivo está en orientación horizontal, el SplitPage de la aplicación para Windows tiene un panel derecho y un panel izquierdo. Cuando el usuario navega a la página de nuestra aplicación, verá la lista de elementos de fuente del panel izquierdo, además de una representación en texto de la fuente seleccionada actualmente en el panel derecho. Cuando el dispositivo está en orientación vertical o la ventana no ocupa todo el ancho, la página dividida utiliza VisualStates para comportarse como si se tratara de dos hojas separadas. Esto se denomina "navegación de página lógica" en el código.

  1. Ahora vayamos a SplitPage.xaml (Windows 8.1). La página predeterminada ya tiene su contexto de datos y conjunto CollectionViewSource.

    Vamos retocar la cuadrícula titlePanel para que se extienda por dos columnas. Esto permitirá que el título de la fuente ocupe todo el ancho de la pantalla:

    
    <Grid x:Name="titlePanel" Grid.ColumnSpan="2">
    
    
  2. Ahora busca pageTitle TextBlock en esta misma cuadrícula y cambia el Binding de Title a Feed.Title.

    
    Text="{Binding Feed.Title}"
    
    
  3. Ahora busca el comentario de "Lista de elementos de desplazamiento vertical" y cambia el valor predeterminado de ListView por este otro:

    
            <!-- Vertical scrolling item list -->
            <ListView
                x:Name="itemListView"
                AutomationProperties.AutomationId="ItemsListView"
                AutomationProperties.Name="Items"
                TabIndex="1"
                Grid.Row="1"
                Margin="10,10,0,0"
                Padding="10,0,0,60"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                IsSwipeEnabled="False"
                SelectionChanged="ItemListView_SelectionChanged"
                ItemTemplate="{StaticResource ListItemTemplate}">
    
                <ListView.ItemContainerStyle>
                    <Style TargetType="FrameworkElement">
                        <Setter Property="Margin" Value="0,0,0,10"/>
                    </Style>
                </ListView.ItemContainerStyle>
            </ListView>
    
    
    
  4. El panel de detalles de un SplitPage puede contener lo que quieras. En esta aplicación pondremos un RichTextBlock y mostraremos una versión en texto simple de la entrada de blog. Podemos usar una función de utilidad proporcionada por la API de Windows para analizar el código HTML de FeedItem y devolver un Platform::String; a continuación, usaremos nuestra propia clase de utilidad para dividir en párrafos la cadena devuelta y compilar elementos de texto enriquecido. Esta vista no mostrará ninguna imagen, pero se carga rápidamente y, si quieres extender esta aplicación, puedes agregar más adelante una opción que permita al usuario ajustar el tipo y el tamaño de fuente.

    Busca el elemento ScrollViewer debajo del comentario "Detalles del elemento seleccionado" y elimínelo. A continuación, pega este marcado:

    
            <!-- 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 y SaveState (SplitPage de la aplicación para Windows)

  1. En SplitPage.xaml.cpp, agrega esta directiva using:

    
    using namespace Windows::UI::Xaml::Documents;
    
    
    
  2. Ahora reemplaza LoadState y SaveState por este código:

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

    Ten en cuenta que estamos usando el método GetCurrentFeedAsync que agregamos anteriormente al proyecto compartido. Una diferencia entre esta página y la del teléfono es que ahora realizamos un seguimiento del elemento seleccionado. En SaveState, insertamos el elemento seleccionado actualmente en el objeto PageState; de este modo, SuspensionManager lo persistirá como necesario para que vuelva a estar disponible en el objeto PageState cuando se llame a LoadState. Vamos a necesitar esa cadena para buscar el elemento FeedItem en el Feed actual.

Controladores de eventos (SplitPage de aplicación para Windows)

Cuando el elemento seleccionado cambie, el panel de detalles utilizará la clase TextHelper para representar el texto.

  1. En SplitPage.xaml.cpp, agrega estas directivas #include:

    
    #include "TextHelper.h"
    #include "WebViewerPage.xaml.h"
    
    
    
  2. Reemplaza el stub de controlador de eventos SelectionChanged predeterminado por este:

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

    Esta función especifica una devolución de llamada que se pasará a un hipervínculo que creamos en el texto enriquecido.

  3. Agrega esta función de miembro privado en SplitPage.xaml.h:

    
    void RichTextHyperlinkClicked(Windows::UI::Xaml::Documents::Hyperlink^ link, 
        Windows::UI::Xaml::Documents::HyperlinkClickEventArgs^ args);
    
    
    
  4. Y esta implementación en 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);
        }
    }
    
    
    

    A su vez, esta función hace referencia a la página siguiente de la pila de navegación. Ahora puedes presionar F5 y ver el texto actualizado al cambiar la selección. Ejecuta el simulador y gira el dispositivo virtual para comprobar si los objetos VisualState predeterminados pueden adaptarse a las orientaciones vertical y horizontal según lo esperado. Haz clic en el texto de Link del blog y navega a WebViewerPage. Por supuesto, aún no tiene contenido, pero nos ocuparemos de eso en cuanto adelantemos el proyecto para teléfono.

Sobre la navegación hacia atrás

Quizá hayas observado que, en la aplicación para Windows, SplitPage proporciona un botón de navegación hacia atrás que nos lleva a la página MainPage sin necesidad agregar más código. En el teléfono, la funcionalidad del botón atrás la proporciona el correspondiente botón de hardware, no los botones de software. La navegación del botón atrás del teléfono se controla mediante la clase NavigationHelper en la carpeta Common. Busca "BackPressed" (Ctrl + Mayús + F) en la solución para ver el código correspondiente. Al igual que antes, no tienes que hacer nada más. ¡Funciona por sí mismo!

Parte 9: Agregar una vista web de la publicación seleccionada

La última página que agregaremos mostrará la entrada de blog en su página web original. Y es que, a veces, el lector también quiere ver las fotos. La desventaja de ver páginas web es que el texto puede ser difícil de leer en una pantalla de teléfono. Además, no todas las páginas web tienen un formato adaptado para dispositivos móviles. A veces los márgenes se extienden más allá del lateral de la pantalla y requieren una gran cantidad de desplazamiento horizontal. Nuestra página WebViewerPage es relativamente simple. Tan solo agregaremos un control WebView en la página y lo dejaremos trabajar. Empezaremos por el proyecto de teléfono:

Hh465045.wedge(es-es,WIN.10).gifAgregar el código XAML (WebViewerPage de aplicación para teléfono)

  • En WebViewerPage.xaml, agrega el panel de título y el Grid 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(es-es,WIN.10).gifLoadState y SaveState (WebViewerPage de aplicación para teléfono)

  1. En WebViewerPage.xaml.h, agrega esta variable de miembro privado:

    
    Windows::Foundation::Uri^ m_feedItemUri;
    
    
  2. En WebViewerPage.xaml.cpp, reemplaza LoadState y SaveState por este código:

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

    Observa la animación gratuita al comienzo de la función. Puedes encontrar más información sobre animaciones en el Centro de desarrollo de Windows. Ten en cuenta que de nuevo tenemos que tratar con las dos vías posibles para llegar a esta página. Si estamos arrancando, entonces debemos buscar nuestro estado.

Eso es todo. Presiona F5 y ya puedes navegar desde TextViewerPage a WebViewerPage.

Ahora vuelve al proyecto de Windows. Los pasos serán muy parecidos a los del teléfono.

Hh465045.wedge(es-es,WIN.10).gifAgregar el código XAML (WebViewerPage de aplicación para Windows)

  1. En WebViewerPage.xaml, agrega un evento SizeChanged al elemento Page y llámalo pageRoot_SizeChanged. Coloca el punto de inserción sobre él y presiona F12 para generar el código subyacente.

  2. Busca la cuadrícula "Botón Atrás y título de página" y elimina el TextBlock. El título de la página se mostrará en la página web y no nos ocupará espacio aquí.

  3. Ahora, inmediatamente después de la cuadrícula del botón atrás, agrega el Border con 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 control WebView desempeña una gran labor, pero tiene algunas peculiaridades que lo hacen diferente en algunos aspectos de otros controles XAML. Si vas a utilizarlo mucho en una aplicación, deberías leer sobre él.

Hh465045.wedge(es-es,WIN.10).gifAgregar variable de miembro

  1. Agrega la siguiente declaración privada en WebViewerPage.xaml.h:

    
    Platform::String^ m_feedItemUri;
    
    
    

Hh465045.wedge(es-es,WIN.10).gifLoadState y SaveState (WebViewerPage de aplicación para Windows)

  1. Reemplaza las funciones LoadState y SaveState por este código, que es muy parecido a la página de teléfono:

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

    El WebViewerPage es tan parecido en ambos proyectos que probablemente podríamos utilizar el mismo código XAML para ambas páginas y ponerlo en el proyecto compartido. Pero eso queda a tu elección.

  2. Establece el proyecto de Windows como proyecto de inicio y presiona F5. Si haces clic en el vínculo de TextViewerPage, deberías ir a la página WebViewerPage, y si haces clic en el botón atrás de WebViewerPage, deberías volver a TextViewerPage.

Parte 10: Agregar y quitar fuentes

A estas alturas la aplicación ya funciona muy bien tanto en Windows como en Windows Phone —eso sí, siempre que los usuarios no quieran leer otra cosa aparte de las tres fuentes que hemos codificado de forma rígida—. Pero como paso final, seamos prácticos y permitamos que los usuarios puedan agregar y eliminar fuentes de su elección. Les mostraremos algunas fuentes predeterminadas para que la pantalla no esté en blanco cuando inicien la aplicación por primera vez. A continuación, agregaremos algunos botones para que puedan agregar y eliminar fuentes. Naturalmente, deberemos almacenar la lista de fuentes de usuario para que persistan entre sesiones. Esto nos irá muy bien para aprender sobre los datos locales de aplicación.

Como primer paso, necesitaremos almacenar algunas fuentes predeterminadas para que se muestren la primera vez que se inicia la aplicación. Pero en vez de codificarlas de forma rígida, las pondremos en un archivo de recursos de cadena donde pueda encontrarlas el ResourceLoader. Necesitamos que esos recursos se compilen en la aplicación para Windows y en la aplicación para teléfono, así que vamos a crear el archivo .resw en el proyecto compartido.

Hh465045.wedge(es-es,WIN.10).gifAgregar recursos de cadena

  1. En el Explorador de soluciones, selecciona el proyecto compartido, haz clic con el botón secundario y agrega un elemento nuevo. En el panel izquierdo elige Recurso y después, en el panel central, elige Archivo de recursos (.resw). (No elijas el archivo .rc porque es para aplicaciones de escritorio). Deja el nombre predeterminado o asígnale cualquier otro nombre. A continuación, haz clic en Agregar.

  2. Agrega los siguientes pares de nombre-valor:

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

    Cuando acabes, el editor de recursos debería tener la apariencia siguiente.

    Recursos de cadena

Hh465045.wedge(es-es,WIN.10).gifAgregar código compartido para la adición y eliminación de fuentes

  1. Vamos a agregar el código para cargar las direcciones URL en la clase FeedDataSource. En feeddata.h, agrega esta función de miembro privado en FeedDataSource:

    
    concurrency::task<Windows::Foundation::Collections::IVector<Platform::String^>^> GetUserURLsAsync();
    
    
  2. Agrega estas instrucciones en FeedData.cpp.

    
    using namespace Windows::Storage;
    using namespace Windows::Storage::Streams;
    
    
    
  3. Y después agrega la implementación:

    
    
    /// <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 comprobará si existe el archivo feeds.txt. Si no existe, lo crea y agrega las direcciones URL de los recursos de cadena. Los archivos que el usuario agregue irán al archivo feeds.txt. Dado que todas las operaciones de escritura de archivos son asincrónicas, utilizamos una tarea y una continuación .then para asegurarnos de que el trabajo asincrónico se lleva a cabo antes de que intentemos acceder a los datos del archivo.

  4. Ahora reemplaza la antigua implementación de InitDataSource por este código que llama a GetUerURLsAsync:

    
    ///<summary>
    /// Retrieve the data for each atom or rss feed and put it into our custom data structures.
    ///</summary>
    void FeedDataSource::InitDataSource()
    {
        auto urls = GetUserURLsAsync()
            .then([this](IVector<String^>^ urls)
        {
            // Populate the list of feeds.
            SyndicationClient^ client = ref new SyndicationClient();
            for (auto url : urls)
            {
                RetrieveFeedAndInitData(url, client);
            }
        });
    }
    
    
  5. Las funciones para agregar y quitar fuentes son las mismas en Windows y Windows Phone, así que las pondremos en la clase App. En App.xaml.h,

  6. Agrega estos miembros internos:

    
    void AddFeed(Platform::String^ feedUri);
    void RemoveFeed(Platform::String^ feedUri);
    
    
    
    
  7. En App.xaml.cpp, agrega este espacio de nombres:

    
    using namespace Platform::Collections;
    
    
  8. En App.xaml.cpp:

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

Hh465045.wedge(es-es,WIN.10).gifAgregar el marcado XAML para la adición y eliminación de botones (8.1 de Windows)

  1. Los botones para agregar y quitar fuentes pertenecen a la página principal MainPage. Vamos a poner los botones en un TopAppBar en la aplicación para Windows y en un BottomAppBar en la aplicación para teléfono (las aplicaciones de teléfono no tienen una barra superior). En el proyecto de Windows, en MainPage.xaml:, agrega TopAppBar justo después del nodo 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. En cada uno de los cuatro nombres de controlador de eventos Click (add, remove, delete, cancel), pon el cursor sobre el nombre del controlador y presiona F12 para generar las funciones en el código subyacente.

  3. Agrega este segundo VisualStateGroup dentro del elemento <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(es-es,WIN.10).gifAgrega los controladores de eventos para agregar y quitar las fuentes (Windows 8.1):

  • En MainPage.xaml.cpp, reemplaza los cuatro stubs de controlador de eventos por este código:

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

    Presiona F5 con el proyecto de Windows como proyecto de inicio. Como verás, cada una de estas funciones miembro establece la propiedad de visibilidad de los botones en el valor apropiado y, a continuación, pasa al estado visual normal.

Hh465045.wedge(es-es,WIN.10).gifAgregar el marcado XAML para la adición y eliminación de botones (Windows Phone 8.1)

  1. Agrega la barra de aplicación inferior con los botones después del nodo 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. Presiona F12 en cada uno de los nombres de evento Click para generar el código subyacente.

  3. Agrega el VisualStateGroup "Checkboxes" para que todo el nodo VisualStateGroups tenga esta apariencia:

    
    <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(es-es,WIN.10).gifAgregar controladores de eventos para los botones de adición y eliminación de fuentes (Windows Phone 8.1)

  • En MainPage.xaml.cpp (WIndows Phone 8.1) reemplaza los controladores de eventos de stub que acabas de crear por este código:

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

    Presiona F5 e intenta utilizar los nuevos botones para agregar o quitar las fuentes. Para agregar una fuente en el teléfono, haz clic en un vínculo RSS de una página web y después selecciona Guardar. A continuación, presiona el cuadro de edición que tiene el nombre de la URL y después presiona el icono de copiar. Navega de nuevo a la aplicación, coloca el punto de inserción en el cuadro de edición y vuelve a presionar el icono de copiar para pegar la URL. La lista de fuentes debería mostrar la fuente casi de inmediato.

    La aplicación SimpleBlogReader ya se puede utilizar correctamente y está lista para implementarla en tu dispositivo de Windows.

Para implementar la aplicación en tu propio teléfono, primero hay que registrarla siguiendo las indicaciones del artículo sobre Registro de un Windows Phone.

Hh465045.wedge(es-es,WIN.10).gifPara implementar en un Windows Phone desbloqueado

  1. Crea una compilación de versión.

    VS 2013 Release Build C++
  2. En el menú principal, selecciona Proyecto | Tienda | Crear paquetes de aplicaciones. En este ejercicio NO queremos implementar la aplicación en la tienda. Acepte los valores predeterminados en la pantalla siguiente a menos que tenga una razón para cambiarlos.

  3. Si los paquetes se crearon correctamente, se te pedirá que ejecutes el kit para la certificación de aplicaciones en Windows (WACK, por sus siglas en inglés). Es interesante que hagas esto a fin de asegurarte de que la aplicación no tiene ningún defecto oculto que impida su aceptación en la tienda. Pero dado que no vamos a implementarla en la tienda, este paso es opcional.

  4. En el menú principal, selecciona Herramientas | Windows Phone 8.1 | Implementación de aplicación. Aparece el asistente de implementación de aplicación; en la primera pantalla, Destino debería decir "Dispositivo". Haz clic en el botón Examinar para navegar hasta la carpeta AppPackages del árbol de proyecto, que se encuentra en el mismo nivel que las carpetas Depurar y Liberar. Busca el paquete más reciente en esa carpeta (si hay más de uno) y haz doble clic en él. A continuación, haz clic en el archivo appx o appxbundle que contiene.

  5. Asegúrate de que tu teléfono está conectado al equipo y que no está bloqueado con la pantalla de bloqueo. Presiona el botón Implementar del asistente y espera a que finalice la implementación. Pasados unos segundos debería aparecer un mensaje indicando que la implementación se realizó correctamente. Busca la aplicación en la lista de aplicaciones del teléfono y pulsa en ella para ejecutarla.

    Nota: Agregar nuevas direcciones URL no es un trabajo muy intuitivo al principio. Busca una URL que quieras agregar y pulsa en el vínculo. Cuando se te pregunte, confirma que quieres abrirla. Copia la dirección URL del RSS, por ejemplo http://feeds.bbci.co.uk/news/world/rss.xml, NO el nombre de archivo xml temporal que aparece después de que IE abre el archivo. Si la página XML se abre en IE, tendrás que navegar a la pantalla anterior de IE para captar la URL en la barra de direcciones. Una vez que la hayas copiado, vuelve a Simple Blog Reader y pégala en el bloque de texto Agregar fuente. A continuación, presiona el botón "Agregar fuente". Verás que la fuente se ha inicializado completamente y aparece en tu página principal. Ejercicio para el lector: implementar un contrato para contenido compartido u otro medio destinado a simplificar la adición de nuevas direcciones URL a SimpleBlogReader. ¡Feliz lectura!

A continuación

En este tutorial hemos aprendido a usar las plantillas de página integradas de Microsoft Visual Studio Express 2012 for Windows 8 para compilar una aplicación de varias páginas, y a navegar y pasar datos entre las páginas. Hemos aprendido a usar los estilos y las plantillas para hacer que nuestra aplicación se ajuste a la personalidad del sitio web de The Windows Blog. También hemos aprendido a utilizar animaciones de tema y una barra de la aplicación para hacer que la aplicación se ajuste a la personalidad de una aplicación de la Tienda Windows. Y, por último, hemos aprendido a adaptar nuestras aplicaciones a diferentes diseños y orientaciones para que se vea siempre de la mejor manera.

Nuestra aplicación está casi lista para ser enviada a la Tienda Windows. Para obtener más información sobre cómo enviar una aplicación a la Tienda Windows, consulta:

Temas relacionados

Guía básica para crear aplicaciones de Windows en tiempo de ejecución con C++

 

 

Mostrar:
© 2015 Microsoft