Compartir a través de


Windows Phone

Entre bastidores: Una aplicación de lectura de fuentes para Windows Phone

Matt Stroshane

Descargar el ejemplo de código

Soy un adicto a las fuentes. Me encanta la magia de las fuentes RSS y Atom, la forma en que las noticias vienen a mi en vez de que sea al revés. Pero con un acceso cómodo a tanta información, consumirla de manera significativa se ha convertido en un verdadero desafío. Por esto, cuando supe que algunos practicantes de Microsoft estaban desarrollando una aplicación de lectura de fuentes para Windows Phone, me emocionó la forma en que abordaron el problema.

Como parte de su práctica, Francisco Aguilera, Suman Malani y Ayomikun (George) Okeowo tenían 12 semanas para desarrollar una aplicación para Windows Phone que incluyera algunas de las nuevas características de Windows Phone SDK 7.1. Al no tener experiencia en el desarrollo de Windows Phone, eran unos sujetos de prueba excelentes para nuestra plataforma, herramientas y documentación.

Después de evaluar las opciones, optaron por una aplicación de lectura de fuentes que ejemplificaría una base de datos local, Live Tiles y un agente en segundo plano. ¡Lograron demostrar mucho más que eso! En este artículo, voy lo guiaré paso a paso por cómo ellos usaron estas características. Así que instale Windows Phone SDK 7.1, descargue el código y póngalo en la pantalla. ¡Comencemos!

Uso de la aplicación

El concentrador central de la aplicación es la página central, MainPage.xaml (Ilustración 1). Consta de cuatro paneles de panorámicos: “what’s new,” “featured,” “all” y “settings” (“novedades,” “destacados,” “todo” y “configuración”). El panel “what’s new” muestra las actualizaciones más recientes de las fuentes. “Featured” muestra seis artículos que considera que nos interesarán según nuestro historial de lectura. El panel “all” enumera todas las categorías y fuentes. Para descargar artículos únicamente a través de Wi-Fi, podemos usar la configuración en el panel “settings”.

The Main Page of the App After Creating a Windows Phone News CategoryIlustración 1 Página principal de la aplicación después de crear una categoría de noticias de Windows Phone

Los paneles “what’s new” y “featured” proporcionan una forma de ir directamente al artículo. El panel “all” proporciona una lista de todas las categorías y fuentes. Desde el panel “all” podemos navegar hacia una colección de artículos que están agrupados por fuente o categoría. También podemos usar la barra de la aplicación en el panel “all” para agregar una nueva fuente o categoría. En la Ilustración 2 vemos cómo se relaciona la página principal con las otras ocho páginas de la aplicación.

The Page Navigation Map, with Auxiliary Pages in GrayIlustración 2 Mapa de navegación de la página, con las páginas auxiliares en gris

Semejante a un pivot, podemos desplazarnos horizontalmente en las páginas Category, Feed o Article (Categoría, Fuente o Artículo). Cuando nos encontramos en una de estas páginas, aparecen flechas en la barra de la aplicación (ver la Ilustración 3). Las flechas nos permiten mostrar los datos de la categoría, fuente o artículo anterior o siguiente de la base de datos. Por ejemplo, si estamos viendo la categoría Business (Negocios) en la página Category, al tocar la flecha “next” (“siguiente”) aparecerá la categoría Entertainment (Entretenimiento) en la página Category.

The Category, Feed and Article Pages with Their Application Bars ExpandedIlustración 3 Las páginas Category, Feed y Article con las barras de aplicación expandidas

Sin embargo, las teclas de flecha realmente no navegan hacia otra página Category. En vez de eso, la misma página se enlaza a una fuente de datos distinta. Al tocar el botón Back (Regresar) del teléfono volvemos al panel “all” sin la necesidad de ningún código de navegación especial.

Desde la página Article, podemos navegar a la página Share (Compartir) y enviar un vínculo a través de un mensaje, correo electrónico o red social. La barra de la aplicación también permite ver el artículo en Internet Explorer, “agregarlo a los favoritos” o eliminarlo de la base de datos.

Aspectos técnicos

Cuando abra la solución en Visual Studio, verá que es una aplicación C# dividida en tres proyectos:

  1. FeedCast: la parte que el usuario ve, la aplicación en primer plano (código View y ViewModel).
  2. FeedCastAgent: el código del agente en segundo plano (tarea programada periódicamente).
  3. FeedCastLibrary: el código de red y datos compartido.

El equipo usó Silverlight para el kit de herramientas de Windows Phone (noviembre de 2011) y Microsoft Silverlight 4 SDK. En la mayoría de las páginas de la aplicación se usan los controles del kit de herramientas (Microsoft.Phone.Controls.Toolkit.dll). Por ejemplo, se usan los controles HubTile para mostrar los artículos en el panel “featured” de la página principal. Para la red, el equipo usó System.ServiceModel.Syndication.dll de Silverlight 4 SDK. Este ensamblado no está incluido en Windows Phone SDK y no está optimizado especialmente para aplicaciones de teléfonos, pero los miembros del equipo descubrieron que les servía para sus propósitos.

El proyecto de la aplicación en primer plano, FeedCast, es el mayor de la solución. Nuevamente, esta es la parte que el usuario ve. Está organizado en nueve carpetas:

  1. Converters: convertidores de valor que salvan el vacío entre los datos y la interfaz de usuario.
  2. Icons: iconos usados en las barras de la aplicación.
  3. Images: imágenes usadas por HubTiles cuando los artículos no tienen imágenes.
  4. Libraries: los ensamblajes de kit de herramientas y de sindicación.
  5. Models: código relacionado con los datos que el agente en segundo plano no usa.
  6. Resources: archivos de recursos de localización en inglés y español.
  7. Themes: personalizaciones para el control HeaderedListBox.
  8. ViewModels: ViewModels y otras clases auxiliares.
  9. Views: código para cada página en la aplicación en primer plano.

Esta aplicación sigue el patrón Model-View-ViewModel (MVVM). El código en la carpeta Views se concentra principalmente en la interfaz de usuario. La lógica y los datos asociados con las páginas individuales están definidas por el código en la carpeta ViewModels. Si bien la carpeta Models contiene código relacionado con los datos, los objetos de datos se definen en el proyecto FeedCastLibrary. La aplicación en primer plano y el agente en segundo plano reutilizan el código “Model”. Para obtener más información sobre MVVM, consulte wpdev.ms/mvvmpnp.

El proyecto FeedCastLibrary contiene los datos y el código de red usado por la aplicación en primer plano y por el agente en segundo plano. Este proyecto contiene dos carpetas: Data y Networking. En la carpeta Datas, el “Model” FeedCast se describe por clases parciales en cuatro archivos: LocalDatabaseDataContext.cs, Article.cs, Category.cs y Feed.cs. El archivo DataUtils.cs contiene el código que realiza las operaciones comunes de base de datos. Una clase auxiliar para usar una configuración de almacenamiento aislado se encuentra en el archivo Settings.cs. La carpeta Networking del proyecto FeedCastLibrary contiene el código usado para descargar y analizar contenidos de la web y los más importantes de todos son los métodos Download en el archivo WebTools.cs.

Solo hay una clase en el proyecto FeedCastAgent, que es Scheduled­Agent.cs, que corresponde al código de agente en segundo plano. El método OnInvoke se llama durante la ejecución y el método SendToDatabase se llama al completar la descarga. Más adelante profundizaré en las descargas.

Base de datos local

Para alcanzar una productividad máxima, cada uno de los miembros del equipo se enfocó en un área diferente de la aplicación. Aguilera se enfocó en la interfaz de usuario, Views y ViewModels en la aplicación en primer plano. Okeowo trabajó en la red y en obtener datos de las fuentes. Malani trabajó en la arquitectura y las operaciones de la base de datos.

En Windows Phone, podemos almacenar los datos en una base de datos local. Es local porque corresponde a un archivo de base de datos que reside en el almacenamiento aislado (el depósito de almacenamiento del dispositivo de la aplicación, aislado de las otras aplicaciones). Básicamente, podemos describir las tablas de la base de datos como objetos CLR típicos, donde las propiedades de dichos objetos representan las columnas de la base de datos. Esto permite que cada objeto de la clase se almacene como una fila en la tabla correspondiente. Para representar la base de datos, debemos crear un objeto especial, llamado contexto de datos, que se hereda de System.Data.Linq.DataContext.

El ingrediente mágico de la base de datos local es el tiempo de ejecución LINQ to SQL; nuestro sirviente de datos. Podemos solicitar el método CreateDatabase del contexto de datos y LINQ to SQL crea el archivo .sdf en el almacenamiento aislado. Creamos consultas LINQ para especificar los datos que deseamos y LINQ to SQL devuelve objetos fuertemente tipados que podemos enlazar a la interfaz de usuario. LINQ to SQL administra todas las operaciones de base de datos de bajo nivel, permitiéndonos así enfocarnos en el código. Para obtener más información sobre el uso de la base de datos local, consulte wpdev.ms/localdb.

En vez de escribir todas las clases, Malani usó Visual Studio 2010 Ultimate para seguir un camino diferente. Ella creó las tablas de la base de datos en forma visual, con el cuadro de diálogo Agregar conexión del Explorador de servidores para crear una base de datos SQL Server CE y luego el cuadro de diálogo Nueva tabla para crear las tablas.

Una vez diseñado el esquema, usó SqlMetal.exe para generar un contexto de datos. SqlMetal.exe es una utilidad de línea de comando de LINQ to SQL de escritorio. Sirve para crear una clase de contexto de datos a partir de una base de datos SQL Server. El código que genera es bastante similar a un contexto de datos de Windows Phone. Al usar esta técnica, ella pudo crear las tablas en forma visual y generar el contexto de datos rápidamente. Para obtener más información sobre SqlMetal.exe, consulte wpdev.ms/sqlmetal.

En la Ilustración 4 podemos apreciar la base de datos que creó Malani. Las tres tablas principales son Category, Feed y Article (Categoría, Fuente y Artículo). Además, la tabla de vínculo Category_Feed, sirve crear una relación de varios a varios entre las categorías y las fuentes. Cada categoría se puede asociar con varias fuentes y cada fuente se puede asociar con varias categorías. Observe que la característica “favorite” de la aplicación solo es una categoría especial que no se puede eliminar.

The Database SchemaIlustración 4 Esquema de la base de datos

Sin embargo, el contexto de datos generado por SqlMetal.exe todavía contenía código incompatible con Windows Phone. Después de agregar el archivo de código de contexto de datos al proyecto de Windows Phone, Malani compiló el proyecto para identificar el código que no era válido. Recuerda que tuvo que eliminar un constructor, pero que el resto se compiló sin problemas.

Es posible que al revisar el archivo de contexto de datos LocalDatabase­DataContext.cs, se percate de que todas las tablas corresponden a clases parciales. El resto del código asociado con estas tablas (que no se generaron automáticamente con SqlMetal.exe) está almacenado en los archivos de código Article.cs, Category.cs y Feed.cs. Esta separación permite que Malani haga cambios en el esquema de la base de datos sin afectar las definiciones de método de extensibilidad que escribió a mano. Si no hubiera hecho eso, hubiera tenido que volver a agregar los métodos cada vez genere LocalDatabaseDataContext.cs automáticamente (ya que SqlMetal.exe sobrescribe todo el código del archivo).

Mantener la simultaneidad

Al igual que la mayoría de las aplicaciones para Windows Phone que tienen como objetivo proporcionar una experiencia fluida y con capacidad de respuesta, esta usa varios subprocesos simultáneos para realizar el trabajo. Además del subproceso de la interfaz de usuario, que acepta entradas del usuario, puede haber varios subprocesos en segundo plano que trabajen en la descarga y análisis de las fuentes RSS. Cada uno de estos subprocesos finalmente deberá realizar cambios en la base de datos.

Si bien la base de datos en sí ofrece un acceso simultáneo robusto, la clase DataContext no es segura para los subprocesos. En otras palabras, el objeto global único DataContext usado en esta aplicación no se puede compartir entre varios subprocesos sin agregar algún modelo de simultaneidad. Para abordar este problema, Malani usó las API de simultaneidad de LINQ to SQL y un objeto de exclusión mutua del espacio de nombre System.Threading.

En el archivo DataUtils.cs, se usan los métodos de exclusión mutua WaitOne y ReleaseMutex para sincronizar el acceso a los datos en los casos en que podría haber una contención entre las clases DataContext. Por ejemplo, si varios subprocesos simultáneos (de la aplicación en primer plano o del agente en segundo plano) solicitan el método SaveChangesToDB más o menos simultáneamente, el primer código que ejecute WaitOne primero podrá continuar. La solicitud WaitOne del otro no se completará hasta que el primer código llame ReleaseMutex. Por esta razón, resulta importante poner la llamada a ReleaseMutex en la instrucción finally al usar try/catch/finally en las operaciones de base de datos. Sin una llamada a ReleaseMutex, el otro código esperará en la llamada WaitOne hasta que salga el subproceso dueño. Desde la perspectiva del usuario, esto podría tardar “por siempre”.

En vez de un objeto único global DataContext, también podríamos diseñar la aplicación para crear y destruir objetos DataContext pequeños en función del subproceso. Sin embargo, los miembros del equipo determinaron que el método del DataContext global simplificaba el desarrollo. También debo mencionar que como la aplicación solo debía protegerse contra un acceso entre subprocesos y no contra el acceso entre procesos, también podrían haber usado un bloqueo en vez de la exclusión mutua. Es posible que con el bloqueo hubieran obtenido un rendimiento mejor.

Consumo de datos

Okeowo enfocó sus esfuerzos en proporcionarle los datos a la aplicación. El archivo WebTools.cs contiene el código donde ocurre gran parte de la acción. Pero la clase WebTools no solo se usa para descargar fuentes: también se usa en la página de fuentes nuevas para buscar fuentes nuevas en Bing. Esto lo logró al crear una interfaz común, IXmlFeedParser, y al abstraer el código de análisis en diferentes clases. La clase SynFeedParser analiza las fuentes y la clase SearchResultParser analiza los resultados de la búsqueda en Bing.

Sin embargo, la consulta a Bing realmente no devuelve artículos (a pesar de la colección de objetos Article devueltos por la interfaz IXmlFeedParser). En lugar de esto, devuelve una lista de nombres y URI de fuentes. ¿De qué se trata? Bueno, Okeowo notó que la clase Article ya tenía las propiedades que necesitaba para describir una fuente; no hacía falta que creara otra clase. Al analizar los resultados de la búsqueda, usó ArticleTitle para el nombre de la fuente y ArticleBaseURI para la URI de la fuente. Consulte SearchResultParser.cs en la descarga de código adjunta para conocer más al respecto.

El código en la nueva página ViewModel (NewFeedPageViewModel.cs en el código de ejemplo) muestra cómo se consumen los resultados de la búsqueda en Bing. En primer lugar se usa el método GetSearchString para armar la URI de la cadena de búsqueda de Bing a partir de los términos de búsqueda que el usuario escribe en NewFeedPage, tal como se aprecia en las siguientes líneas:

private string GetSearchString(string query)
{
  // Format the search string.
  string search = "http://api.bing.com/rss.aspx?query=feed:" + query +
    "&source=web&web.count=" + _numOfResults.ToString() +
    "&web.filetype=feed&market=en-us";
  return search;
}

El valor _numOfResults limita la cantidad de resultados de búsqueda que se devuelven. Para obtener más información sobre el acceso a Bing a través de RSS, consulte la página de la MSDN Library, “Acceso a Bing a través de RSS”, en bit.ly/kc5uYO.

El método GetSearchString se llama en el método GetResults, donde los datos se recuperan de Bing (consulte la Ilustración 5). El método GetResults luce como si estuviera al revés ya que enumera una expresión lambda que controla el evento AllDownloadsFinished “en línea”, antes de llamar el código para iniciar la descarga. Cuando se llama el método Download, el objeto WebTools consulta a Bing a través de la URI que se construyó con GetSearchString.

Ilustración 5 El método GetResults en NewFeedPageView­Model.cs solicita fuentes nuevas en Bing

public void GetResults(string query, Action<int> Callback)
{
  // Clear the page ViewModel.
  Clear();
  // Get the search string and put it into a feed.
  Feed feed = new Feed { FeedBaseURI = GetSearchString(query) };
  // Lambda expression to add results to the page
  // ViewModel after the download completes.
  // _feedSearch is a WebTools object.
  _feedSearch.AllDownloadsFinished += (sender, e) =>
    {
      // See if the search returned any results.
      if (e.Downloads.Count > 0)
      {
        // Add the search results to the page ViewModel.
        foreach (Collection<Article> result in e.Downloads.Values)
        {
          if (null != result)
          {
            Deployment.Current.Dispatcher.BeginInvoke(() =>
              {
                foreach (Article a in result)
                {
                  lock (_lockObject)
                  {
                    // Add to the page ViewModel.
                    Add(a);
                  }
                }
                Callback(Count);
              });
          }
        }
      }
      else
      {  
        // If no search results were returned.
        Deployment.Current.Dispatcher.BeginInvoke(() =>
          {
            Callback(0);
          });
      }
    };
  // Initiate the download (a Bing search).
  _feedSearch.Download(feed);
}

El agente en segundo plano también usa el método WebTools Download (consulte la Ilustración 6), pero de manera diferente. En vez de descargar de una sola fuente, el agente le pasa al método una lista de varias fuentes. Para recuperar los resultados, el agente adopta una estrategia diferente. En vez de esperar hasta que se descarguen los artículos de todas las fuentes (a través del evento AllDownloadsFinished), el agente guarda los artículos inmediatamente al terminar la descarga de la fuente (mediante el evento SingleDownloadFinished).

Ilustración 6 El agente en segundo plano inicia una descarga (sin comentarios de depuración)

protected override void OnInvoke(ScheduledTask task)
{
  // Run the periodic task.
  List<Feed> allFeeds = DataBaseTools.GetAllFeeds();
  _remainingDownloads = allFeeds.Count;
  if (_remainingDownloads > 0)
  {
    Deployment.Current.Dispatcher.BeginInvoke(() =>
      {
        WebTools downloader = new WebTools(new SynFeedParser());
        downloader.SingleDownloadFinished += SendToDatabase;
        try
        {
          downloader.Download(allFeeds);
        }
        // TODO handle errors.
        catch { }
      });
  }
}

La tarea del agente en segundo plano es mantener todas las fuentes actualizadas. Para esto, le entrega al método Download una lista de todas las fuentes. El agente en segundo plano solo cuenta con un tiempo limitado para ejecutarse; cuando este se acaba, el proceso se detiene inmediatamente. Así que a medida que el agente descarga las fuentes, envía los artículos a la base de datos, una fuente a la vez. De esta forma, el agente en segundo plano tiene una probabilidad mucho más alta de guardar los artículos nuevos antes de detenerse.

Los métodos Download de una fuente y de varias fuentes en realidad son sobrecargas para el mismo código. El código de descarga inicia HttpWebRequest para cada fuente (en forma asincrónica). En cuanto se devuelve la primera solicitud, llama al controlador de eventos SingleDownloadFinished. La información y los artículos de la fuente luego se empaquetan dentro del evento con SingleDownloadFinishedEventArgs. Como se muestra en la Ilustración 7, el método SendToDatabase está conectado con el método SingleDownloadFinshed. Cuando este termina, SendToDatabase saca los artículos desde los argumentos del evento y los pasa al objeto DataUtils llamado DataBaseTools.

Ilustración 7 El agente en segundo plano guarda los artículos en la base de datos (sin comentarios de depuración)

private void SendToDatabase(object sender, 
  SingleDownloadFinishedEventArgs e)
{
  // Ensure download is not null!
  if (e.DownloadedArticles != null)
  {
    DataBaseTools.AddArticles(e.DownloadedArticles, e.ParentFeed);
    _remainingDownloads--;
  }
  // If no remaining downloads, tell scheduler the background agent is done.
  if (_remainingDownloads <= 0)
  {
    NotifyComplete();
  }
}

Si el agente finaliza todas las descargas en el tiempo asignado, llama el método NotifyComplete para notificarle al sistema operativo que terminó. Esto le permite al sistema operativo asignar los recursos en desuso a otros agentes en segundo plano.

Si seguimos el código un paso más allá, vemos que el método AddArticles de la clase DataUtils se asegura de que el artículo sea nuevo antes de agregarlo a la base de datos. Observe en la Ilustración 8 cómo se usa nuevamente una exclusión mutua para evitar la contención en el contexto de datos. Finalmente, cuando se determina que el artículo es nuevo, este se guarda en la base de datos con el método SaveChangesToDB.

Ilustración 8 Agregar artículos a la base de datos en el archivo DataUtils.cs

public void AddArticles(ICollection<Article> newArticles, Feed feed)
{
  dbMutex.WaitOne();
  // DateTime date = SynFeedParser.latestDate;
  int downloadedArticleCount = newArticles.Count;
  int numOfNew = 0;
  // Query local database for existing articles.
  for (int i = 0; i < downloadedArticleCount; i++)
  {
    Article newArticle = newArticles.ElementAt(i);
    var d = from q in db.Article
            where q.ArticleBaseURI == newArticle.ArticleBaseURI
            select q;
    List<Article> a = d.ToList();
    // Determine if any articles are already in the database.
    bool alreadyInDB = (d.ToList().Count == 0);
    if (alreadyInDB)
    {
      newArticle.Read = false;
      newArticle.Favorite = false;
      numOfNew++;
    }
    else
    {
      // If so, remove them from the list.
      newArticles.Remove(newArticle);
      downloadedArticleCount--;
      i--;
    }               
  }
  // Try to submit and update counts.
  try
  {
    db.Article.InsertAllOnSubmit(newArticles);
    Deployment.Current.Dispatcher.BeginInvoke(() =>
      {
        feed.UnreadCount += numOfNew;
        SaveChangesToDB();
      });
    SaveChangesToDB();
  }
  // TODO handle errors.
  catch { }
  finally { dbMutex.ReleaseMutex(); }
}

La aplicación en primer plano usa una técnica semejante a la que se encuentra en el agente en segundo plano para consumir los datos con el método Download. Consulte el archivo ContentLoader.cs en la descarga de código adjunta para ver el código comparable.

Programación del agente en segundo plano

El agente en segundo plano no es más que eso, un agente que trabaja en segundo plano para la aplicación en primer plano. Tal como vimos anteriormente en la Ilustración 6 y en la Ilustración 7, el código que define dicha labor es una clase llamada Scheduled­Agent. Se deriva de Microsoft.Phone.Scheduler.ScheduledTaskAgent (que a su vez se deriva de Microsoft.Phone.BackgroundAgent). Aunque el agente recibe bastante atención porque realiza el trabajo pesado, no podría se ejecutar si no fuera por la tarea programada.

La tarea programada es el objeto que se usa para especificar cuándo y con qué frecuencia se ejecuta el agente en segundo plano. En esta aplicación, la tarea programada es una tarea periódica (Microsoft.Phone.Scheduler.PeriodicTask). Las tareas periódicas se ejecutan periódicamente durante un tiempo corto. Para programar la tarea, solicitarla y realizar otras acciones, usamos el servicio de acción programada (ScheduledActionService). Para obtener más información sobre los agentes en segundo plano, consulte wpdev.ms/bgagent.

El código de la tarea programada para esta aplicación se encuentra en el archivo BackgroundAgentTools.cs, en el proyecto de la aplicación en primer plano. Dicho código define el método StartPeriodicAgent, que App.xaml.cs llama en el constructor de la aplicación (consulte la Ilustración 9).

Ilustración 9 Programación de la tarea periódica en BackgroundAgentTools.cs (sin los comentarios)

public bool StartPeriodicAgent()
{
  periodicDownload = ScheduledActionService.Find(periodicTaskName) as PeriodicTask;
  bool wasAdded = true;
  // Agents have been disabled by the user.
  if (periodicDownload != null && !periodicDownload.IsEnabled)
  {
    // Can't add the agent. Return false!
    wasAdded = false;
  }
  // If the task already exists and background agents are enabled for the
  // application, then remove the agent and add again to update the scheduler.
  if (periodicDownload != null && periodicDownload.IsEnabled)
  {
    ScheduledActionService.Remove(periodicTaskName);
  }
  periodicDownload = new PeriodicTask(periodicTaskName);
  periodicDownload.Description =
    "Allows FeedCast to download new articles on a regular schedule.";
  // Scheduling the agent may not be allowed because maximum number
  // of agents has been reached or the phone is a 256MB device.
  try
  {
    ScheduledActionService.Add(periodicDownload);
  }
  catch (SchedulerServiceException) { }
  return wasAdded;
}

Antes de programar la tarea periódica, StartPeriodicAgent realiza algunos controles, ya que siempre existe la posibilidad de que no podamos programar la tarea programada. En primer lugar, los usuarios pueden deshabilitar las tareas programadas en la lista de tareas en segundo plano en el panel de Applications de Settings. Los dispositivos también tienen una cantidad limitada de tareas que pueden habilitar al mismo tiempo. Depende de la configuración del dispositivo, pero puede llegar a ser tan baja como seis. Si intentamos programar una tarea programada después de excedido el límite, o si la aplicación se ejecuta en un dispositivo de 256 MB, o si ya programamos la misma tarea, el método Add generará una excepción.

Esta aplicación llama el método StartPeriodicTask cada vez que se inicia, ya que los agentes en segundo plano vencen después de 14 días. Al actualizar el agente en cada inicio se garantiza que se pueda seguir ejecutando el agente, incluso si la aplicación no se vuelve a iniciar en varios días.

La variable periodicTaskName en la Ilustración 9, que se usa para buscar una tarea existente, es equivalente a “FeedCastAgent”. Observe que este nombre no identifica el código del agente en segundo plano correspondiente. Simplemente es un nombre amigable que podemos usar para trabajar con ScheduledActionService. La aplicación en primer plano ya conoce el código del agente en segundo plano, porque se agregó como referencia al proyecto de la aplicación en primer plano. Como el código del agente en segundo plano se creó como un proyecto del tipo agente de tarea programada de Windows Phone, las herramientas fueron capaces de conectar todo correctamente cuando se agregó la referencia. Puede ver la especificación de la relación entre la aplicación en primer plano y el agente en segundo plano en el manifiesto de la aplicación en primer plano (WMAppManifest.xml en el código de muestra), tal como apreciamos aquí:

<Tasks>
  <DefaultTask Name="_default" 
    NavigationPage="Views/MainPage.xaml" />
  <ExtendedTask Name="BackgroundTask">
    <BackgroundServiceAgent Specifier="ScheduledTaskAgent" 
      Name="FeedCastAgent"
      Source="FeedCastAgent" Type="FeedCastAgent.ScheduledAgent"/>
  </ExtendedTask>
</Tasks>

Mosaicos

Aguilera trabajó en la interfaz de usuario, Visualizaciones y ViewModels. También trabajó en la localización y la característica de mosaicos. Los mosaicos, conocidos también como Live Tiles, muestran el contenido y vínculo dinámico de la aplicación en Inicio. El mosaico de aplicación de cualquier aplicación se puede anclar en Inicio (sin que el desarrollador tenga que hacer ningún trabajo). Sin embargo, si queremos crear un vínculo a un lugar diferente de la página principal de la aplicación, debemos implementar los mosaicos secundarios. Esto permite que el usuario navegue con mayor profundidad en la aplicación (más allá de la página principal), hasta una página que podemos personalizar a lo que sea que el mosaico secundario representa.

En FeedCast, los usuarios pueden anclar una fuente o categoría (mosaico secundario) al Inicio. Con un simple toque, pueden leer instantáneamente los últimos artículos relacionados con dicha fuente o categoría. Para habilitar esta experiencia, primero deben poder anclar la fuente o categoría al Inicio. Aguilera usó el kit de herramientas de Silverlight para Windows Phone ContextMenu para facilitar esta labor. Al tocar mantener presionada una fuente o categoría en el panel “all” de la página principal, aparece el menú contextual. Desde ahí, los usuarios pueden optar por eliminar o anclar la fuente o categoría al Inicio. En la Ilustración 10 se ilustra el proceso de extremo a extremo desde la perspectiva del usuario.


Ilustración 10 Anclar la categoría Windows Phone News al Inicio e iniciar la página Category

En la Ilustración 11 vemos el XAML que hace posible el menú de contexto. El segundo MenuItem muestra “anclar al inicio” (“pin to start”, cuando el idioma de visualización es el inglés). Cuando se toca ese elemento, el evento click llama el método OnCategoryPinned para iniciar el anclado. Como esta aplicación está localizada, el texto del menú contextual proviene de un archivo de recurso. Por esta razón el valor del encabezado está enlazado con LocalizedResources.ContextMenuPinToStartText.

Ilustración 11 El menú de contexto para eliminar o anclar una categoría al Inicio

<toolkit:ContextMenuService.ContextMenu>
  <toolkit:ContextMenu>
    <toolkit:MenuItem Tag="{Binding}"
      Header="{Binding LocalizedResources.ContextMenuRemoveText,
               Source={StaticResource LocalizedStrings}}"
      IsEnabled="{Binding IsRemovable}"
      Click="OnCategoryRemoved"/>
    <toolkit:MenuItem Tag="{Binding}"
      Header="{Binding LocalizedResources.ContextMenuPinToStartText,
               Source={StaticResource LocalizedStrings}}"
      IsEnabled="{Binding IsPinned, 
      Converter={StaticResource IsPinnable}}"
      Click="OnCategoryPinned"/>
  </toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>

Esta aplicación solo posee dos archivos de recursos, uno para español y otro para inglés (predeterminado). Sin embargo, como la localización ya está implementada, resultaría bastante fácil agregar más idiomas. En la Ilustración 12 se muestra el archivo de recursos predeterminado, AppResources.resx. Para obtener más información, consulte wpdev.ms/globalized.

The Default Resource File, AppResources.resx, Supplies the UI Text for All Languages Except SpanishIlustración 12 El archivo de recursos predeterminado, AppResources.resx, suministra el texto de la interfaz de usuario para todos los idiomas, excepto para español

En un principio, el equipo no tenía claro cómo iba a determinar exactamente qué categoría o fuente se debía anclar. Entonces Aguilera descubrió el atributo Tag de XAML (consulte la Ilustración 11). Los miembros del equipo descubrieron que podían enlazarlo a los objetos de categoría o fuente en ViewModel y luego recuperar los objetos individuales posteriormente, de manera programada. En la página principal, la lista de categorías está enlazada con el objeto MainPageAllCategoriesViewModel. Cuando se llama el método OnCategoryPinned, este usa el método GetTagAs para obtener el objeto Category (enlazado con Tag) que corresponde al elemento puntual en la lista, del siguiente modo:

private void OnCategoryPinned(object sender, EventArgs e)
{
  Category tappedCategory = GetTagAs<Category>(sender);
  if (null != tappedCategory)
  {
    AddTile.AddLiveTile(tappedCategory);
  }
}

El método GetTagAs es un método genérico para obtener cualquier objeto que se enlazó con el atributo Tag de un contenedor. Aunque esto resulta eficaz, no es realmente necesario para la mayoría de los casos en MainPage.xaml.cs. Los elementos en la lista ya están enlazado al objeto, por lo que vincularlos a Tag resulta redundante. En vez de usar Tag, podemos usar DataContext del objeto Sender. Por ejemplo, en la Ilustración 13 vemos muestra cómo se vería OnCategoryPinned con el método recomendado con DataContext.

Ilustración 13 Un ejemplo del uso de DataContext en vez de GetTagAs

private void OnCategoryPinned(object sender, EventArgs e)
{
  Category tappedCategory = null;
  if (null != sender)
  {
    FrameworkElement element = sender as FrameworkElement;
    if (null != element)
    {
      tappedCategory = element.DataContext as Category;
      if (null != tappedCategory)
      {
        AddTile.AddLiveTile(tappedCategory);
      }
    }
  }
}

Este método con DataContext funciona bien para todos los casos de MainPage.xaml.cs, excepto uno, el método OnHubTileTapped. Este se desencadena cuando tocamos un artículo destacado en el panel “featured” de la página principal. La dificultad se debe a que el remitente no está enlazado con una clase Article: está enlazado con MainPageFeaturedViewModel. Este ViewModel contiene seis artículos, así que no se sabe a partir de DataContext, cuál se tocó. Al usar la propiedad Tag, en este caso, resulta muy fácil enlazar con el artículo apropiado.

Como podemos anclar las fuentes y las categorías al Inicio, el método AddLiveTile posee dos sobrecargas. Los objetos y los mosaicos secundarios son tan diferentes que el equipo decidió no fusionar la funcionalidad en un solo método genérico. En la Ilustración 14 vemos la versión de Category del método AddLiveTile.

Ilustración 14 Anclar un objeto Category al Inicio

public static void AddLiveTile(Category cat)
{
  // Does Tile already exist? If so, don't try to create it again.
  ShellTile tileToFind = ShellTile.ActiveTiles.FirstOrDefault(x => 
    x.NavigationUri.ToString().Contains("/Category/" + 
    cat.CategoryID.ToString()));
  // Create the Tile if doesn't already exist.
  if (tileToFind == null)
  {
    // Create an image for the category if there isn't one.
    if (cat.ImageURL == null || cat.ImageURL == String.Empty)
    {
      cat.ImageURL = ImageGrabber.GetDefaultImage();
    }
    // Create the Tile object and set some initial properties for the Tile.
    StandardTileData newTileData = new StandardTileData
    {
      BackgroundImage = new Uri(cat.ImageURL, 
      UriKind.RelativeOrAbsolute),
      Title = cat.CategoryTitle,
      Count = 0,
      BackTitle = cat.CategoryTitle,
      BackContent = "Read the latest in " + cat.CategoryTitle + "!",
    };
    // Create the Tile and pin it to Start.
    // This will cause a navigation to Start and a deactivation of the application.
    ShellTile.Create(
      new Uri("/Category/" + cat.CategoryID, UriKind.Relative), 
      newTileData);
    cat.IsPinned = true;
    App.DataBaseUtility.SaveChangesToDB();
  }
}

Antes de agregar un mosaico de Category, el método AddLiveTile usa la clase ShellTile para ver las URI de navegación de todos los mosaicos activos para determinar si la categoría ya se agregó. De no ser así, continúa y obtiene la URL de una imagen para asociarla con el mosaico nuevo. Siempre que se crea un mosaico nuevo, la imagen de fondo debe provenir de un recurso local. En este caso, usa la clase ImageGrabber para obtener el archivo de una imagen local asignada en forma aleatoria. Sin embargo, después de crear el mosaico, podemos actualizar la imagen de fondo con una URL remota. Pero esta aplicación específica no hace eso.

Toda la información que debemos especificar para crear un mosaico nuevo está contenida en la clase StandardTileData. Esta clase se usa para colocar texto, números e imágenes de fondo en el mosaico. Cuando creamos el mosaico con el método Create, se pasa StandardTileData como un parámetro. El otro parámetro importante que se pasa es la URI de navegación del mosaico. Corresponde a la URI que se usa para llevar a los usuarios a un lugar importante en la aplicación.

En esta aplicación, la URI de navegación del mosaico solo lleva al usuario hasta la aplicación. Para ir más allá de eso, se usa una clase UriMapper básica para dirigir a los usuarios a la página correcta. El elemento de navegación App.xaml especifica toda la asignación de URI para la aplicación. En cada elemento UriMapping, el valor especificado por el atributo Uri corresponde a la URI entrante. El valor especificado por el atributo MappedUri corresponde al lugar donde se llevará al usuario. Para conservar el contexto de la categoría, fuente o artículo específica, se envía el valor id entre los corchetes {id} desde la URI entrante hacia la URI asignada, del siguiente modo:

<navigation:UriMapping Uri="/Category/{id}" MappedUri=
  "/Views/CategoryPage.xaml?id={id}"/>

Es posible que existan otras razones para usar un asignador URI (como la extensibilidad de búsqueda, por ejemplo), pero no es necesario usar un mosaico secundario. En esta aplicación, el uso del asignador URI fue una decisión de estilo. El equipo pensó que las URI más cortas eran más elegantes y fáciles de usar. De manera alternativa, los mosaicos secundarios podrían especificar una URI específica para una página (como los valores MappedUri) para conseguir el mismo resultado.

Independientemente del medio, una vez que la URI del mosaico secundario se asigna a la página apropiada, el usuario llega a la página Category que contiene una lista de sus artículos. Misión cumplida. Para obtener más información sobre los mosaicos, consulte wpdev.ms/secondarytiles.

Pero espere, ¡que hay más!

Esta aplicación tiene mucho más de lo que analicé aquí. Eche una mirada al código para aprender más sobre cómo el equipo abordó estos problemas y otros. Por ejemplo, SynFeedParser.cs contiene una forma buena de limpiar los datos de las fuentes que algunas veces vienen con etiquetas HTML.

Solo tenga presente que esta es una instantánea del trabajo de los practicantes luego de 12 semanas, menos un poco de limpieza. Es posible que algunos desarrolladores profesionales prefieran codificar algunas partes de manera diferente. No obstante, creo que la aplicación hizo un muy buen trabajo al integrar una base de datos local, agentes de segundo plano y los mosaicos. Espero que haya disfrutado esta mirada “entre bastidores”. ¡Que disfrute la codificación!

Matt Stroshane  escribe documentación para los desarrolladores del equipo de Windows Phone. Sus otras contribuciones a la biblioteca de MSDN tratan acerca de productos tales como SQL Server, SQL Azure y Visual Studio. Cuando no está escribiendo, puede toparse con él en las calles de Seattle, donde entrena para la siguiente maratón. Puede seguirlo por Twitter en twitter.com/mattstroshane.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Francisco Aguilera, Thomas Fennel, John Gallardo, Sean McKenna, Suman Malani, Ayomikun (George) Okeowo y Himadri Sarkar