Windows Phone

Nos bastidores: Um aplicativo leitor de feeds para o Windows Phone

Matt Stroshane

Baixar o código de exemplo

Sou viciado em feeds. Adoro a magia dos feeds Atom e RSS, agora as notícias chegam até mim, e não o contrário. Mas com o acesso conveniente a tantas informações, consumi-las de maneira significativa tornou-se um desafio. Assim, quando soube que alguns estagiários da Microsoft estavam desenvolvendo um aplicativo leitor de feeds para o Windows Phone, fiquei entusiasmado ao descobrir como eles abordaram o problema.

Como parte do seu estágio, Francisco Aguilera, Suman Malani e Ayomikun (George) Okeowo tiveram 12 semanas para desenvolver um aplicativo para o Windows Phone que incluía alguns novos recursos do Windows Phone SDK 7.1. Sendo novos no desenvolvimento do Windows Phone, eles foram boas cobaias para nossas plataforma, ferramentas e documentação.

Depois de considerar suas opções, decidiram por um aplicativo leitor de feeds que exibiria um banco de dados local, Live Tiles, e um agente de segundo plano. Eles exibiram muito mais do que isso. Neste artigo, vou conduzi-los por como eles usaram esses recursos. Então, instale o Windows Phone SDK 7.1, baixe o código e coloque-o em sua tela. Vamos começar.

Usando o aplicativo

O hub central do aplicativo é a página principal, MainPage.xaml (Figura 1). Consiste em quatro painéis panorâmicos: “what’s new,” “featured,” “all” e “settings.” O painel “what’s new” mostras as mais recentes atualizações dos feeds. “Featured” exibe seis artigos do quais ele imagina que você irá gostar com base em seu histórico de leitura. O painel “all” lista todas as suas categorias e feeds. Para baixar artigos somente via Wi-Fi, use a configuração no painel “settings”.

The Main Page of the App After Creating a Windows Phone News CategoryFigura 1 A página principal do aplicativo depois de criar uma categoria de notícias no Windows Phone

Os painéis “what’s new” e “featured” oferecem uma maneira de navegar diretamente para um artigo. O painel “all” oferece uma lista de categorias e feeds. Do painel “all” é possível navegar para uma coleção de artigos que são agrupados por feed ou categoria. Também é possível usar a barra do aplicativo no painel “all” para adicionar um novo feed ou nova categoria. A Figura 2 mostra como a página principal se relaciona com as outras oito páginas do aplicativo.

The Page Navigation Map, with Auxiliary Pages in GrayFigura 2 O mapa de navegação de páginas, com páginas auxiliares em cinza

Semelhante à dinamização, você pode navegar horizontalmente nas páginas de categorias, feeds e artigos. Quando você está em uma dessa páginas, setas aparecem na barra do aplicativo (consulte a Figura 3). As setas permitem exibir os dados da categoria, feed ou artigo anterior ou próximo no banco de dados. Por exemplo, se estiver visualizando a categoria de negócios na página de categorias, tocar na seta “next” exibe a categoria de entretenimento na página de categorias.

The Category, Feed and Article Pages with Their Application Bars ExpandedFigura 3 As páginas de categorias, feeds e artigos com suas barras do aplicativo ampliadas

No entanto, as teclas de seta não navegam realmente para outra página de categorias. Em vez disso, a mesma página é associada a uma fonte de dados diferente. Tocar no botão Voltar do telefone faz você retornar ao painel “all” sem a necessidade de qualquer código de navegação especial.

Da página de artigos você pode navegar para a página Share e enviar um link por mensagem, email ou rede social. A barra do aplicativo também fornece a capacidade de exibir o artigo no Internet Explorer, adicionar aos seus favoritos ou removê-lo do banco de dados.

Nos bastidores

Ao abrir a solução no Visual Studio, você verá que é um aplicativo em C# dividido em três projetos:

  1. FeedCast: A parte que o usuário vê—o aplicativo em primeiro plano (códigos View e ViewModel).
  2. FeedCastAgent: O código do agente de segundo plano (tarefa agendada periódica)
  3. FeedCastLibrary: A rede compartilhada e o código de dados.

A equipe usou o Kit de Ferramentas do Silverlight para Windows Phone (novembro de 2011) e o Microsoft Silverlight 4 SDK. Controles do kit de ferramentas—Microsoft.Phone.Controls.Toolkit.dll—são usados na maiorias das páginas do aplicativo. Por exemplo, os controles HubTile são usados para exibir artigos no painel “featured” da página principal. Para ajudar na rede, a equipe usou System.ServiceModel.Syndication.dll do Silverlight 4 SDK. Esse assembly não está incluído no Windows Phone SDK e não está otimizado especificamente para aplicativos de telefone, mas os membros da equipe descobriram que ele funcionava bem para suas necessidades.

O projeto do aplicativo em primeiro plano, FeedCast, é o maior dos três na solução. Novamente, essa é a parte do aplicativo que o usuário vê. Está organizado em nove pastas.

  1. Converters: conversores de valores criam uma ponte entre os dados e a interface do usuário.
  2. Icons: ícones usados pelas barras do aplicativo.
  3. Images: imagens usadas por HubTiles quando os artigos não têm imagens.
  4. Libraries: os assemblies Toolkit e Syndication.
  5. Models: código relacionado a dados não usado pelo agente de segundo plano.
  6. Resources: arquivos de recursos de localização em inglês e espanhol.
  7. Themes: personalizações para o controle HeaderedListBox.
  8. ViewModels: ViewModels e outras classes auxiliares.
  9. Views: código para cada página do aplicativo em primeiro plano.

Esse aplicativo segue o padrão Model-View-ViewModel (MVVM). O código na pasta Views se concentra principalmente na interface do usuário. A lógica e os dados associados a páginas individuais são definidos pelo código na pasta ViewModels. Embora a pasta Models contenha algum código relacionado a dados, os objetos de dados são definidos no projeto FeedCastLibrary. O código “Model” aí é reutilizado pelo aplicativo em primeiro plano e pelo agente de segundo plano. Para saber mais sobre MVVM, consulte wpdev.ms/mvvmpnp.

O projeto FeedCastLibrary contém os dados e o código de rede usados pelo aplicativo em primeiro plano e pelo agente de segundo plano. Esse projeto contém duas pastas: Data e Networking. Na pasta Data, o “Model” FeedCast é descrito por classes parciais em quatro arquivos: LocalDatabaseDataContext.cs, Article.cs, Category.cs e Feed.cs. O arquivo DataUtils.cs contém o código que executa operações do banco de dados comuns. Uma classe auxiliar para usar configurações de armazenamento isolado está no arquivo Settings.cs. A pasta Networking do projeto FeedCastLibrary contém o código usado para baixar e analisar conteúdo da Web, os mais significativos dos quais são os métodos Download no arquivo WebTools.cs.

Só há uma classe no projeto FeedCastAgent, Scheduled­Agent.cs, que é o código do agente de segundo plano. O método OnInvoke é chamado quando ele é executado e o método SendToDatabase é chamado quando os downloads são concluídos. Falarei mais sobre download posteriormente.

Banco de dados local

Para produtividade máxima, cada um dos membros da equipe se concentrou em uma área diferente do aplicativo. Aguilera se concentrou na interface do usuário, Views e ViewModels do aplicativo em primeiro plano. Okeowo trabalhou na rede e na obtenção de dados dos feeds. Malani trabalhou na arquitetura e operações do banco de dados.

No Windows Phone você pode armazenar seus dados em um banco de dados local. A parte local é devido a ser um arquivo de banco de dados residindo em armazenamento isolado (o bucket de armazenamento no dispositivo do seu aplicativo, isolado de outros aplicativos). Essencialmente, você descreve suas tabelas do banco de dados como Plain Old CLR Objects, com as propriedades desses objetos representando as colunas do banco de dados. Isso permite que cada objeto dessa classe seja armazenado como uma linha na tabela correspondente. Para representar o banco de dados, crie um objeto especial chamado de contexto de dados que é herdeiro de System.Data.Linq.DataContext.

O ingrediente mágico do banco de dados local é o tempo de execução do LINQ to SQL, seu repositório de dados. Você chama o método CreateDatabase do contexto de dados e o LINQ to SQL cria o arquivo .sdf em armazenamento isolado. Crie consultas do LINQ para especificar os dados que você quer, e o LINQ to SQL retorna objetos fortemente tipados que podem ser associados à sua interface do usuário. O LINQ to SQL permite que você se concentre em seu código enquanto ele manipula todas as operações de banco de dados de baixo nível. Para saber mais sobre o uso de um banco de dados local, consulte wpdev.ms/localdb.

Em vez de tipificar todas as classes, Malani usou o Visual Studio 2010 Ultimate para seguir uma abordagem diferente. Ela criou as tabelas do banco de dados visualmente, usando a caixa de diálogo Server Explorer Add Connection para criar um banco de dados SQL Server CE e, em seguida, a caixa de diálogo New Table para criar as tabelas.

Tendo projetado seu esquema, Malani usou SqlMetal.exe para gerar um contexto de dados. SqlMetal.exe é um utilitário de linha de comando do LINQ to SQL de área de trabalho. Seu objetivo é criar uma classe de contexto de dados com base em um banco de dados do SQL Server. O código gerado é bastante similar a um contexto de dados do Windows Phone. Usando essa técnica, ela pôde criar as tabelas visualmente e gerar o contexto de dados rapidamente. Para saber mais sobre SqlMetal.exe, consulte wpdev.ms/sqlmetal.

O banco de dados que Malani construiu é mostrado na Figura 4. As três principais tabelas são Category, Feed e Article. Ainda, uma tabela de vinculação, Category_Feed, é usada para permitir uma relação muitos-para-muitos entre categorias e feeds. Cada categoria pode ser associada a vários feeds e cada feed pode ser associado a várias categorias. Observe que o recurso “favorite” do aplicativo é apenas uma categoria especial que não pode ser excluída.

The Database SchemaFigura 4 O esquema do banco de dados

No entanto, o contexto de dados gerado por SqlMetal.exe ainda continha algum código que não é suportado no Windows Phone. Depois que Milani adicionou o arquivo de código do contexto de dados ao projeto do Windows Phone, ela compilou o projeto para localizar qual código não era válido. Ela se lembra de ter de remover um construtor, mas o resto compilou bem.

Ao inspecionar o arquivo do contexto de dados, LocalDatabase­DataContext.cs, você deve notar que todas as tabelas são classes parciais. O resto do código associado a essas tabelas (que não foi gerado automaticamente por SqlMetal.exe) é armazenado nos arquivos de código Article.cs, Category.cs e Feed.cs. Separando o código dessa maneira, Milani pôde fazer alterações no esquema do banco de dados sem afetar as definições do método de extensibilidade que ela escreveu manualmente. Tivesse ela não feito isso, ela iria ter de adicionar novamente os métodos toda vez que ela gerasse automaticamente LocalDatabaseDataContext.cs (porque SqlMetal.exe substitui todo o código no arquivo).

Mantendo a simultaneidade

Como com a maioria dos aplicativos para o Windows Phone que objetivam uma experiência responsiva e fluida, esse usa vários threads simultâneos para realizar seu trabalho. Além do thread da interface do usuário, que aceita entrada do usuário, vários threads de segundo plano podem estar lidando com o download e análise dos RSS feeds. Cada um desses threads precisará, por sua vez, fazer alterações no banco de dados.

Enquanto o próprio banco de dados oferece um acesso simultâneo robusto, a classe DataContext não é thread-safe. Em outras palavras, o objeto DataContext global único usado nesse aplicativo não pode ser compartilhado entre vários threads sem adicionar alguma forma de modelo de simultaneidade. Para abordar esse problema, Malani usou as APIs de simultaneidade do LINQ to SQL e um objeto mutex do namespace System.Threading.

No arquivo DataUtils.cs, os métodos WaitOne e ReleaseMutex de mutex são usados para sincronizar o acesso a dados em casos em que poderia haver contenção entre as classes DataContext. Por exemplo, se vários threads simultâneos (do aplicativo em primeiro plano ou do agente de segundo plano) tivessem de chamar o método SaveChangesToDB aproximadamente ao mesmo tempo, o código que executar WaitOne primeiro continuará. A chamada WaitOne do outro não será concluída até que o primeiro código chame ReleaseMutex. Por essa razão, é importante colocar sua chamada ReleaseMutex na instrução finally ao usar try/catch/finally para operações do banco de dados. Sem uma chamada para ReleaseMutex, o outro código esperará na chamada WaitOne até que o thread proprietário saia. Da perspectiva do usuário, isso poderia demorar uma eternidade.

Em vez de um objeto DataContext global único, também é possível projetar seu aplicativo para criar e destruir objetos DataContext menores por thread. No entanto, os membros da equipe descobriram que a abordagem do DataContext global simplificava o desenvolvimento. Eu deveria ainda observar que porque o aplicativo apenas precisava proteger contra o acesso entre threads, não acesso entre processos, eles poderiam também ter usado um bloqueio em vez do mutex. O bloqueio talvez tivesse oferecido um melhor desempenho.

Consumindo dados

Okeowo concentrou seus esforços em trazer dados para o aplicativo. O arquivo WebTools.cs contém o código em que a maior parte da ação acontece. Mas a classe WebTools não é usada apenas para baixar feeds, é também usada na página de feed novo para pesquisar novos feeds no Bing. Ele realizou isso criando uma interface comum, IXmlFeedParser, e abstraindo o código de análise em classes diferentes. A classe SynFeedParser analisa os feeds e a classe SearchResultParser analisa os resultados da pesquisa do Bing.

No entanto, a consulta do Bing na verdade não retorna artigos (apesar da coleção de objetos Article retornada pela interface IXmlFeedParser). Em vez disso, ela retorna uma lista de nomes de feed e URIs. O que acontece? Bem, Okeowo percebeu que a classe Article já tinha as propriedades de que precisava para descrever um feed; ele não precisava criar um outra classe. Ao analisar os resultados da pesquisa, ele usou ArticleTitle para o nome do feed e ArticleBaseURI para o URI do feed. Consulte SearchResultParser.cs no download de código que acompanha este artigo para saber mais.

O código na nova página ViewModel (NewFeedPageViewModel.cs no código de exemplo) mostra como os resultados da pesquisa do Bing são consumidos. Primeiro, o método GetSearchString é usado para juntar o URI em cadeia de caracteres da pesquisa do Bing com base nos termos de pesquisa que o usuário insere em NewFeedPage, como mostrado no seguinte trecho de código:

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

O valor _numOfResults limita quantos resultados da pesquisa são retornados. Para saber mais sobre o acesso ao Bing através de RSS, consulte a página da Biblioteca MSDN, “Acessando o Bing através de RSS,” em bit.ly/kc5uYO.

O método GetSearchString é chamado no método GetResults, onde os dados são realmente recuperados do Bing (consulte a Figura 5). O método GetResults parece um pouco invertido, pois ele lista uma expressão lambda que manipula o evento AllDownloadsFinished “inline,” antes que o código para iniciar o download seja realmente iniciado. Quando o método Download é chamado, o objeto WebTools consulta o Bing pelo URI que foi construído com GetSearchString.

Figura 5 O método GetResults em NewFeedPageView­Model.cs consulta o Bing quanto a novos feeds

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

O método WebTools Download é também usado pelo agente de segundo plano (consulte a Figura 6), mas de uma maneira diferente. Em vez de download de um único feed, o agente passa para o método uma lista de vários feeds. Para recuperar resultados, o agente usa uma estratégia diferente. Em vez de esperar até que os artigos de todos os feeds estejam baixados (via evento AllDownloadsFinished), o agente salva os artigos assim que o download de cada feed estiver concluído (via evento SingleDownloadFinished).

Figura 6 O agente de segundo plano inicia um download (sem comentários de depuração)

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

O trabalho do agente de segundo plano é manter todos os seus feeds atualizados. Para isso, ele passa para o método Download uma lista de todos os feeds. O agente de segundo plano tem apenas um curto período de tempo para ser executado e quando o tempo acaba o processo é interrompido imediatamente. Assim, à medida que o agente baixa feeds, ele envia os artigos para o banco de dados, um feed de cada vez. Desse modo, o agente de segundo plano tem uma probabilidade muito maior de salvar novos artigos antes de ser parado.

Os métodos Download de único feed e de vários feeds são na realidade sobrecargas do mesmo código. O código de download inicia uma HttpWebRequest para cada feed (de forma assíncrona). Assim que a primeira solicitação é retornada, ele chama o manipulador de eventos SingleDownloadFinished. As informações do feed e os artigos são então empacotados em um evento usando o SingleDownloadFinishedEventArgs. Como mostrado na Figura 7, o método SendToDatabase está conectado ao método SingleDownloadFinshed. Ao retornar, SendToDatabase retira os artigos dos argumentos do evento e os passa para o objeto do DataUtils chamado DataBaseTools.

Figura 7 O agente de segundo plano salva artigos no banco de dados (sem comentários de depuração)

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

Se o agente terminar todos os downloads dentro do tempo alocado, ele chama o método NotifyComplete para notificar o sistema operacional de que ele terminou. Isso permite que o sistema operacional aloque os recursos não utilizados para outros agentes de segundo plano.

Aprofundando um pouco mais no código, o método AddArticles da classe DataUtils verifica se o artigo é novo antes de adicioná-lo ao banco de dados. Observe na Figura 8 como um mutex é usado novamente para impedir a contenção no contexto de dados. Finalmente, quando for constatado que o artigo é novo, ele será salvo no banco de dados com o método SaveChangesToDB.

Figura 8 Adicionando artigos ao banco de dados no arquivo 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(); }
}

O aplicativo em primeiro plano usa uma técnica similar àquela encontrada no agente de segundo plano para consumir dados com o método Download. Consulte o arquivo ContentLoader.cs no download de código que acompanha esse artigo para ver o código parecido.

Agendando o agente de segundo plano

O agente de segundo plano é apenas isso, um agente que executa trabalho em segundo plano para o aplicativo de segundo plano. Como visto anteriormente na Figura 6 e Figura 7, o código que define esse trabalho é uma classe chamada Scheduled­Agent. Ele é derivado de Microsoft.Phone.Scheduler.ScheduledTaskAgent (que é derivado de Microsoft.Phone.BackgroundAgent). Embora o agente receba muita atenção porque faz o trabalho pesado, ele nunca seria executado se não fosse a tarefa agendada.

A tarefa agendada é o objeto usado para especificar quando e com que frequência o agente de segundo plano será executado. A tarefa agendada usada nesse aplicativo é uma tarefa periódica (Microsoft.Phone.Scheduler.PeriodicTask). Uma tarefa periódica é executada regularmente por um curto período de tempo. Para realmente colocar a tarefa na agenda, consultá-la e assim por diante, use o serviço de ação agendada (ScheduledActionService). Para saber mais sobre agentes de segundo plano, consulte wpdev.ms/bgagent.

O código para tarefa agendada desse aplicativo está no arquivo BackgroundAgentTools.cs, no projeto do aplicativo em primeiro plano. Esse código define o método StartPeriodicAgent, que é chamado por App.xaml.cs no construtor do aplicativo (consulte a Figura 9).

Figura 9 Agendando a tarefa periódica em BackgroundAgentTools.cs (menos os comentários)

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 agendar a tarefa periódica, StartPeriodicAgent executa algumas verificações porque sempre há uma chance de que você não possa agendar a tarefa agendada. Primeiro de tudo, as tarefas agendadas podem ser exibidas pelos usuários na lista de tarefas em segundo plano no painel Applications de Settings. Também há um limite de quantas tarefas podem ser habilitadas em um dispositivo de cada vez. Varia por configuração de dispositivo, mas poderia ser tão baixo quanto seis. Se você tentar agendar uma tarefa agendada depois de ultrapassar o limite, ou se seu aplicativo estiver sendo executado em um dispositivo de 256 MB, ou se você já agendou a mesma tarefa, o método Add gerará uma exceção.

Esse aplicativo chama o método StartPeriodicTask toda vez que ele é iniciado, porque agentes de segundo plano expiram depois de 14 dias. Atualizar o agente a cada inicialização garante que o agente possa continuar a ser executado mesmo que o aplicativo não seja iniciado novamente por vários dias.

A variável periódica TaskName na Figure 9, usada para localizar uma tarefa existente, é igual a “FeedCastAgent.” Observe que esse nome não identifica o código do agente de segundo plano correspondente. Simplesmente é um nome amigável que pode ser usado para trabalhar com ScheduledActionService. O aplicativo em primeiro plano já sabe sobre o agente de segundo plano, porque ele foi adicionado como referência ao projeto do aplicativo em primeiro plano. Como o código do agente de segundo plano foi criado como projeto do tipo Windows Phone Scheduled Task Agent, as ferramentas foram capazes de conectar as coisas corretamente quando a referência foi adicionada. Você pode ver a relação aplicativo em primeiro plano-agente de segundo plano especificada no manifesto do aplicativo em primeiro plano (WMAppManifest.xml no código de exemplo), conforme mostrado aqui:

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

Blocos

Aguilera trabalhou na interface do usuário, Views e ViewModels. Também trabalhou na localização e no recurso Quadros. Quadros, algumas vezes conhecido como Quadros ao vivo, exibem conteúdo dinâmico e se vinculam ao aplicativo na tela inicial. O aplicativo Tile de qualquer aplicativo pode ser fixado na tela inicial (sem qualquer trabalho por parte do desenvolvedor). No entanto, se desejar vincular a qualquer outro lugar além da página principal de seu aplicativo, você precisa implementar Quadros secundários. Eles permitem que você navegue o usuário mais profundamente em seu aplicativo, além da página principal, para uma página que você pode personalizar para o que quer que o Quadro secundário represente.

Em FeedCast, os usuários podem fixar um feed ou uma categoria (Quadro secundário) na tela inicial. Com um simples toque, eles podem instantaneamente ler os mais recentes artigos relacionados ao feed ou categoria. Para permitir essa experiência, primeiro eles precisam ser capazes de fixar o feed ou categoria na tela inicial. Aguilera usou ContextMenu do Kit de Ferramentas do Silverlight para Windows Phone para facilitar isso. Tocar e segurar um feed ou categoria no painel “all” da página principal faz com que o menu de contexto apareça. A partir daí, os usuários podem escolher remover ou fixar o feed ou categoria na tela inicial. A Figura 10demonstra o processo ponta a ponta da perspectiva do usuário.


Figura 10 Fixando a categoria News do Windows Phone na tela inicial e iniciando a página Category

A Figura 11 mostra o XAML que torna o menu de contexto possível. O segundo MenuItem exibe “pin to start” (quando inglês é o idioma de exibição). Quando esse item é tocado, o evento de clique chama o método OnCategoryPinned para iniciar a fixação. Como esse aplicativo é localizado, o texto do menu de contexto realmente vem de um arquivo de recurso. Essa é a razão por que o valor Header é associado a LocalizedResources.ContextMenuPinToStartText.

Figura 11 O menu de contexto para remover ou fixar uma categoria na tela inicial

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

Esse aplicativo tem apenas dois arquivos de recursos, um para espanhol e outro para inglês (o padrão). No entanto, como a localização está funcionando, seria relativamente fácil adicionar mais idiomas. A Figura 12 mostra o arquivo de recurso padrão, AppResources.resx. Para obter mais informações, consulte wpdev.ms/globalized.

The Default Resource File, AppResources.resx, Supplies the UI Text for All Languages Except SpanishFigura 12 O arquivo de recurso padrão, AppResources.resx, fornece o texto da interface do usuário para todos os idiomas exceto espanhol

Inicialmente, a equipe não estava bem certa de como iria determinar exatamente qual categoria ou feed precisava ser fixada. Então Aguilera descobriu o atributo Tag do XAML (consulte a Figura 11). Os membros da equipe perceberam que eles poderiam associá-lo aos objetos de categoria ou feed no ViewModel e então recuperar os objetos individuais posteriormente por meio de programação. Na página principal, a lista de categorias está associada a um objeto MainPageAllCategoriesViewModel. Quando o método OnCategoryPinned é chamado, ele usa o método GetTagAs para obter o objeto Category (associado ao Tag) que corresponde ao item específico da lista, da seguinte forma:

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

O método GetTagAs é um método genérico para obtenção de qualquer objeto que tenha sido associado ao atributo Tag de um contêiner. Embora seja efetivo, não é realmente necessário para a maioria de seus usos em MainPage.xaml.cs. Os itens da lista já estão associados ao objeto, portanto, associá-los ao Tag é algo redundante. Em vez de usar Tag, é possível usar o DataContext do objeto Sender. Por exemplo, a Figura 13 mostra como OnCategoryPinned pareceria usando a abordagem do DataContext recomendada.

Figura 13 Um exemplo de uso de DataContext em 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);
      }
    }
  }
}

A abordagem do DataContext funciona bem para todos os casos em MainPage.xaml.cs exceto um, o método OnHubTileTapped. Ele é disparado quando você toca em um artigo em destaque no painel “featured” da página principal. O desafio é devido ao fato do remetente não estar associado a uma classe Article, ele está associado a MainPageFeaturedViewModel. Esse ViewModel contém seis artigos, portanto não é completamente sabido do DataContext qual foi tocado. Nesse caso, usar a propriedade Tag realmente facilita a associação à Article apropriada.

Como você pode fixar feeds e categorias na página inicial, o método AddLiveTile tem duas sobrecargas. Os objetos e os Quadros secundários têm tantas diferenças que a equipe decidiu não mesclar a funcionalidade em um único método genérico. A Figura 14 mostra a versão Category do método AddLiveTile.

Figura 14 Fixando um objeto Category na página inicial

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 adicionar um quadro Category, o método AddLiveTile usa a classe ShellTile para examinar os URIs de navegação de todos os quadros ativos para determinar se a categoria já foi adicionada. Caso negativo, ele continua e obtém uma URL de imagem para associar ao novo quadro. Sempre que um novo quadro é criado, a imagem do plano de fundo precisa vir de um recurso local. Nesse caso, ele usa a classe ImageGrabber para obter um arquivo de imagem local atribuído aleatoriamente. No entanto, depois de criar um quadro, é possível atualizar a imagem do plano de fundo com uma URL remota. Mas esse aplicativo específico não faz isso.

Todas as informações que você precisa especificar para criar um novo quadro estão contidas na classe StandardTileData. Essa classe é usada para colocar texto, números e imagens do plano de fundo nos quadros. Ao criar o quadro com o método Create, o StandardTileData é passado como um parâmetro. O outro parâmetro importante que é passado é o URI de navegação do quadro. Ele é o URI que é usado para trazer os usuários a um lugar significativo em seu aplicativo.

Nesse aplicativo, o URI de navegação do quadro apenas leva o usuário tão longe quanto o aplicativo. Para ir além disso, uma classe UriMapper básica é usada para rotear os usuários à página correta. O elemento de navegação App.xaml especifica todo o mapeamento de URIs do aplicativo. Em cada elemento UriMapping, o valor especificado pelo atributo Uri é o URI de entrada. O valor especificado pelo atributo MappedUri é para onde o usuário será navegado. Para manter o contexto da categoria, feed ou artigo específicos o valor id entre colchetes, {id}, é transportado do URI de entrada para o Uri mapeado, da seguinte forma:

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

Você talvez tenha outras razões para usar um mapeador de URI, como, por exemplo, extensibilidade de pesquisa, mas não é necessário usar uma quadro secundário. Nesse aplicativo, foi uma decisão de estilo usar o mapeador de URI. A equipe sentiu que as URIs mais curtas eram mais elegantes e fáceis de usar. Como alternativa, os quadros secundários poderiam ter especificado um URI específico de página (como os valores MappedUri) para efeito semelhante.

Independentemente dos meios, depois que o URI do quadro secundário é mapeado para a página apropriada, o usuário chega na página Category com uma lista de seus artigos. Missão cumprida. Para saber mais sobre quadros, consulte wpdev.ms/secondarytiles.

Mas espere, tem mais!

Há muito mais sobre esse aplicativo do que abordei aqui. Certifique-se de dar uma olhada no código para saber mais sobre como a equipe abordou esses e outros problemas. Por exemplo, SynFeedParser.cs tem uma ótima maneira de limpar os dados de feeds que algumas vezes estão repletos de marcas HTML.

Lembre-se simplesmente de que esse é um instantâneo do trabalho dos estagiários ao final de 12 semanas, menos uma pequena limpeza. Desenvolvedores profissionais talvez prefiram codificar algumas partes de maneira diferente. Contudo, acho que o aplicativo fez um grande trabalho na integração de um banco de dados local, um agente de segundo plano e quadros. Espero que tenham aproveitado essa olhada “nos bastidores.” Boa codificação!

Matt Stroshane escreve a documentação destinada ao desenvolvedor para a equipe do Windows Phone. As outras contribuições de Matt para a Biblioteca MSDN incluem produtos como o SQL Server, SQL Azure e Visual Studio. Quando ele não está escrevendo, você pode encontrá-lo nas ruas de Seattle, treinando para a sua próxima maratona. Siga-o no Twitter em twitter.com/mattstroshane.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Francisco Aguilera, Thomas Fennel, John Gallardo, Sean McKenna, Suman Malani, Ayomikun (George) Okeowo e Himadri Sarkar