MVVM

Criando uma camada de apresentação multiplataforma com o MVVM

Brent Edwards

Baixar o código de exemplo

Com o lançamento do Windows 8 e do Windows Phone 8, a Microsoft fez um grande avanço em direção ao desenvolvimento multiplataforma. Agora, os dois sistemas executam no mesmo kernel, o que significa que, com um pouco de planejamento, grande parte do código de seu aplicativo pode ser reutilizado nos dois. Utilizando o padrão Model-View-ViewModel (MVVM), alguns outros padrões comuns de design e alguns truques, você pode escrever uma camada de apresentação multiplataforma que funcionará no Windows 8 e no Windows Phone 8.

Neste artigo, examinarei alguns desafios específicos à multiplataforma que já enfrentei e abordarei soluções que podem ser aplicadas, que permitem que meu aplicativo mantenha uma separação de preocupações clara sem sacrificar a habilidade de escrever bons testes de unidade para ele.

Sobre o aplicativo de exemplo

Na edição de julho de 2013 da MSDN Magazine, apresentei o código de um aplicativo de exemplo da Windows Store e o início de uma estrutura multiplataforma de software livre que desenvolvi, chamada Charmed ("Aproveitando os recursos do Windows 8 com o MVVM", msdn.microsoft.com/magazine/dn296512). Neste artigo, mostrarei como utilizei esse aplicativo e estrutura de exemplo e os desenvolvi para se tornarem mais multiplataforma. Também desenvolvi um aplicativo complementar para o Windows Phone 8 com a mesma funcionalidade, utilizando a mesma estrutura. A estrutura e os aplicativos de exemplo estão disponíveis no GitHub em github.com/brentedwards/Charmed. O código continuará a evoluir conforme eu me mover para o artigo final em minha série sobre o MVVM, que será aprofundada para realmente testar a camada de apresentação e as considerações adicionais sobre código que pode ser testado.

O aplicativo é um leitor de blog simples, chamado Charmed Reader. A versão de cada plataforma do aplicativo tem funcionalidade suficiente para ilustrar alguns dos principais conceitos relacionados ao desenvolvimento de multiplataforma. As duas versões são semelhantes no UX que fornecem, mas ainda se ajustam à aparência de seus respectivos sistemas operacionais.

Estrutura da solução

Qualquer discussão apropriada sobre desenvolvimento de multiplataforma com o Visual Studio 2012 deve começar do início: a estrutura da solução. Embora o Windows 8 e o Windows Phone 8 executem no mesmo kernel, seus aplicativos ainda são compilados de maneira diferente e têm diferentes tipos de projetos. Existem várias maneiras de abordar a criação de uma solução com diferentes tipos de projetos, mas prefiro ter uma solução que inclua todos os meus projetos específicos à plataforma. A Figura 1 mostra a estrutura da solução dos aplicativos de exemplo que apresentarei.

Cross-Platform Charmed Reader Solution Structure
Figura 1 Estrutura da solução Charmed Reader multiplataforma

O Visual Studio permite que você tenha mais de um arquivo de projeto que faça referência a um único arquivo de classe física, permitindo que você adicione um arquivo de classe existente selecionando Adicionar como vínculo (Add as Link), conforme mostrado na Figura 2.

Adding an Existing Item with Add As Link
Figura 2 Adicionando um item existente com Adicionar como vínculo

Utilizando a funcionalidade Adicionar como vínculo, posso escrever grande parte de meu código uma vez e usá-lo para o Windows 8 e o Windows Phone 8. No entanto, não desejo fazer isso para cada arquivo de classe. Como demonstrarei, há situações onde cada plataforma terá sua própria implementação.

Diferenças de exibição

Embora eu possa reutilizar grande parte do código que contém a lógica da minha apresentação, que é escrita em C#, não poderei usar o código da apresentação real, que é escrito em XAML. Isso é porque o Windows 8 e o Windows Phone 8 têm tipos de XAML levemente diferentes que não executam bem entre si para serem intercambiáveis. Parte do problema é a sintaxe, principalmente as declarações de namespace, mas a maior parte é devida aos diferentes controles disponíveis para as plataformas, bem como os diferentes conceitos de estilo implementados. Por exemplo, o Windows 8 usa GridView e ListView de maneira bem pesada, mas esses controles não estão disponíveis no Windows Phone 8. Por outro lado, o Windows Phone 8 tem o controle Pivot e o LongListSelector, que não estão disponíveis no Windows 8.

Apesar dessa falta de capacidade de reutilização do código XAML, ainda é possível usar alguns ativos de design nas duas plataformas, principalmente no design da interface do usuário do Windows Phone. Isso é devido ao conceito de Exibição de ajuste do Windows 8, que tem uma largura fixa de 320 pixels. A Exibição de ajuste usa 320 pixels porque os designers de dispositivos móveis têm feito designs para larguras de tela de 320 pixels há anos. No desenvolvimento de multiplataforma, isso funciona em meu favor porque não preciso propor um design completamente novo para a Exibição de ajuste, posso adaptar meu design do Windows Phone. É claro que preciso me lembrar de que cada plataforma tem seus próprios princípios exclusivos de design, portanto, posso precisar modificar um pouco a aparência natural de cada aplicativo para sua plataforma.

Como mostra a Figura 3, implementei a interface do usuário da Exibição de ajuste do Windows 8 para que fosse muito semelhante, mas não completamente idêntica à interface do usuário do Windows Phone 8. É claro que não sou um designer, e isso é mostrado em meu design pouco atraente da interface. Mas espero que isso ilustre como a Exibição de ajuste do Windows 8 de o Windows Phone 8 podem ser semelhantes no design de suas interfaces do usuário.

Sample App UI for Windows 8 Snap View (left) and Windows Phone 8 (right)
Figura 3 Interface do usuário do aplicativo de exemplo para a Exibição de ajuste do Windows 8 (à esquerda) e o Windows Phone 8 (à direita)

Diferenças do código

Um dos desafios interessantes que enfrentei ao embarcar no desenvolvimento de multiplataforma com o Windows 8 e o Windows Phone 8 foi o fato de que cada plataforma trata determinadas tarefas de maneira diferente. Por exemplo, embora as duas plataformas sigam um esquema de navegação baseado em URI, os parâmetros que utilizam são diferentes. A criação de blocos secundários também é diferente. Embora as duas plataformas deem suporte a eles, o comportamento de cada plataforma é fundamentalmente diferente quando um bloco secundário é tocado. Cada plataforma também tem sua própria maneira de lidar com as configurações dos aplicativos e diferentes classes para interagir com essas configurações.

Finalmente, o Windows 8 tem recursos que o Windows Phone 8 não tem. Os principais conceitos utilizados por meu aplicativo de exemplo do Windows 8 que não têm suporte do Windows Phone 8 são os contratos e o menu Botões. Isso significa que o Windows Phone não dá suporte aos botões Compartilhar ou Configurações.

Portanto, como lidar com essas diferenças fundamentais de código? Há várias técnicas que podem ser utilizadas para isso.

Diretivas de compilador Quando o Visual Studio cria tipos de projeto do Windows 8 e do Windows Phone 8, ele define automaticamente diretivas de compilador específicas à plataforma nas configurações do projeto, ­NETFX_CORE para Windows 8 e WINDOWS_PHONE para Windows Phone 8. Utilizando essas diretivas de compilador, é possível indicar para o Visual Studio o que compilar para cada plataforma. Das técnicas possíveis, essa é a mais básica, mas também a mais confusa. O resultado é um código parecido com um queijo suíço: cheio de furos. Embora, algumas vezes, isso seja um mal necessário, há técnicas melhores que funcionam em muitos casos.

Abstração Essa é a técnica mais limpa que uso para lidar com as diferenças das plataformas e envolve a abstração da funcionalidade específica da plataforma em uma interface ou em uma classe abstrata. Essa técnica permite fornecer implementações específicas à plataforma para interfaces e, ao mesmo tempo, uma interface consistente para uso em toda a base de código. Em casos onde há código auxiliar que cada implementação específica à plataforma pode utilizar, é possível implementar uma classe abstrata com esse código auxiliar comum e, em seguida, fornecer implementações específicas à plataforma. Essa técnica requer que a interface ou a classe abstrata esteja disponível nos dois projetos por meio da funcionalidade Adicionar como vínculo mencionada anteriormente.

Abstração mais diretivas de compilador A técnica final que pode ser usada é uma combinação das duas anteriores. É possível abstrair as diferenças das plataformas em uma interface ou classe abstrata e utilizar diretivas de compilador na implementação real. Isso é útil em casos onde as diferenças das plataformas são secundárias o suficiente e não valha a pena separá-las para cada tipo de projeto.

Na prática, descobri que raramente uso diretivas de compilador por si próprias, principalmente em meus modelos de exibição. Prefiro manter meus modelos de exibição limpos sempre que possível. Assim, quando as diretivas de compilador são a melhor solução, também insiro alguma abstração para manter o queijo suíço um pouco mais oculto.

Um dos primeiros desafios que encontrei em minhas viagens por multiplataforma foi a navegação. A navegação não é igual no Windows 8 e no Windows Phone 8, mas é próxima. O Windows 8 agora usa a navegação baseada em URI, o que o Windows Phone usa desde o começo. A diferença está na forma como os parâmetros são passados. O Windows 8 utiliza um único objeto como um parâmetro, enquanto o Windows Phone 8 utiliza tantos parâmetros quanto você desejar, mas através da cadeia de caracteres de consulta. Como o Windows Phone usa a cadeia de caracteres de consulta, todos os parâmetros devem ser serializados em uma cadeia de caracteres. Acontece que o Windows 8 não é tão diferente nesse aspecto.

Embora o Windows 8 utilize um único objeto como um parâmetro, esse objeto deve, de alguma maneira, ser serializado quando outro aplicativo ocupar o palco central e meu aplicativo for desativado. O sistema operacional utiliza a rota fácil e chama ToString naquele parâmetro, o que não é muito útil para mim quando meu aplicativo é ativado novamente. Como desejo reunir essas duas plataformas ao máximo possível em meu esforço de desenvolvimento, faz sentido serializar meu parâmetro como uma cadeia de caracteres antes da navegação e, em seguida, desserializá-lo quando a navegação for concluída. Posso até ajudar a facilitar esse processo com a implementação de meu navegador.

Observe que desejo evitar referenciar as exibições diretamente em meus modelos de exibição, portanto, desejo que a navegação seja controlada por modelos de exibição. Minha solução é utilizar uma convenção na qual as exibições sejam colocadas em um namespace/pasta Views e os modelos de exibição sejam colocados em um namespace/pasta ViewModels. Também garantirei que minhas exibições sejam denominadas {Something}Page e meus modelos de exibição sejam denominados {Something}ViewModel. Com essa convenção definida, posso fornecer alguma lógica simples para resolver uma instância de uma exibição, com base no tipo do modelo de exibição.

Agora, preciso decidir de qual outra funcionalidade preciso para a navegação:

  • navegação controlada por modelo de exibição
  • A capacidade de voltar
  • Para Windows Phone 8, a capacidade de remover uma entrada da pilha voltar

As suas primeiras são simples. Explicarei a necessidade de remover uma entrada da pilha voltar mais tarde, mas essa capacidade é interna no Windows Phone 8 e não no Windows 8.

O Windows 8 e o Windows Phone 8 utilizam classes para sua navegação que não são facilmente simuladas. Como uma das minhas principais metas para esses aplicativos é mantê-los testáveis, desejo abstrair esse código por trás de uma interface simulável. Portanto, meu navegador utilizará uma combinação de abstração e de diretivas de compilador. Isso me deixa com a seguinte interface:

public interface INavigator
{
  bool CanGoBack { get; }
  void GoBack();
  void NavigateToViewModel<TViewModel>(object parameter = null);
#if WINDOWS_PHONE
  void RemoveBackEntry();
#endif // WINDOWS_PHONE
}

Observe o uso de #if WINDOWS_PHONE. Isso informa ao compilador para compilar RemoveBackEntry na definição da interface apenas quando a diretiva de compilador WINDOWS_PHONE estiver definida, como será para projetos do Windows Phone 8. Aqui está a minha implementação, conforme mostrado na Figura 4.

Figura 4 Implementando INavigator

public sealed class Navigator : INavigator
{
  private readonly ISerializer serializer;
  private readonly IContainer container;
#if WINDOWS_PHONE
  private readonly Microsoft.Phone.Controls.PhoneApplicationFrame frame;
#endif // WINDOWS_PHONE
  public Navigator(
    ISerializer serializer,
    IContainer container
#if WINDOWS_PHONE
    , Microsoft.Phone.Controls.PhoneApplicationFrame frame
#endif // WINDOWS_PHONE
    )
  {
    this.serializer = serializer;
    this.container = container;
#if WINDOWS_PHONE
    this.frame = frame;
#endif // WINDOWS_PHONE
  }
  public void NavigateToViewModel<TViewModel>(object parameter = null)
  {
    var viewType = ResolveViewType<TViewModel>();
#if NETFX_CORE
    var frame = (Frame)Window.Current.Content;
#endif // NETFX_CORE
      if (parameter != null)
                             {
#if WINDOWS_PHONE
      this.frame.Navigate(ResolveViewUri(viewType, parameter));
#else
      frame.Navigate(viewType, this.serializer.Serialize(parameter));
#endif // WINDOWS_PHONE
    }
    else
    {
#if WINDOWS_PHONE
      this.frame.Navigate(ResolveViewUri(viewType));
#else
      frame.Navigate(viewType);
#endif // WINDOWS_PHONE
    }
  }
  public void GoBack()
  {
#if WINDOWS_PHONE
    this.frame.GoBack();
#else
    ((Frame)Window.Current.Content).GoBack();
#endif // WINDOWS_PHONE
  }
  public bool CanGoBack
  {
    get
    {
#if WINDOWS_PHONE
      return this.frame.CanGoBack;
#else
      return ((Frame)Window.Current.Content).CanGoBack;
#endif // WINDOWS_PHONE
    }
  }
  private static Type ResolveViewType<TViewModel>()
  {
    var viewModelType = typeof(TViewModel);
    var viewName = viewModelType.AssemblyQualifiedName.Replace(
      viewModelType.Name,
      viewModelType.Name.Replace("ViewModel", "Page"));
    return Type.GetType(viewName.Replace("Model", string.Empty));
  }
  private Uri ResolveViewUri(Type viewType, object parameter = null)
  {
    var queryString = string.Empty;
    if (parameter != null)
    {
      var serializedParameter = this.serializer.Serialize(parameter);
      queryString = string.Format("?parameter={0}", serializedParameter);
    }
    var match = System.Text.RegularExpressions.Regex.Match(
      viewType.FullName, @"\.Views.*");
    if (match == null || match.Captures.Count == 0)
    {
      throw new ArgumentException("Views must exist in Views namespace.");
    }
    var path = match.Captures[0].Value.Replace('.', '/');
    return new Uri(string.Format("{0}.xaml{1}", path, queryString),
      UriKind.Relative);
  }
#if WINDOWS_PHONE
  public void RemoveBackEntry()
  {
    this.frame.RemoveBackEntry();
  }
#endif // WINDOWS_PHONE
}

Preciso destacar algumas partes da implementação do Navegador na Figura 4, principalmente o uso das diretivas de compilador WINDOWS_PHONE e NETFX_CORE. Isso me permite manter o código específico à plataforma separado dentro do mesmo arquivo de código. Também quero destacar o método ResolveViewUri, principalmente a maneira como a cadeia de caracteres da pesquisa é definida. Para manter as coisas o mais consistente possível entre as duas plataformas, estou permitindo que apenas um parâmetro seja passado. Esse parâmetro será então serializado e passado para a navegação específica à plataforma. No caso do Windows Phone 8, esse parâmetro será passado por meio de uma variável "parameter" na cadeia de caracteres de pesquisa.

É claro que minha implementação de navegador é muito limitada, principalmente devido à expectativa de convenção excessivamente simples. Se você estiver usando uma biblioteca MVVM, como Caliburn.Micro, ela poderá tratar da navegação real para você de uma maneira mais robusta. No entanto, em sua própria navegação, você talvez ainda queira aplicar essa técnica de abstração mais diretivas de compilador para suavizar as diferenças de plataforma existentes nas próprias bibliotecas.

Configurações do aplicativo

As configurações do aplicativo são outra área onde o Windows 8 e o Windows Phone 8 divergem. Cada plataforma tem a capacidade de salvar as configurações do aplicativo de maneira razoavelmente fácil, e suas implementações também são muito semelhantes. A diferença está nas classes que elas usam, mas as duas usam classes que não são facilmente simuláveis, o que inviabilizaria a capacidade de teste de meu modelo de exibição. Portanto, mais uma vez, optarei por abstração mais diretivas de compilador. Primeiro preciso decidir qual deve ser a aparência da minha interface. A interface deve:

  • adicionar ou atualizar uma configuração
  • Tentar obter uma configuração, sem gerar uma exceção em caso de falha
  • Remover uma configuração
  • Determinar se existe uma configuração para uma determinada chave

Coisas bastante simples, portanto, minha interface será bastante simples:

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

Como as duas plataformas terão a mesma funcionalidade, não preciso me preocupar com diretivas de compilador na interface, mantendo as coisas certas e limpas para meus modelos de exibição. A Figura 5 mostra a minha implementação da interface ISettings.

Figura 5 Implementando ISettings

public sealed class Settings : ISettings
{
  public void AddOrUpdate(string key, object value)
  {
#if WINDOWS_PHONE
    IsolatedStorageSettings.ApplicationSettings[key] = value;
    IsolatedStorageSettings.ApplicationSettings.Save();
#else
    ApplicationData.Current.RoamingSettings.Values[key] = value;
#endif // WINDOWS_PHONE
  }
  public bool TryGetValue<T>(string key, out T value)
  {
#if WINDOWS_PHONE
    return IsolatedStorageSettings.ApplicationSettings.TryGetValue<T>(
      key, out value);
#else
    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;
#endif // WINDOWS_PHONE
  }
  public bool Remove(string key)
  {
#if WINDOWS_PHONE
    var result = IsolatedStorageSettings.ApplicationSettings.Remove(key);
    IsolatedStorageSettings.ApplicationSettings.Save();
    return result;
#else
    return ApplicationData.Current.RoamingSettings.Values.Remove(key);
#endif // WINDOWS_PHONE
  }
  public bool ContainsKey(string key)
  {
#if WINDOWS_PHONE
    return IsolatedStorageSettings.ApplicationSettings.Contains(key);
#else
    return ApplicationData.Current.RoamingSettings.Values.ContainsKey(key);
#endif // WINDOWS_PHONE
  }
}

A implementação mostrada na Figura 5 é muito simples, da mesma forma como a própria interface ISettings. Ela ilustra como cada plataforma é apenas levemente diferente e exige apenas código levemente diferente para adicionar, recuperar e remover configurações do aplicativo. A única coisa que destacarei é que a versão do Windows 8 do código utiliza configurações de roaming, funcionalidade específica ao Windows 8 que permite que um aplicativo armazene configurações na nuvem e que outros usuários abram o aplicativo em outro dispositivo Windows 8 e vejam as mesmas configurações aplicadas.

Blocos secundários

Como disse anteriormente, o Windows 8 e o Windows Phone 8 dão suporte à criação de blocos secundários, blocos que são criados programaticamente e fixados na tela inicial do usuário. Os blocos secundários fornecem funcionalidade de vinculação profunda, o que significa que o usuário pode tocar em um bloco secundário e pular diretamente para uma parte específica de um aplicativo. Isso é valioso para os usuários, porque podem essencialmente marcar partes de seu aplicativo e saltar diretamente para elas sem perder tempo. No caso de meus aplicativos de exemplo, quero que o usuário possa marcar uma única postagem de blog (FeedItem) e saltar diretamente para ele na tela inicial.

O que é interessante no caso de blocos secundários é que, embora as duas plataformas deem suporte a eles, a maneira como devo implementá-los em cada plataforma é diferente. Esse é um caso perfeito para explorar um exemplo de abstração mais complexo.

Se você leu meu artigo de julho de 2013, recordará que falei sobre como abstrair blocos secundários para desenvolvimento de MVVM no Windows 8. A solução que apresentei funciona perfeitamente bem para o Windows 8, mas não é nem compilada no Windows Phone 8. O que apresentarei aqui é a evolução natural daquela solução do Windows 8, que funciona bem no Windows 8 e no Windows Phone 8.

Esta é a aparência que a interface tinha no Windows 8:

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

Como mencionei, essa interface não é compilada no Windows Phone 8. O uso da enumeração FrameworkElement e Placement é particularmente problemático. Nada disso é o mesmo no Windows Phone 8 como é no Windows 8. Minha meta é modificar um pouco essa interface para que possa ser usada pelas duas plataformas sem problemas. Como você pode ver, o método ISecondaryPinner.Pin utiliza um objeto TileInfo como um parâmetro. TileInfo é um DTO (objeto de transferência de dados) que criei contendo informações relevantes necessárias para criar um bloco secundário. A coisa mais fácil para eu fazer é mover os parâmetros que a versão do Windows 8 precisa para a classe TileInfo e, em seguida, usar diretivas de compilador para compilá-los na versão do Windows 8 da classe TileInfo. Isso altera minha interface ISecondaryPinner para o seguinte:

public interface ISecondaryPinner
{
  Task<bool> Pin(TileInfo tileInfo);
  Task<bool> Unpin(TileInfo tileInfo);
  bool IsPinned(string tileId);
}

Você pode ver que os métodos ainda são os mesmos, mas os parâmetros de Pin e Unpin foram levemente alterados. Como resultado, a classe TileInfo também foi alterada do que era anteriormente e agora é parecida com isto:

public sealed class TileInfo
{
  public string TileId { get; set; }
  public string ShortName { get; set; }
  public string DisplayName { get; set; }
  public string Arguments { get; set; }
  public Uri LogoUri { get; set; }
  public Uri WideLogoUri { get; set; }
  public string AppName { get; set; }
  public int? Count { get; set; }
#if NETFX_CORE
  public Windows.UI.StartScreen.TileOptions TileOptions { get; set; }
  public Windows.UI.Xaml.FrameworkElement AnchorElement { get; set; }
  public Placement RequestPlacement { get; set; }
#endif // NETFX_CORE
}

Na realidade, prefiro fornecer construtores para cada cenário no qual DTOs auxiliares como esses são usados para ser mais explícito sobre quais parâmetros são necessários e quando. Para resumir, deixei os construtores diferentes de fora do trecho de código de TileInfo, mas você pode vê-los em toda a sua glória no código de exemplo.

O TileInfo agora tem todas as propriedades necessárias para o Windows 8 e o Windows Phone 8, portanto, a próxima coisa a fazer é implementar a interface ISecondaryPinner. Como a implementação será completamente diferente para cada plataforma, usarei a mesma interface nos dois tipos de projeto, mas fornecerei implementações específicas à plataforma em cada projeto. Isso reduzirá o efeito de queijo suíço que as diretivas de compilador causariam nesse caso. A Figura 6 mostra a implementação de ISecondaryPinner no Windows 8, agora com as assinaturas do método atualizadas.

Figura 6 Implementação de ISecondaryPinner no Windows 8

public sealed class Win8SecondaryPinner : ISecondaryPinner
{
  public async Task<bool> Pin(TileInfo tileInfo)
  {
    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(tileInfo.AnchorElement), tileInfo.RequestPlacement);
    }
    return isPinned;
  }
  public async Task<bool> Unpin(TileInfo tileInfo)
  {
    var wasUnpinned = false;
    if (SecondaryTile.Exists(tileInfo.TileId))
    {
      var secondaryTile = new SecondaryTile(tileInfo.TileId);
      wasUnpinned = await secondaryTile.RequestDeleteForSelectionAsync(
        GetElementRect(tileInfo.AnchorElement), tileInfo.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));
  }
}

É importante observar que no Windows 8 você não pode criar silenciosamente um bloco secundário programaticamente sem a aprovação do usuário. Isso é diferente do Windows Phone 8, que permite fazer isso. Portanto, devo criar uma instância de SecondaryTile e chamar RequestCreateForSelectionAsync, que exibirá uma caixa de diálogo pop-up na posição que forneço, solicitando que o usuário aprove a criação (ou a exclusão) do bloco secundário. O método auxiliar GetElementRect utiliza um FrameworkElement, que será o botão que o usuário pressionará para fixar o bloco secundário, e, em seguida, calcula seu retângulo a ser usado para posicionar a caixa de dialogo de solicitação.

A Figura 7 mostra a implementação de ISecondaryPinner no Windows Phone 8.

Figura 7 Implementação de ISecondaryPinner para Windows Phone 8

public sealed class WP8SecondaryPinner : ISecondaryPinner
{
  public Task<bool> Pin(TileInfo tileInfo)
  {
    var result = false;
    if (!this.IsPinned(tileInfo.TileId))
    {
      var tileData = new StandardTileData
      {
        Title = tileInfo.DisplayName,
        BackgroundImage = tileInfo.LogoUri,
        Count = tileInfo.Count,
        BackTitle = tileInfo.AppName,
        BackBackgroundImage = new Uri("", UriKind.Relative),
        BackContent = tileInfo.DisplayName
      };
      ShellTile.Create(new Uri(tileInfo.TileId, UriKind.Relative), 
        tileData);
      result = true;
    }
  return Task.FromResult<bool>(result);
  }
  public Task<bool> Unpin(TileInfo tileInfo)
  {
    ShellTile tile = this.FindTile(tileInfo.TileId);
    if (tile != null)
    {
      tile.Delete();
    }
    return Task.FromResult<bool>(true);
  }
  public bool IsPinned(string tileId)
  {
    return FindTile(tileId) != null;
  }
  private ShellTile FindTile(string uri)
  {
    return ShellTile.ActiveTiles.FirstOrDefault(
      tile => tile.NavigationUri.ToString() == uri);
  }
}

Há duas coisas que quero destacar na implementação do Windows Phone 8. A primeira é como o bloco secundário é criado usando a classe StandardTileData e o método estático ShellTile.Create. A segunda é que a implementação do Windows Phone 8 para criação de blocos secundários não é assíncrona. Como a implementação do Windows 8 é assíncrona, precisei fazer com que a interface desse suporte ao padrão async/await. Felizmente, é muito fácil fazer o que de outra forma seria um método não assíncrono dar suporte ao padrão async/await utilizando o método estático genérico Task.FromResult. Os modelos de exibição que usam a interface ISecondaryPinner não precisam se preocupar com o fato de que o Windows 8 é inerentemente assíncrono e o Windows Phone 8 não é.

Agora você pode ver como o Windows 8 (Figura 6) e o Windows Phone 8 (Figura 7) variam em sua implementação de blocos secundários. No entanto, esse não é o fim da história dos blocos secundários. Mostrei apenas a implementação da interface ISecondaryPinner. Como cada plataforma é diferente e deve fornecer valores para diferentes propriedades da classe TileInfo, também devo fornecer implementações específicas à plataforma dos modelos de exibição que as usam. Em meu aplicativo de exemplo, fornecerei a capacidade de fixar uma única postagem de blog ou FeedItem, para que o modelo de exibição em questão seja o FeedItemViewModel.

Existe alguma funcionalidade comum no Windows 8 e no Windows Phone 8, da perspectiva do modelo de exibição. Quando o usuário fixar um FeedItem, quero que as duas plataformas salvem esse FeedItem localmente para que possa ser recarregado quando o usuário tocar em seu bloco secundário. Por outro lado, quando o usuário desafixar um FeedItem, quero que as duas plataformas excluam esse FeedItem do armazenamento local. As duas plataformas precisarão implementar essa funcionalidade comum e ainda expandi-la com implementações específicas à plataforma para a funcionalidade de bloco secundário. Portanto, faz sentido fornecer uma classe base que implemente a funcionalidade comum e torne essa classe base disponível nas duas plataformas. Em seguida, cada plataforma pode herdar essa classe base com classes específicas à plataforma que fornecem implementações específicas às plataformas para fixar e desafixar blocos secundários.

A Figura 8 mostra o FeedItemViewModel, que é a classe base a ser herdada pelas duas plataformas. O FeedItemViewModel contém tudo o que é comum às duas plataformas.

Figura 8 Classe base FeedItemViewModel

public abstract class FeedItemViewModel : ViewModelBase<FeedItem>
{
  private readonly IStorage storage;
  protected readonly ISecondaryPinner secondaryPinner;
  public FeedItemViewModel(
    ISerializer serializer,   
    IStorage storage,
    ISecondaryPinner secondaryPinner)
    : base(serializer)
  {
    this.storage = storage;
    this.secondaryPinner = secondaryPinner;
  }
  public override void LoadState(FeedItem navigationParameter,
    Dictionary<string, object> pageState)
  {
    this.FeedItem = navigationParameter;
  }
  protected async Task SavePinnedFeedItem()
  {
    var pinnedFeedItems =
      await this.storage.LoadAsync<List<FeedItem>>(
      Constants.PinnedFeedItemsKey);
    if (pinnedFeedItems == null)
    {
      pinnedFeedItems = new List<FeedItem>();
    }
    pinnedFeedItems.Add(feedItem);
    await this.storage.SaveAsync(Constants.PinnedFeedItemsKey, 
      pinnedFeedItems);
  }
  protected async Task RemovePinnedFeedItem()
  {
    var pinnedFeedItems =
      await this.storage.LoadAsync<List<FeedItem>>(
      Constants.PinnedFeedItemsKey);
    if (pinnedFeedItems != null)
    {
       var pinnedFeedItem = pinnedFeedItems.FirstOrDefault(fi => fi.Id ==
         this.FeedItem.Id);
       if (pinnedFeedItem != null)
       {
         pinnedFeedItems.Remove(pinnedFeedItem);
       }
      await this.storage.SaveAsync(Constants.PinnedFeedItemsKey, 
        pinnedFeedItems);
    }
  }
  private FeedItem feedItem;
  public FeedItem FeedItem
  {
    get { return this.feedItem; }
    set { this.SetProperty(ref this.feedItem, value); }
  }
  private bool isFeedItemPinned;
  public bool IsFeedItemPinned
  {
    get { return this.isFeedItemPinned; }
    set { this.SetProperty(ref this.isFeedItemPinned, value); }
  }
}

Com uma classe base definida para facilitar o salvamento e a exclusão de FeedItems fixados, posso continuar com as implementações específicas às plataformas. A implementação concreta de FeedItemViewModel no Windows 8 e o uso da classe TileInfo com as propriedades que são importantes para o Windows 8 são mostrados na Figura 9.

Figura 9 Implementação concreta de FeedItemViewModel no Windows 8

public sealed class Win8FeedItemViewModel : FeedItemViewModel
{
  private readonly IShareManager shareManager;
  public Win8FeedItemViewModel(
    ISerializer serializer,
    IStorage storage,
    ISecondaryPinner secondaryPinner,
    IShareManager shareManager)
    : base(serializer, storage, secondaryPinner)
  {
  this.shareManager = shareManager;
  }
  public override void LoadState(Models.FeedItem navigationParameter,
    Dictionary<string, object> pageState)
  {
    base.LoadState(navigationParameter, pageState);
    this.IsFeedItemPinned =
      this.secondaryPinner.IsPinned(FormatSecondaryTileId());
  }
  public override void SaveState(Dictionary<string, object> pageState)
  {
    base.SaveState(pageState);
    this.shareManager.Cleanup();
  }
  public async Task Pin(Windows.UI.Xaml.FrameworkElement anchorElement)
  {
    // Pin the feed item, then save it locally to make sure it is still
    // available when they return.
    var tileInfo = new TileInfo(
      this.FormatSecondaryTileId(),
      this.FeedItem.Title,
      this.FeedItem.Title,
      Windows.UI.StartScreen.TileOptions.ShowNameOnLogo |
        Windows.UI.StartScreen.TileOptions.ShowNameOnWideLogo,
      new Uri("ms-appx:///Assets/Logo.png"),
      new Uri("ms-appx:///Assets/WideLogo.png"),
      anchorElement,
      Windows.UI.Popups.Placement.Above,
      this.FeedItem.Id.ToString());
    this.IsFeedItemPinned = await this.secondaryPinner.Pin(tileInfo);
    if (this.IsFeedItemPinned)
    {
       await SavePinnedFeedItem();
    }
  }
  public async Task Unpin(Windows.UI.Xaml.FrameworkElement anchorElement)
  {
    // Unpin, then delete the feed item locally.
  var tileInfo = new TileInfo(this.FormatSecondaryTileId(), anchorElement,
    Windows.UI.Popups.Placement.Above);
    this.IsFeedItemPinned = !await this.secondaryPinner.Unpin(tileInfo);
    if (!this.IsFeedItemPinned)
    {
      await RemovePinnedFeedItem();
    }
  }
  private string FormatSecondaryTileId()
  {
    return string.Format(Constants.SecondaryIdFormat, this.FeedItem.Id);
  }
}

A Figura 10 mostra a implementação concreta de FeedItemViewModel no Windows Phone 8. O uso de TileInfo no Windows Phone 8 exige menos propriedades do que no Windows 8.

Figura 10 Implementação concreta de FeedItemViewModel no Windows Phone 8

public sealed class WP8FeedItemViewModel : FeedItemViewModel
{
  public WP8FeedItemViewModel(
    ISerializer serializer,
    IStorage storage,
    ISecondaryPinner secondaryPinner)
    : base(serializer, storage, secondaryPinner)
  {
  }
  public async Task Pin()
  {
    // Pin the feed item, then save it locally to make sure it is still
    // available when they return.
    var tileInfo = new TileInfo(
      this.FormatTileIdUrl(),
      this.FeedItem.Title,
      Constants.AppName,
      new Uri("/Assets/ApplicationIcon.png", UriKind.Relative));
    this.IsFeedItemPinned = await this.secondaryPinner.Pin(tileInfo);
    if (this.IsFeedItemPinned)
    {
      await this.SavePinnedFeedItem();
    }
  }
  public async Task Unpin()
  {
    // Unpin, then delete the feed item locally.
    var tileInfo = new TileInfo(this.FormatTileIdUrl());
    this.IsFeedItemPinned = !await this.secondaryPinner.Unpin(tileInfo);
    if (!this.IsFeedItemPinned)
    {
      await this.RemovePinnedFeedItem();
    }
  }
  private string FormatTileIdUrl()
  {
    var queryString = string.Format("parameter={0}", FeedItem.Id);
    return string.Format(Constants.SecondaryUriFormat, queryString);
  }
}

Depois das implementação de FeedItemViewModel no Windows 8 (Figura 9) e no Windows Phone 8 (Figura 10), meus aplicativos estão todos prontos para fixar blocos secundários em suas respectivas telas iniciais. A única coisa que falta para fechar o loop da funcionalidade é identificar quando o usuário realmente toca nos blocos secundários fixados. Meu objetivo nos dois aplicativos é iniciar o aplicativo diretamente em qualquer postagem de blog que o bloco secundário represente, mas permitir que o usuário pressione o botão voltar para a página principal que lista todos os blogs e não simplesmente sair do próprio aplicativo.

Da perspectiva do Windows 8, nada muda em relação ao que falei no artigo de julho de 2013. A Figura 11 mostra o código do Windows 8 inalterado para identificar quando o aplicativo é iniciado, que é um trecho do arquivo da classe App.xaml.cs.

Figura 11 Iniciando o aplicativo no Windows 8

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

No Windows Phone 8, isso é um pouco mais trabalhoso. Nesse caso, o bloco secundário utiliza um uri, em vez de apenas parâmetros. Isso significa que o Windows Phone 8 pode iniciar automaticamente meu aplicativo em qualquer página que eu desejar, sem precisar ir para um ponto de início centralizado primeiro, como faz o Windows 8. Como desejo fornecer uma experiência consistente entre as duas plataformas, criei um ponto de início centralizado próprio no Windows Phone 8, que chamei de SplashViewModel, mostrado na Figura 12. Configurei meu projeto para ser iniciado nele sempre que o aplicativo for iniciado, por meio de um bloco secundário ou não.

Figura 12 SplashViewModel para Windows Phone 8

public sealed class SplashViewModel : ViewModelBase<int?>
{
  private readonly IStorage storage;
  private readonly INavigator navigator;
  public SplashViewModel(
    IStorage storage,
    INavigator navigator,
    ISerializer serializer)
    : base(serializer)
  {
    this.storage = storage;
    this.navigator = navigator;
  }
  public override async void LoadState(
    int? navigationParameter, Dictionary<string, object> pageState)
  {
    this.navigator.NavigateToViewModel<MainViewModel>();
    this.navigator.RemoveBackEntry();
    if (navigationParameter.HasValue)
    {
      List<FeedItem> pinnedFeedItems =
        await storage.LoadAsync<List<FeedItem>>(Constants.PinnedFeedItemsKey);
      if (pinnedFeedItems != null)
      {
        var pinnedFeedItem =
          pinnedFeedItems.FirstOrDefault(fi => 
            fi.Id == navigationParameter.Value);
        if (pinnedFeedItem != null)
        {
  this.navigator.NavigateToViewModel<FeedItemViewModel>(pinnedFeedItem);
        }
      }
    }
  }
}

O código de SplashViewModel é bastante simples. A única coisa que destacarei é que desejo remover essa página da pilha voltar, caso contrário ela impediria que os usuários pudessem sair do aplicativo. Eles seriam enviados de volta para o aplicativo constantemente toda vez que voltassem para essa página. É aqui que a adição valiosa ao INavigator para Windows Phone 8 entra em cena: RemoveBackEntry. Depois de navegar para o MainViewModel, chamo RemoveBackEntry para remover a página inicial da pilha voltar. Agora ela é uma página de um único uso apenas para quando o aplicativo for iniciado.

Conclusão

Neste artigo, discuti o desenvolvimento multiplataforma no Windows 8 e no Windows Phone 8. Falei sobre o que pode ser reutilizado entre as plataformas (designs e algum código) e o que não pode (XAML). Também falei sobre alguns dos desafios enfrentados pelos desenvolvedores que trabalham com aplicativos multiplataforma e apresentei várias soluções para esses desafios. Espero que essas soluções possam ser aplicadas a mais casos do que apenas aos de navegação, configurações de aplicativos e blocos secundários sobre os quais falei. Essas soluções podem ajudar a manter seus modelos de exibição testáveis fornecendo a habilidade de abstrair algumas das interações do sistema operacional e de usar interfaces para tornar essas interações simuláveis.

Em meu próximo artigo, examinarei mais especificamente o teste de unidade real desses aplicativos multiplataforma, agora que estão todos configurados para teste. Falarei mais sobre por que tomei algumas das decisões que tomei, uma vez que estão relacionadas a testes, bem como sobre como eu realmente faço o teste de unidade dos aplicativos.

Com a abordagem do desenvolvimento multiplataforma para fornecer UXs semelhantes nas duas plataformas e com um pouco de planejamento prévio, posso escrever aplicativos que maximizem a reutilização do código e promovam o teste de unidade. Posso utilizar recursos específicos à plataforma, como o menu Botões do Windows 8, sem sacrificar a experiência que cada plataforma oferece.

Brent Edwards é consultor associado principal da Magenic, uma empresa de desenvolvimento de 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: Jason Bock (Magenic)