MVVM

Aproveitando os recursos do Windows 8 com o MVVM

Brent Edwards

Baixar o código de exemplo

O Windows 8 apresenta inúmeros novos recursos que os desenvolvedores poderão explorar para criar aplicativos interessantes e uma sofisticada experiência do usuário. Infelizmente, esses recursos nem sempre são simples em termos de teste de unidade. Recursos como compartilhamento e blocos secundários podem tornar o aplicativo mais interativo e agradável, porém menos aberto a testes.

Neste artigo, examinarei diversas formas de programar um aplicativo para usar recursos como compartilhamento, configurações, blocos secundários, configurações de aplicativo e armazenamento de aplicativo. Utilizando o padrão MVVM (Model-View-ViewModel), injeção de dependência e alguma abstração, demonstrarei como explorar esses recursos e, ao mesmo tempo, manter a camada de apresentação amigável em termos de teste de unidade.

Sobre o aplicativo de exemplo

Para ilustrar os conceitos a serem discutidos neste artigo, usei o MVVM na compilação de um aplicativo de exemplo da Windows Store que permite ao usuário visualizar postagens de blogs no feed RSS de seus blogs favoritos. O aplicativo ilustra como:

  • Compartilhar informações sobre postagens de blog com outros aplicativos via botão Compartilhar.
  • Alterar os blogs que o usuário deseja ler com o botão Configurações
  • Fixando uma postagem de blog favorito na tela Iniciar para leitura posterior com blocos secundários
  • Salvar blogs favoritos para visualização em todos os dispositivos utilizando configurações de roaming

Além do aplicativo de exemplo, utilizei a funcionalidade específica do Windows 8, tema deste artigo, e a abstraí em uma biblioteca de códigos abertos chamada Charmed. Charmed pode ser usada como uma biblioteca auxiliar ou simplesmente de consulta. O objetivo da Charmed é ser uma biblioteca multiplataforma de suporte ao MVVM para Windows 8 e Windows Phone 8. Falarei mais sobre a modalidade Windows Phone 8 da biblioteca em um futuro artigo. Confira o progresso da biblioteca Charmed em bit.ly/17AzFxW.

Meu objetivo com este artigo e o código de exemplo é demonstrar a abordagem que adotei aos aplicativos testáveis com o MVVM, empregando alguns dos novos recursos oferecidos pelo Windows 8.

Visão geral do MVVM

Antes de me aprofundar no código e nos recursos específicos do Windows 8, apresentarei rapidamente o MVVM. O MVVM é um padrão de design que conquistou enorme popularidade nos últimos anos entres as tecnologias baseadas em XAML, como Windows Presentation Foundation (WPF), Silverlight, Windows Phone 7, Windows Phone 8 e Windows 8 (o Tempo de Execução do Windows, ou WinRT). O MVVM desmembra a arquitetura dos aplicativos em três camadas lógicas: Modelo, Modelo de exibição e Exibição, conforme mostrado na Figura 1.

The Three Logical Layers of Model-­View-ViewModel
Figura 1 As três camadas lógicas do Model-­View-ViewModel

A camada Modelo abriga a lógica de negócios do aplicativo: objetos de negócios, validação de dados, acesso a dados, e assim por diante. Na realidade, a camada Modelo normalmente se subdivide em mais camadas e provavelmente em inúmeras outras camadas. Conforme mostrado na Figura 1, a camada Modelo é o alicerce lógico, ou fundamento, do aplicativo.

A camada Modelo de exibição contém a lógica de apresentação do aplicativo e essa inclui dados a serem exibidos, propriedades destinadas a ajudar a habilitar os elementos da interface do usuário ou torná-los visíveis, e métodos que irão interagir com as camadas Modelo e Exibição. Basicamente, a camada Modelo de exibição é uma representação independente da exibição do estado atual da interface do usuário. Digo “independente da exibição” por simplesmente fornecer dados e métodos com os quais a exibição deverá interagir, mas não determina a forma de a exibição representar esses dados ou permitir ao usuário interagir com os métodos citados. Conforme mostrado na Figura 1, a camada Modelo de exibição se situa de maneira lógica entre as camadas Modelo e Exibição e é capaz de interagir com ambas. A camada Modelo de exibição contém códigos que anteriormente se situavam no codebehind da camada Exibição.

A camada Exibição contém a apresentação real do aplicativo. Nos aplicativos baseados em XAML, como os do Tempo de Execução do Windows, a camada Exibição consiste principalmente, e não totalmente, em XAML. A camada Exibição explora o poderoso mecanismo de vinculação de dados XAML para associar-se a propriedades no modelo de exibição, aplicando uma aparência aos dados que de outra forma não teriam nenhuma representação visual. Conforme mostrado na Figura 1, a camada Modelo é o ápice lógico do aplicativo. A camada Exibição interage diretamente com a camada Modelo de exibição, porém desconhece totalmente a camada Modelo.

A principal finalidade do padrão MVVM é desvincular a apresentação de determinado aplicativo de sua funcionalidade. Ao fazer isso, o aplicativo passa a ser mais condizente com os testes de unidade, pois a funcionalidade agora reside em POCOs (Plain Old CLR Objects), e não em exibições com seus próprios ciclos de vida.

Contratos

O Windows 8 introduziu o conceito de contratos, isto é, acordos entre dois ou mais aplicativos no sistema do usuário. Esses contratos oferecem consistência entre todos os aplicativos, permitindo aos desenvolvedores explorar a funcionalidade a partir de qualquer aplicativo compatível. O aplicativo pode declarar os contratos compatíveis no arquivo Package.appxmanifest, conforme mostrado na Figura 2.

Contracts in the Package.appxmanifest File
Figura 2 Contratos no arquivo Package.appxmanifest

Embora o suporte a contratos seja opcional, normalmente é uma excelente ideia. Há três contratos em particular que devem receber suporte dos aplicativos, a saber Compartilhamento, Configurações e Pesquisa), pois estão sempre disponíveis no menu Botões, conforme mostrado na Figura 3.

The Charms Menu
Figura 3 O menu Botões

Irei me concentrar em dois tipos de contrato: compartilhamento e configurações.

Compartilhamento

O contrato de compartilhamento permite ao aplicativo compartilhar dados específicos ao contexto com outros aplicativos no sistema do usuário. Existem dois lados do contrato de compartilhamento: a fonte e o destino. A fonte representa o aplicativo que está executando o compartilhamento. Ela fornece certos dados para compartilhamento, em qualquer formato necessário. O destino representa o aplicativo que recebe os dados compartilhados. Visto que o botão Compartilhar está sempre disponível ao usuário no menu Botões, gostaria de definir então o aplicativo de exemplo como, no mínimo, uma fonte de compartilhamento. Nem todos os aplicativos têm de ser um destino de compartilhamento, pois nem todos precisam aceitar dados de outras fontes. Contudo, há uma chance muito boa de qualquer aplicativo em particular ter, no mínimo, algo que valha a pena ser compartilhado com outros aplicativos. Por essa razão, a maioria dos aplicativos provavelmente julgará útil ser uma fonte de compartilhamento.

Quando o usuário pressiona o botão de Compartilhamento, um objeto chamado Share Broker inicia o processo de separar os dados e compartilhamentos (se houver algum) do aplicativo e enviá-los ao destino de compartilhamento de acordo com as especificações do usuário. Posso usar um objeto chamado DataTransferManager para compartilhar dados durante o processo. O DataTransferManager inclui um evento chamado DataRequested, que é gerado ao pressionar o botão Compartilhamento. O código a seguir mostra como recuperar referências a elementos do DataTransferManager e inscrever-se no evento DataRequested:

public void Initialize()
{
  this.DataTransferManager = DataTransferManager.GetForCurrentView();
  this.DataTransferManager.DataRequested += 
    this.DataTransferManager_DataRequested;
}
private void DataTransferManager_DataRequested(
  DataTransferManager sender, DataRequestedEventArgs args)
{
  // Do stuff ...
}

Chamar DataTransferManager.GetForCurrentView retorna uma referência ao DataTransferManager ativo para a exibição atual. Embora seja possível inserir esse código em modelos de exibição, cria-se uma forte dependência no DataTransferManager, uma classe selada que não pode ser simulada nos testes de unidade. Por estar realmente interessado em manter meu aplicativo o mais disponível possível para testes, a solução não é a ideal. Uma melhor solução seria abstrair a interação com o DataTransferManager para uma classe auxiliar e definir uma interface a ser implementada pela referida classe.

Antes de abstrair a interação, devo decidir que partes realmente importam. Há três partes da interação com o DataTransferManager com as quais me importo:

  1. Inscrever-se no evento DataRequested quando minha exibição for ativada.
  2. Cancelar a inscrição no evento DataRequested quando minha exibição for desativada.
  3. Poder adicionar dados ao DataPackage.

Com esses três pontos em mente, minha interface se materializa:

public interface IShareManager
{
  void Initialize();
  void Cleanup();
  Action<DataPackage> OnShareRequested { get; set; }
}

A inicialização deve obter uma referência ao DataTransferManager e inscrever-se no evento DataRequested: A limpeza deve cancelar a inscrição no evento DataRequested. O OnShareRequested representa o local onde posso definir quais métodos serão chamados se o evento DataRequested já foi criado. Agora, posso implementar o IShareManager, conforme mostrado na Figura 4.

Figura 4 Implementando IShareManager

public sealed class ShareManager : IShareManager
{
  private DataTransferManager DataTransferManager { get; set; }
  public void Initialize()
  {
    this.DataTransferManager = DataTransferManager.GetForCurrentView();
    this.DataTransferManager.DataRequested +=
      this.DataTransferManager_DataRequested;
  }
  public void Cleanup()
  {
    this.DataTransferManager.DataRequested -=
      this.DataTransferManager_DataRequested;
  }
  private void DataTransferManager_DataRequested(
    DataTransferManager sender, DataRequestedEventArgs args)
  {
    if (this.OnShareRequested != null)
    {
      this.OnShareRequested(args.Request.Data);
    }
  }
  public Action<DataPackage> OnShareRequested { get; set; }
}

Ao criar o evento DataRequested, os argumentos do evento que surgem contêm um DataPackage. O DataPackage representa o local onde os dados reais compartilhados devem ser colocados, motivo pelo qual a ação para OnShareRequested adota um DataPackage como parâmetro. Após definir a interface IShareManager e implementá-la com o ShareManager, estou agora pronto para incluir compartilhamento no modelo de exibição, sem sacrificar a capacidade de teste que procuro.

Após usar minha inversão preferencial de contêiner de controle (IoC) para injetar certa instância do IShareManager no modelo de visualização, poderei disponibilizá-la para uso, conforme mostrado na Figura 5.

Figura 5 Conectando IShareManager

public FeedItemViewModel(IShareManager shareManager)
{
  this.shareManager = shareManager;
}
public override void LoadState(
  FeedItem navigationParameter, Dictionary<string, 
  object> pageState)
{
  this.shareManager.Initialize();
  this.shareManager.OnShareRequested = ShareRequested;
}
public override void SaveState(Dictionary<string, 
  object> pageState)
{
  this.shareManager.Cleanup();
}

O LoadState é chamado ao ativar a página e o modelo de exibição; por sua vez, o SaveState é chamado ao desativar a página e o modelo de exibição. Após configurar o ShareManager por completo e deixá-lo pronto para tratar compartilhamentos, terei de implementar o método ShareRequested a ser chamado quando o usuário iniciar o compartilhamento. Quero compartilhar algumas informações sobre certa postagem em blog (FeedItem), conforme mostrado na Figura 6.

Figura 6 Preenchendo o DataPackage no ShareRequested

private void ShareRequested(DataPackage dataPackage)
{
  // Set as many data types as possible.
  dataPackage.Properties.Title = this.FeedItem.Title;
  // Add a Uri.
  dataPackage.SetUri(this.FeedItem.Link);
  // Add a text-only version.
  var text = string.Format(
    "Check this out! {0} ({1})", 
    this.FeedItem.Title, this.FeedItem.Link);
  dataPackage.SetText(text);
  // Add an HTML version.
  var htmlBuilder = new StringBuilder();
  htmlBuilder.AppendFormat("<p>Check this out!</p>", 
    this.FeedItem.Author);
  htmlBuilder.AppendFormat(
    "<p><a href='{0}'>{1}</a></p>", 
    this.FeedItem.Link, this.FeedItem.Title);
  var html = HtmlFormatHelper.CreateHtmlFormat(htmlBuilder.ToString());
  dataPackage.SetHtmlFormat(html);
}

Optei por compartilhar diversos tipos diferentes de dados. De modo geral, essa é uma excelente ideia, pois não há controle nenhum sobre que aplicativos o usuário possui em seu sistema ou que tipos de dados esses aplicativos aceitam. Vale a pena lembrar que o compartilhamento é basicamente um cenário do tipo dispare e esqueça. Você não tem ideia nenhuma de qual aplicativo o usuário irá escolher para compartilhamento nem o que o aplicativo fará com os dados compartilhados. De modo a compartilhar com o maior público possível, forneci um título, uma URI e uma versão somente texto, bem como uma versão em HTML.

Configurações

O contrato de configurações permite ao usuário alterar configurações específicas ao contexto em determinado aplicativo. Estas podem ser configurações com poder de afetar o aplicativo como um todo ou apenas itens específicos relacionados ao atual contexto. Os usuários do Windows 8 se verão condicionados a usar o botão Configurações para fazer alterações no aplicativo, e minha intenção é tornar o aplicativo de exemplo compatível pelo fato de estar sempre disponível ao usuário via menu Botões. Na verdade, se declarar capacidade de acesso à Internet via arquivo Package.appxmanifest, o aplicativo terá de implementar o contrato de configurações fornecendo um link para uma política de privacidade baseada na Web em algum lugar do menu Configurações. Aplicativos que utilizam modelos do Visual Studio 2012 declaram automaticamente sua capacidade de acesso à Internet tão logo ativados, e não podemos ignorar o fato.

Quando o usuário pressiona o botão Configurações, o sistema operacional começa a criar de maneira dinâmica o menu a ser exibido. O menu e o submenu associado são controlados pelo sistema operacional. Não posso controlar a aparência do menu nem do submenu, mas posso adicionar opções. Um objeto chamado SettingsPane me notifica sempre que o usuário selecionar o botão Configurações por meio do evento CommandsRequested. Obter uma referência para o SettingsPane e inscrever-se no evento CommandsRequested é algo extremamente simples:

public void Initialize()
{
  this.SettingsPane = SettingsPane.GetForCurrentView();
  this.SettingsPane.CommandsRequested += 
    SettingsPane_CommandsRequested;
}
private void SettingsPane_CommandsRequested(
  SettingsPane sender, 
  SettingsPaneCommandsRequestedEventArgs args)
{
  // Do stuff ...
}

O problema, porém, representa outra forte dependência. Desta vez, a dependência é SettingsPane, uma classe diferente que não pode ser simulada. Pelo fato de querer estar apto a executar um teste de unidade no modelo de exibição que utiliza SettingsPane, preciso obter referências relacionadas, assim como fiz com o DataTransferManager. Na verdade, minhas interações com SettingsPane são bastante semelhantes às interações com DataTransferManager:

  1. Inscrever-se no evento CommandsRequested para a exibição atual.
  2. Canelar inscrição no evento CommandsRequested para a exibição atual.
  3. Adicionar meus próprios objetos SettingsCommand ao criar o evento.

Portanto, a interface que deverei abstrair é bastante semelhante à interface IShare­Manager:

public interface ISettingsManager
{
  void Initialize();
  void Cleanup();
  Action<IList<SettingsCommand>> OnSettingsRequested { get; set; }
}

A inicialização deve obter uma referência a SettingsPane e inscrever-se no evento CommandsRequested: A limpeza deve cancelar a inscrição no evento CommandsRequested. O OnSettingsRequested representa o local onde posso definir quais métodos serão chamados se o evento CommandsRequested já foi criado. Agora, posso implementar o ISettings­Manager, conforme mostrado na Figura 7.

Figura 7 Implementando ISettingsManager

public sealed class SettingsManager : ISettingsManager
{
  private SettingsPane SettingsPane { get; set; }
  public void Initialize()
  {
    this.SettingsPane = SettingsPane.GetForCurrentView();
    this.SettingsPane.CommandsRequested += 
      SettingsPane_CommandsRequested;
  }
  public void Cleanup()
  {
    this.SettingsPane.CommandsRequested -= 
      SettingsPane_CommandsRequested;
  }
  private void SettingsPane_CommandsRequested(
    SettingsPane sender, SettingsPaneCommandsRequestedEventArgs args)
  {
    if (this.OnSettingsRequested != null)
    {
      this.OnSettingsRequested(args.Request.ApplicationCommands);
    }
  }
  public Action<IList<SettingsCommand>> OnSettingsRequested { get; set; }
}

Ao criar o evento CommandsRequested, os argumentos do evento basicamente me oferecem acesso à lista de objetos SettingsCommand que representam as opções do menu Configurações. Para adicionar minhas próprias opções do menu Configurações, basta adicionar uma instância SettingsCommand à lista. Objetos SettingsCommand não requerem muito; apenas um identificador exclusivo, texto dos rótulos e códigos a serem executados quando o usuário selecionar a opção.

Usei meu contêiner IoC para injetar uma instância de ISettingsManager ao modelo de exibição e configurei-o para inicializar e executar uma limpeza, conforme mostrado na Figura 8.

Figura 8 Conectando ISettingsManager

public ShellViewModel(ISettingsManager settingsManager)
{
  this.settingsManager = settingsManager;
}
public void Initialize()
{
  this.settingsManager.Initialize();
  this.settingsManager.OnSettingsRequested = 
    OnSettingsRequested;
}
public void Cleanup()
{
  this.settingsManager.Cleanup();
}

Usarei as Configurações para permitir aos usuários alterar quais feeds RSS poderão ser visualizados no aplicativo de exemplo. Trata-se de algo que gostaria que o usuário fosse capaz de alterar em qualquer lugar no aplicativo, por isso incluí o ShellViewModel, que é instanciado quando o aplicativo inicializa. Se quisesse que os feeds RSS fossem alterados apenas em uma das outras exibições, incluiria o código de configurações no modelo de exibição associado.

O Tempo de Execução do Windows carece de uma Funcionalidade integrada para criar e manter submenus de configurações. Há uma exigência muito maior por codificação do que a necessária para obter a funcionalidade que deveria ser consistente em todos os aplicativos. Felizmente, não sou o único que pensa assim. Tim Heuer, gerente de programas da equipe de XAML da Microsoft, criou uma excelente estrutura chamada Callisto, que ajuda nesse ponto problemático. O Callisto está disponível em GitHub (bit.ly/Kijr1S) e NuGet (bit.ly/112ehch). Eu a utilizo no aplicativo de exemplo e recomendo dar uma conferida.

Por ter o SettingsManager totalmente vinculado ao meu modelo de exibição, preciso apenas fornecer o código a ser executado quando as configurações forem solicitadas, conforme mostrado na Figura 9.

Figura 9 Mostrando SettingsView em SettingsRequested com o Callisto

private void OnSettingsRequested(IList<SettingsCommand> commands)
{
  SettingsCommand settingsCommand =
    new SettingsCommand("FeedsSetting", "Feeds", (x) =>
  {
    SettingsFlyout settings = new Callisto.Controls.SettingsFlyout();
    settings.FlyoutWidth =
      Callisto.Controls.SettingsFlyout.SettingsFlyoutWidth.Wide;
    settings.HeaderText = "Feeds";
    var view = new SettingsView();
    settings.Content = view;
    settings.HorizontalContentAlignment = 
      HorizontalAlignment.Stretch;
    settings.VerticalContentAlignment = 
      VerticalAlignment.Stretch;
    settings.IsOpen = true;
  });
  commands.Add(settingsCommand);
}

Criei um novo SettingsCommand e atribuí o ID “FeedsSetting” e o texto de rótulo “Feeds.” O lambda usado para o callback, que é chamado quando o usuário seleciona o item de menu “Feeds”, utiliza o controle SettingsFlyout do Callisto. O controle SettingsFlyout faz o trabalho pesado de onde inserir o submenu, com que tamanho criá-lo e quando abrir e fechá-lo. Tudo o que preciso fazer é instruí-lo se quero a versão ampla ou estreita, inserir algum conteúdo e texto de cabeçalho e definir IsOpen como verdadeiro para abri-lo. Recomendo também configurar o HorizontalContentAlignment e o VerticalContent­Alignment como Esticar. De outra forma, o conteúdo não corresponderá ao tamanho do SettingsFlyout.

Barramento de mensagem

Um ponto importante ao lidar com o contrato de configurações é que qualquer alteração nas configurações deverá ser aplicada ao e refletida imediatamente no aplicativo. H[a inúmeras formas pelas quais é possível concluir a difusão das alterações nas configurações feitas pelo usuário. O método de minha preferência é o barramento de mensagem (também conhecido como agregador de evento). O barramento de mensagem é um sistema de publicação de mensagens em nível do aplicativo. O conceito de barramento de mensagem não é integrado ao Tempo de Execução do Windows, ou seja, tenho de criar ou usar um barramento de outra estrutura. Incluí uma implementação de barramento de mensagem que usei em diversos projetos com a estrutura Charmed. Você pode encontrar a fonte em bit.ly/12EBHrb. Há também várias outras excelentes implementações. A Caliburn.Micro tem o EventAggregator, e a MVVM Light tem o Messenger. Normalmente, todas as implementações seguem o mesmo padrão, oferecendo meios de inscrever-se, cancelar inscrições e publicar mensagens.

Utilizando o barramento de mensagem do Charmed no cenário de configurações, configurei meu MainViewModel (o que exibe os feeds) para inscrição em um FeedsChangedMessage:

this.messageBus.Subscribe<FeedsChangedMessage>((message) =>
  {
    LoadFeedData();
  });

Após configurar o MainViewModel para localizar alterações nos feeds, configurei SettingsViewModel para publicar o FeedsChanged­Message quando o usuário adicionar ou remover feeds RSS:

this.messageBus.Publish<FeedsChangedMessage>(new FeedsChangedMessage());

Sempre que houver um bus de mensagem envolvido, é importante que cada seção do aplicativo utilize a mesma instância de bus de mensagem. Portanto, certifiquei-me de configurar meu contêiner IoC para disponibilizar uma instância singleton a cada solicitação para resolver um IMessageBus.

Agora, o aplicativo de exemplo está configurado para permitir ao usuário executar mudanças nos feeds RSS exibidos via botão Configurações e atualizar exibição principal para refletir essa alterações.

Configurações de roaming

Outra coisa bastante interessante introduzida pelo Windows 8 é o conceito de configurações de roaming. As configurações de roaming permite aos desenvolvedores de aplicativos migrar pequenas quantidades de dados entre todos os dispositivos do usuário. Esses dados devem ter menos de 100 KB e um limite de bits de informações necessário à criação de uma experiência do usuário persistente e personalizada em todos os dispositivos. No caso do aplicativo de exemplo, busquei uma forma de persistir os feeds RSS que o usuário deseja ler em todos os dispositivos.

Normalmente, o contrato de configurações discutido anteriormente caminha lado a lado com as configurações de roaming. Faz sentido apenas se as personalizações que eu autorizar o usuário a fazer por meio do contrato de configurações forem persistidas em todos os dispositivos por meio de configurações de roaming.

Obter acesso às configurações de roaming, como os demais problemas analisados até aqui, é bastante simples. A classe ApplicationData dá acesso a LocalSettings e RoamingSettings Inserir algo em RoamingSettings é tão simples quanto disponibilizar uma chave e um objeto:

ApplicationData.Current.RoamingSettings.Values[key] = value;

Embora seja fácil trabalhar com ApplicationData, trata-se de outra classe selada que não pode ser simulada em testes de unidade. Sendo assim, no interesse de manter meus modelos de exibição o mais aberto possível a testes, foi necessário abstrair a interação com ApplicationData. Antes de definir uma interface para abstrair a funcionalidade das configurações de roaming subjacente, tive de decidir o que pretendia fazer com ela:

  1. Conferir se determinada chave existe.
  2. Adicionar ou atualizar uma configuração.
  3. Remover uma configuração.
  4. Obter uma configuração.

Agora, disponho de tudo o que preciso para criar uma interface chamada ISettings:

public interface ISettings
{
  void AddOrUpdate(string key, object value);
  bool TryGetValue<T>(string key, out T value);
  bool Remove(string key);
  bool ContainsKey(string key);
}

Após definir a interface, é necessário implementá-la, conforme a Figura 10 mostra.

Figura 10 Implementando ISettings

public sealed class Settings : ISettings
{
  public void AddOrUpdate(string key, object value)
  {
    ApplicationData.Current.RoamingSettings.Values[key] = value;
  }
  public bool TryGetValue<T>(string key, out T value)
  {
    var result = false;
    if (ApplicationData.Current.RoamingSettings.Values.ContainsKey(key))
    {
      value = (T)ApplicationData.Current.RoamingSettings.Values[key];
      result = true;
    }
    else
    {
      value = default(T);
    }
    return result;
  }
  public bool Remove(string key)
  {
    return ApplicationData.Current.RoamingSettings.Values.Remove(key);
  }
  public bool ContainsKey(string key)
  {
    return ApplicationData.Current.RoamingSettings.Values.ContainsKey(key);
  }
}

TryGetValue verificará primeiro se determinada chave existe e atribuirá o valor ao parâmetro de saída se ela existir. Em vez de lançar uma exceção se a chave não for encontrada, o parâmetro retorna um booleano indicando se a chave foi encontrada ou não. O restante dos métodos é mais ou menos autoexplicativo.

Agora, posso deixar meu contêiner IoC resolver o ISettings e conferi-lo ao meu SettingsViewModel. Finalizado o processo, o modelo de exibição utilizará as configurações para carregar os feeds do usuário de modo que sejam editados, conforme mostrado na Figura 11.

Figura 11 Carregando e salvando feeds do usuário

public SettingsViewModel(
  ISettings settings,
  IMessageBus messageBus)
{
  this.settings = settings;
  this.messageBus = messageBus;
  this.Feeds = new ObservableCollection<string>();
  string[] feedData;
  if (this.settings.TryGetValue<string[]>(Constants.FeedsKey, out feedData))
  {
    foreach (var feed in feedData)
    {
      this.Feeds.Add(feed);
    }
  }
}
public void AddFeed()
{
  this.Feeds.Add(this.NewFeed);
  this.NewFeed = string.Empty;
  SaveFeeds();
}
public void RemoveFeed(string feed)
{
  this.Feeds.Remove(feed);
  SaveFeeds();
}
private void SaveFeeds()
{
  this.settings.AddOrUpdate(Constants.FeedsKey, this.Feeds.ToArray());
  this.messageBus.Publish<FeedsChangedMessage>(new FeedsChangedMessage());
}

Algo a ser observado sobre o código na Figura 11 é que os dados salvos de fato nas configurações são uma matriz de cadeias de caracteres. Pelo fato de as configurações de roaming terem um limite de 100 KB, é preciso manter as coisas simples e adotar tipos primitivos

Blocos secundários

Desenvolver aplicativos capazes de atrair os usuários pode ser por si só um desafio. Como fazer então para os usuários voltarem após terem instalado o aplicativo? Algo que pode ser de grande valia nesse desafio são os blocos secundários. O bloco secundário oferece o recurso de manter um vínculo profundo com o aplicativo, permitindo ao usuário ignorar todo o seu restante e ir direito ao ponto que mais lhe interessa. O bloco secundário fixa-se na tela inicial do usuário com um ícone de sua escolha. Ao ser tocado, o bloco secundário abre o aplicativo com argumentos instruindo o aplicativo a onde ir exatamente e o que carregar. Oferecer a funcionalidade dos blocos secundários aos usuários é uma excelente maneira de permitir-lhes personalizar sua experiência, fazendo com que retornem em busca de mais.

Blocos secundários são mais complicados que outros tópicos abordados neste artigo, pois há uma infinidade de coisas a serem implementadas antes que a experiência total de usá-los funcione perfeitamente.

Fixar um bloco secundário envolve instanciar a classe SecondaryTile. O construtor SecondaryTile emprega diversos parâmetros que o ajudarão a determinar a aparência do bloco, incluindo o nome de exibição, a URI do arquivo de imagem do logotipo a ser usado no bloco e argumentos de cadeia de caracteres que serão transmitidos ao aplicativo quando o bloco for pressionado. Após instanciar o SecondaryTile, terei de chamar um método que for fim exibirá uma janela pop-up solicitando ao usuário permissão para fixar o bloco, conforme mostrado na Figura 12.

SecondaryTile Requesting Permission to Pin a Tile to the Start Screen
Figura 12 SecondaryTile solicitando permissão para fixar um bloco na tela Iniciar

Após o usuário pressionar Fixar para iniciar, metade do trabalho estará completa. A segunda metade é configurar o aplicativo para dar suporte à vinculação profunda utilizando os argumentos fornecidos pelo bloco quando pressionado. Antes de entrar na segunda metade, deixe-me explicar como irei implementar a primeira metade de uma maneira testável.

Visto que o SecondaryTile utiliza métodos que interagem diretamente com o OS e este, por sua vez, mostra os componentes da interface do usuário, poderei então usá-lo diretamente a partir de meus modelos de exibição sem sacrificar a capacidade de teste. Portanto, irei abstrair outra interface à qual darei o nome de ISecondaryPinner (ela deverá me permitir fixar e desafixar blocos e verificar se determinado bloco já foi fixado):

public interface ISecondaryPinner
{
  Task<bool> Pin(FrameworkElement anchorElement,
    Placement requestPlacement, TileInfo tileInfo);
  Task<bool> Unpin(FrameworkElement anchorElement,
    Placement requestPlacement, string tileId);
  bool IsPinned(string tileId);
}

Observe que tanto Fixar quanto Desafixar retornam Task<bool>. O motivo é que o SecondaryTile utiliza tarefas assíncronas para solicitar ao usuário a fixação ou desafixação de um bloco. Significa também que meus métodos de fixação e desafixação ISecondaryPinner podem ser colocados em espera.

Da mesmo forma, observe que Fixar e Desafixar adotam como parâmetros um FrameworkElement e um valor de enumeração de Posicionamento. O motivo é que o SecondaryTile necessita de um retângulo e de um Posicionamento para instruí-lo onde colocar a janela pop-up para solicitação de fixação. Planejo que minha implementação do SecondaryPinner calcule o retângulo com base no FrameworkElement que foi passado.

Por fim, crio uma classe auxiliar, TileInfo, para passar pelos parâmetros obrigatórios e opcionais usados pelo SecondaryTile, conforme mostrado na Figura 13.

Figura 13 A classe auxiliar TileInfo

public sealed class TileInfo
{
  public TileInfo(
    string tileId,
    string shortName,
    string displayName,
    TileOptions tileOptions,
    Uri logoUri,
    string arguments = null)
  {
    this.TileId = tileId;
    this.ShortName = shortName;
    this.DisplayName = displayName;
    this.Arguments = arguments;
    this.TileOptions = tileOptions;
    this.LogoUri = logoUri;
    this.Arguments = arguments;
  }
  public TileInfo(
    string tileId,
    string shortName,
    string displayName,
    TileOptions tileOptions,
    Uri logoUri,
    Uri wideLogoUri,
    string arguments = null)
  {
    this.TileId = tileId;
    this.ShortName = shortName;
    this.DisplayName = displayName;
    this.Arguments = arguments;
    this.TileOptions = tileOptions;
    this.LogoUri = logoUri;
    this.WideLogoUri = wideLogoUri;
    this.Arguments = arguments;
  }
  public string TileId { get; set; }
  public string ShortName { get; set; }
  public string DisplayName { get; set; }
  public string Arguments { get; set; }
  public TileOptions TileOptions { get; set; }
  public Uri LogoUri { get; set; }
  public Uri WideLogoUri { get; set; }
}

TileInfo possui dois construtores que podem ser usados, dependendo dos dados. Agora, posso implementar o ISecondaryPinner, conforme mostrado na Figura 14.

Figura 14 Implementação de ISecondaryPinner

public sealed class SecondaryPinner : ISecondaryPinner
{
  public async Task<bool> Pin(
    FrameworkElement anchorElement,
    Placement requestPlacement,
    TileInfo tileInfo)
  {
    if (anchorElement == null)
    {
      throw new ArgumentNullException("anchorElement");
    }
    if (tileInfo == null)
    {
      throw new ArgumentNullException("tileInfo");
    }
    var isPinned = false;
    if (!SecondaryTile.Exists(tileInfo.TileId))
    {
      var secondaryTile = new SecondaryTile(
        tileInfo.TileId,
        tileInfo.ShortName,
        tileInfo.DisplayName,
        tileInfo.Arguments,
        tileInfo.TileOptions,
        tileInfo.LogoUri);
      if (tileInfo.WideLogoUri != null)
      {
        secondaryTile.WideLogo = tileInfo.WideLogoUri;
      }
      isPinned = await secondaryTile.RequestCreateForSelectionAsync(
        GetElementRect(anchorElement), requestPlacement);
    }
    return isPinned;
  }
  public async Task<bool> Unpin(
    FrameworkElement anchorElement,
    Placement requestPlacement,
    string tileId)
  {
    var wasUnpinned = false;
    if (SecondaryTile.Exists(tileId))
    {
      var secondaryTile = new SecondaryTile(tileId);
      wasUnpinned = await secondaryTile.RequestDeleteForSelectionAsync(
        GetElementRect(anchorElement), requestPlacement);
    }
    return wasUnpinned;
  }
  public bool IsPinned(string tileId)
  {
    return SecondaryTile.Exists(tileId);
  }
  private static Rect GetElementRect(FrameworkElement element)
  {
    GeneralTransform buttonTransform =
      element.TransformToVisual(null);
    Point point = buttonTransform.TransformPoint(new Point());
    return new Rect(point, new Size(
      element.ActualWidth, element.ActualHeight));
  }
}

A fixação verificará primeiramente se o bloco solicitado ainda não existe e solicitará ao usuário para fixá-lo. A desafixação verificará primeiramente se o bloco solicitado existe e solicitará ao usuário para desafixá-lo. Ambos retornarão um booleano indicando se a fixação ou desafixação foi ou não bem-sucedida.

Agora, posso injetar uma instância de ISecondaryPinner em meu modelo de exibição a fim de colocá-lo em uso, conforme mostrado na Figura 15.

Figura 15 Fixando e desafixando com ISecondaryPinner

public FeedItemViewModel(
  IShareManager shareManager,
  ISecondaryPinner secondaryPinner)
{
  this.shareManager = shareManager;
  this.secondaryPinner = secondaryPinner;
}
public async Task Pin(FrameworkElement anchorElement)
{
  var tileInfo = new TileInfo(
    FormatSecondaryTileId(),
    this.FeedItem.Title,
    this.FeedItem.Title,
    TileOptions.ShowNameOnLogo | TileOptions.ShowNameOnWideLogo,
    new Uri("ms-appx:///Assets/Logo.png"),
    new Uri("ms-appx:///Assets/WideLogo.png"),
    this.FeedItem.Id.ToString());
    this.IsFeedItemPinned = await this.secondaryPinner.Pin(
    anchorElement,
    Windows.UI.Popups.Placement.Above,
    tileInfo);
}
public async Task Unpin(FrameworkElement anchorElement)
{
  this.IsFeedItemPinned = !await this.secondaryPinner.Unpin(
    anchorElement,
    Windows.UI.Popups.Placement.Above,
    this.FormatSecondaryTileId());
}

Em Fixar, crio uma instância auxiliar TileInfo, dando a ela um ID exclusivamente formatado, o título do feed, URIs para o logotipo e o logotipo maior e o ID do feed na forma de argumento de inicialização. A fixação adota o botão clicado como o elemento âncora para basear o local da janela pop-up de solicitação da fixação. Utilizo o resultado do método SecondaryPinner.Pin para determinar se o item do feed foi fixado.

Em Desafixar, forneço o IF exclusivamente formatado do bloco, utilizando o inverso do resultado para determinar se o item do feed ainda está fixado. Novamente, o botão clicado passa para Desafixado como o elemento âncora da janela pop-up de solicitação da desafixação.

Após concluir esse procedimento e usá-lo para fixar postagens do blog (FeedItem) na tela Iniciar, posso tocar no bloco recém-criado e abrir o aplicativo. Contudo, o aplicativo será aberto de mesma forma anterior, levando-me à página principal e exibindo todas as postagens do blog. Quero que o aplicativo me leve à postagem específica fixada. É nesse ponto onde a segunda metade da funcionalidade entra em ação.

A segunda metade da funcionalidade entra em app.xaml.cs, de onde o aplicativo é aberto, conforme mostrado na Figura 16.

Figura 16 Abrindo o aplicativo

protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
  Frame rootFrame = Window.Current.Content as Frame;
  if (rootFrame.Content == null)
  {
    Ioc.Container.Resolve<INavigator>().
      NavigateToViewModel<MainViewModel>();
  }
  if (!string.IsNullOrWhiteSpace(args.Arguments))
  {
    var storage = Ioc.Container.Resolve<IStorage>();
    List<FeedItem> pinnedFeedItems =
      await storage.LoadAsync<List<FeedItem>>(Constants.PinnedFeedItemsKey);
    if (pinnedFeedItems != null)
    {
      int id;
      if (int.TryParse(args.Arguments, out id))
      {
        var pinnedFeedItem = pinnedFeedItems.FirstOrDefault(fi => fi.Id == id);
        if (pinnedFeedItem != null)
        {
          Ioc.Container.Resolve<INavigator>().
            NavigateToViewModel<FeedItemViewModel>(
            pinnedFeedItem);
        }
      }
    }
  }
  Window.Current.Activate();
}

Adicionei alguns códigos ao final do método OnLaunched substituído para verificar se foram passados argumentos durante a inicialização. Caso os argumentos tenham sido passados, analiso-os em um int a ser usado como o ID do feed. Obtenho o feed com esse ID dos meus feeds salvos e o passo ao FeedItemViewModel para ser exibido. Observe que me assegurei de garantir que a página principal do aplicativo já estava sendo exibida e primeiramente naveguei até ela se não estivesse em exibição. Dessa forma, o usuário pode pressionar o botão Voltar e acessar a página principal, independentemente de já estar ou não executando o aplicativo.

Conclusão

Neste artigo, discorri sobre minha abordagem à implementação de um aplicativo da Windows Store testável utilizando o padrão MVVM, explorando ao mesmo tempo alguns dos novos e interessantes recursos introduzidos pelo Windows 8. Mais especificamente, analise a abstração de compartilhamento, configurações, configurações de roaming e blocos secundários em classes auxiliares que implementam interfaces simuláveis. Empregando essa técnica, posso executar o máximo possível de testes de unidade em minha funcionalidade de modelo de exibição.

Em artigos futuros, me aprofundarei nos detalhes de como compilar efetivamente testes de unidades para esses modelos de exibição já que os configurei para serem mais testáveis. Explorei também formas de aplicar essas técnicas para tornar meus modelos de exibição multiplataforma com o Windows 8, mantendo-os ao mesmo tempo testáveis.

Com certo planejamento, é possível criar aplicativos atraentes por meio de uma inovadora experiência do usuário que aproveita os novos recursos principais do Windows 8 sem sacrificar práticas recomendadas ou testes de unidade.

Brent Edwards é consultor associado principal da Magenic, uma empresa de desenvolvimento da aplicativos personalizados com foco no pacote de programas Microsoft e no desenvolvimento de aplicativos para celulares. Ele é também cofundador do Twin Cities Windows 8 User Group em Minneapolis, Minnesota Para entrar em contato, escreva para brente@magenic.com.

AGRADECEMOS ao seguinte especialista técnico pela revisão deste artigo: Rocky Lhotka (Magenic)
Rockford Lhotka é o CTO da Magenic e o criado da amplamente usada estrutura de desenvolvimento CSLA .NET. Ele é autor de inúmeros livros e participa regularmente como palestrante em grandes conferências em todo o mundo. Rockford é Diretor Regional da Microsoft e MVP. Magenic (www.magenic.com) é uma empresa especializada em planejamento, design criação e manutenção dos sistemas mais críticos à missão de sua empresa. Para obter mais informações, acesse www.lhotka.net.