Padrões de design

Problemas e soluções do padrão MVVM (Model-View-ViewModel)

Robert McCarter

Baixar o código de exemplo

O Windows Presentation Foundation (WPF) e o Silverlight fornecem APIs avançadas para criar aplicativos modernos, mas entender e empregar todos os recursos do WPF harmoniosamente para criar aplicativos bem projetados e de fácil manutenção pode ser uma tarefa difícil. Por onde você começa? E qual é a maneira certa de criar o aplicativo?

O padrão de design MVVM (Model-View-ViewModel) descreve uma abordagem popular de criação de aplicativos do WPF e do Silverlight. Ele é, ao mesmo tempo, uma ferramenta avançada para criar aplicativos e uma linguagem comum para discutir o design de aplicativos com desenvolvedores. Embora o MVVM seja de fato um padrão útil, ele ainda é relativamente novo e malcompreendido.

Quando o padrão de design MVVM é aplicável e quando ele é desnecessário? Como o aplicativo deve ser estruturado? Quão trabalhoso é gravar e manter a camada ViewModel, e que alternativas existem para reduzir o volume de código nela? Como lidar com as propriedades relacionadas em Model de uma forma elegante? Como expor coleções em Model para View? Onde os objetos ViewModel devem ser instanciados e conectados a objetos Model?

Neste artigo, vou explicar como o ViewModel funciona e discutir alguns dos benefícios e problemas relacionados à implementação de um ViewModel no código. Também mostrarei alguns exemplos concretos de uso do ViewModel como gerenciador de documentos para expor objetos Model na camada View.

Model, ViewModel e View

Todos os aplicativos do WPF e do Silverlight em que trabalhei até agora tinham o mesmo design de componente de alto nível. O Model era o núcleo do aplicativo, e foi necessário muito esforço para projetá-lo segundo as práticas recomendadas de OOAD (análise e design orientados a objeto).

Para mim, o Model é a essência do aplicativo, representando o maior e mais importante ativo comercial, pois ele captura todas as entidades de negócios complexas, suas relações e funcionalidade.

Acima de Model está ViewModel. Os dois principais objetivos de ViewModel são tornar Model facilmente consumível pelo modo de exibição WPF/XAML e separar e encapsular Model de View. São excelentes objetivos, embora, por motivos pragmáticos, às vezes não são atingidos.

Crie ViewModel sabendo como o usuário irá interagir com o aplicativo em um alto nível. No entanto, uma parte importante do padrão de design MVVM é que ViewModel não sabe nada sobre View. Isso permite que designers de interação e artistas gráficos criem interfaces de usuário lindas e funcionais com base em ViewModel e, ao mesmo tempo, trabalhem de perto com os desenvolvedores para projetar um ViewModel apropriado para seus esforços. Alem disso, a dissociação entre View e ViewModel permite aumentar a capacidade de teste de unidade e de reutilização de ViewModel.

Para ajudar a forçar uma separação estrita entre as camadas Model, View e ViewModel, gosto de criar cada uma como um projeto separado do Visual Studio. Combinado a utilitários reutilizáveis, o principal assembly executável e qualquer projeto de teste de unidade (você tem muitos deles, não?), isso pode resultar em vários projetos e assemblies, conforme ilustrado na Figura 1.

Figure 1 The Components of an MVVM Application

Figura 1 Os componentes de um aplicativo MVVM

Dado o grande número de projetos, essa abordagem de separação estrita obviamente é mais útil em projetos de grande porte. No caso de aplicativos pequenos com apenas um ou dois desenvolvedores, as vantagens da separação estrita podem não superar a inconveniência de criar, configurar e manter vários projetos, então apenas separar o código em diferentes namespaces dentro do mesmo projeto pode proporcionar um isolamento mais do que suficiente.

Criar e manter um ViewModel não são tarefas triviais e nem deve ser considerado algo simples. No entanto, a resposta para as perguntas mais básicas (quando você deve considerar o padrão de design MVVM e quando ele é desnecessário) geralmente é encontrada no seu modelo de domínio.

Em projetos grandes, o modelo de domínio pode ser muito complexo, com centenas de classes cuidadosamente projetadas para funcionar em harmonia com qualquer tipo de aplicativo, inclusive serviços Web, aplicativos do WPF ou do ASP.NET. A camada Model pode conter vários assemblies que funcionam em conjunto e, em organizações muito grandes, às vezes o modelo de domínio é criado e mantido por uma equipe de desenvolvimento especializada.

Quando se tem um modelo de domínio grande e complexo, é quase sempre vantajoso introduzir uma camada ViewModel.

Por outro lado, às vezes o modelo de domínio é simples, talvez apenas uma camada fina sobre o banco de dados. As classes podem ser geradas automaticamente e com frequência elas implementam INotifyPropertyChanged. A interface do usuário normalmente é uma coleção de listas ou grades com formulários de edição que permitem ao usuário manipular os dados subjacentes. O conjunto de ferramentas da Microsoft sempre foi muito bom para criar esses tipos de aplicativos facilmente e com rapidez.

Se o seu modelo ou aplicativo se enquadra nessa categoria, um ViewModel provavelmente imporia uma sobrecarga inaceitavelmente alta sem vantagens suficientes para o design do aplicativo.

Isso posto, mesmo nesses casos, ViewModel ainda pode agregar valor. Por exemplo, ViewModel é um excelente lugar para implementar a funcionalidade de desfazer. De modo alternativo, você pode optar por usar o MVVM em uma parte do aplicativo (como no gerenciamento de documentos, conforme abordarei mais adiante) e expor Model de forma pragmática diretamente para View.

Por que usar ViewModel?

Se um ViewModel parece apropriado para seu aplicativo, ainda existem perguntas a serem respondidas antes de você começar a codificar. Uma das primeiras delas é como diminuir o número de propriedades de proxy.

A separação de View de Model promovida pelo padrão de design MVVM é um aspecto importante e valioso do padrão. Consequentemente, se uma classe Model tem 10 propriedades que precisam ser expostas em View, o ViewModel normalmente acaba tendo 10 propriedades idênticas que simplesmente encaminham a chamada para a instância de modelo subjacente. No geral, essas propriedades de proxy geram um evento de alteração de propriedade quando definidas para indicar para View que a propriedade foi alterada.

Nem toda propriedade de Model precisa ter uma propriedade de proxy ViewModel, mas toda propriedade de Model que precisa ser exposta em View normalmente terá uma. As propriedades de proxy geralmente são assim:

public string Description {
  get { 
    return this.UnderlyingModelInstance.Description; 
  }
  set {
    this.UnderlyingModelInstance.Description = value;
    this.RaisePropertyChangedEvent("Description");
  }
}

Qualquer aplicativo não trivial terá dezenas ou centenas de classes Model que precisam ser expostas para o usuário dessa maneira através de ViewModel. Isso é intrínseco à separação feita por MVVM.

Escrever essas propriedades de proxy é uma tarefa entediante e, portanto, suscetível a erros, principalmente porque, para gerar o evento de alteração de propriedade, é necessária uma cadeia de caracteres que deve coincidir com o nome da propriedade (e não será incluída em nenhuma refatoração automática de código). Para eliminar esses eventos de proxy, a solução comum é expor a instância do modelo a partir do wrapper ViewModel diretamente e fazer com que o modelo de domínio implemente a interface INotifyPropertyChanged:

public class SomeViewModel {
  public SomeViewModel( DomainObject domainObject ) {
    Contract.Requires(domainObject!=null, 
      "The domain object to wrap must not be null");
    this.WrappedDomainObject = domainObject;
  }
  public DomainObject WrappedDomainObject { 
    get; private set; 
  }
...

Portanto, ViewModel ainda pode expor os comandos e propriedades adicionais necessários pela exibição sem duplicar propriedades de Model ou criar muitas propriedades de proxy. Essa abordagem certamente é interessante, principalmente se as classes Model já implementam a interface INotifyPropertyChanged. O fato de essa interface ser implementada pelo modelo não é necessariamente algo ruim e era até comum em aplicativos do Microsoft .NET Framework 2.0 w do Windows Forms. No entanto, isso atravanca o modelo de domínio e não seria útil para aplicativos do ASP.NET ou para serviços de domínio.

Com essa abordagem, View tem dependência de Model, mas é apenas uma dependência indireta através da vinculação de dados, que não requer uma referência de projeto do projeto View para o projeto Model. Então, por motivos puramente pragmáticos, às vezes essa abordagem é util.

Entretanto, ela viola o espírito do padrão de design MVVM e diminui a sua capacidade de introduzir posteriormente novas funcionalidades específicas de ViewModel (como recursos de desfazer, por exemplo). Encontrei cenários com essa abordagem que geraram um retrabalho razoável. Imagine a situação não incomum em que existe uma vinculação de dados em uma propriedade profundamente aninhada. Se ViewModel de Person é o contexto de dados atual e Person tem Address, a vinculação de dados poderá ser parecida com o seguinte:

{Binding WrappedDomainObject.Address.Country}

Se você precisar introduzir funcionalidade ViewModel adicional no objeto Address, terá de remover as referências de vinculação de dados a WrappedDomainObject.Address e, no lugar delas, usar as novas propriedades de ViewModel. Isso é complicado porque é difícil testar as atualizações feitas na vinculação de dados XAML (e possivelmente também nos contextos de dados). View é o único componente que não tem testes de regressão automatizados e abrangentes.

Propriedades dinâmicas

Minha solução para a proliferação de propriedades de proxy é usar o novo suporte do .NET Framework 4 e do WPF para objetos dinâmicos e distribuição de métodos dinâmica. Esta última permite determinar, em tempo de execução, como lidar com a leitura ou a gravação em uma propriedade que na verdade não existe na classe. Isso significa que é possível eliminar todas as propriedades de proxy manuscritas em ViewModel e, ao mesmo tempo, continuar encapsulando o modelo subjacente. No entanto, observe que o Silverlight 4 não dá suporte à vinculação a propriedades dinâmicas.

A maneira mais simples de implementar este recurso é fazer com que a classe base ViewModel estenda a nova classe System.Dynamic.DynamicObject e substitua os métodos TryGetMember e TrySetMember. O DLR (Dynamic Language Runtime) chama esses dois métodos quando a propriedade que está sendo referenciada não existe na classe, permitindo que a classe determine, em tempo de execução, como implementar as propriedades ausentes. Combinada a um pouco de reflexão, a classe ViewModel pode encaminhar o acesso de propriedade dinamicamente para a instância de modelo subjacente em apenas algumas linhas de código:

public override bool TryGetMember(
  GetMemberBinder binder, out object result) {

  string propertyName = binder.Name;
  PropertyInfo property = 
    this.WrappedDomainObject.GetType().GetProperty(propertyName);

  if( property==null || property.CanRead==false ) {
    result = null;
    return false;
  }

  result = property.GetValue(this.WrappedDomainObject, null);
  return true;
}

O método começa usando reflexão para localizar a propriedade na instância subjacente de Model. (Para obter mais detalhes, consulte a coluna “Tudo sobre o CRL” de junho de 2007 “Reflexões sobre a reflexão”.) Se o modelo não tem essa propriedade, o método falha retornando false, e a vinculação de dados também falha. Se a propriedade existe, o método usa as informações dela para recuperar e retornar o valor da propriedade de Model. Isso é mais trabalhoso do que o método get da propriedade de proxy tradicional, mas é a única implementação de que você precisa para gravar para todos os modelos e todas as propriedades.

A verdadeira força da abordagem de propriedade de proxy dinâmica está nos setters de propriedades. Em TrySetMember, é possível incluir uma lógica comum, como gerar eventos de alteração de propriedade. O código é parecido com este:

public override bool TrySetMember(
  SetMemberBinder binder, object value) {

  string propertyName = binder.Name;
  PropertyInfo property = 
    this.WrappedDomainObject.GetType().GetProperty(propertyName);

  if( property==null || property.CanWrite==false )
    return false;

  property.SetValue(this.WrappedDomainObject, value, null);

  this.RaisePropertyChanged(propertyName);
  return true;
}

Novamente, o método começa usando reflexão para extrair a propriedade da instância subjacente de Model. Se a propriedade não existe ou se é somente leitura, o método falha retornando false. Se a propriedade existe no objeto de domínio, suas informações são usadas para definir a propriedade de Model. Assim, você pode incluir qualquer lógica comum a todos os setters de propriedades. Neste código de exemplo, eu apenas gero o evento de alteração de propriedade para a propriedade que acabei de definir, mas você pode fazer mais facilmente.

Um dos desafios de encapsular um Model é que, com frequência, Model tem o que a UML (Unified Modeling Language) chama de propriedades derivadas. Por exemplo, uma classe Person provavelmente tem uma propriedade BirthDate e uma propriedade Age derivada. A propriedade Age é somente leitura e calcula a idade automaticamente com base na data de nascimento e na data atual:

public class Person : DomainObject {
  public DateTime BirthDate { 
    get; set; 
  }

  public int Age {
    get {
      var today = DateTime.Now;
      // Simplified demo code!
      int age = today.Year - this.BirthDate.Year;
      return age;
    }
  }
...

Quando a propriedade BirthDate é alterada, a propriedade Age também é alterada de forma implícita, pois a idade é derivada matematicamente da data de nascimento. Então, quando a propriedade BirthDate é definida, a classe ViewModel precisa gerar um evento de alteração de propriedade para as propriedades BirthDate e Age. Com a abordagem de ViewModel dinâmica, você pode fazer isso automaticamente tornando esta relação interpropriedades explícita dentro do modelo.

Primeiro, é necessário um atributo personalizado para capturar a relação de propriedades:

[AttributeUsage(AttributeTargets.Property, AllowMultiple=true)]
public sealed class AffectsOtherPropertyAttribute : Attribute {
  public AffectsOtherPropertyAttribute(
    string otherPropertyName) {
    this.AffectsProperty = otherPropertyName;
  }

  public string AffectsProperty { 
    get; 
    private set; 
  }
}

Defino AllowMultiple como true para dar suporte a cenários onde uma propriedade pode afetar várias outras propriedades. A aplicação desse atributo para codificar a relação entre BirthDate e Age diretamente no modelo é simples:

[AffectsOtherProperty("Age")]
public DateTime BirthDate { get; set; }

Para usar este novo metadado de modelo na classe dinâmica ViewModel, agora posso atualizar o método TrySetMember com três linhas de código adicionais, e ele ficará parecido com o seguinte:

public override bool TrySetMember(
  SetMemberBinder binder, object value) {
...
  var affectsProps = property.GetCustomAttributes(
    typeof(AffectsOtherPropertyAttribute), true);
  foreach(AffectsOtherPropertyAttribute otherPropertyAttr 
    in affectsProps)
    this.RaisePropertyChanged(
      otherPropertyAttr.AffectsProperty);
}

Com as informações de propriedades refletidas já em mãos, o método GetCustomAttributes pode retornar quaisquer atributos AffectsOtherProperty na propriedade do modelo. Em seguida, o código simplesmente executa um loop sobre os atributos, gerando eventos de alteração de propriedade para cada um. Por isso, agora as alterações na propriedade BirthDate através de ViewModel geram eventos de alteração das propriedades BirthDate e Age automaticamente.

É importante perceber que, se você programar uma propriedade explicitamente na classe dinâmica ViewModel (ou, mais provavelmente, em classes ViewModel derivadas específicas de modelo), o DLR não chamará os métodos TryGetMember e TrySetMember e, em vez disso, chamará as propriedades diretamente. Nesse caso, você perde este comportamento automático. No entanto, é possível refatorar o código facilmente para que as propriedades personalizadas também possam utilizar essa funcionalidade.

Voltemos ao problema da vinculação de dados em uma propriedade profundamente aninhada (em que ViewModel é o contexto de dados atual do WPF) parecida com esta:

{Binding WrappedDomainObject.Address.Country}

Usar propriedades de proxy dinâmicas significa que o objeto de domínio encapsulado subjacente não fica mais exposto, por isso a vinculação de dados na verdade seria parecida com o seguinte:

{Binding Address.Country}

Nesse caso, a propriedade Address ainda acessaria diretamente a instância de Address do modelo subjacente. Contudo, agora, quando você quer introduzir um ViewModel em torno de Address, basta adicionar uma nova propriedade na classe ViewModel de Person. A nova propriedade Address é muito simples:

public DynamicViewModel Address {
  get {
    if( addressViewModel==null )
      addressViewModel = 
        new DynamicViewModel(this.Person.Address);
    return addressViewModel;
  }
}

private DynamicViewModel addressViewModel;

Não é preciso alterar as vinculações de dados XAML porque a propriedade ainda se chama Address, mas agora o DLR chama a nova propriedade concreta em vez do método dinâmico TryGetMember. (Observe que a instanciação lenta nesta propriedade Address não é thread-safe. No entanto, somente View deve acessar ViewModel e a exibição do WPF/Silverlight tem um único thread, então isso não é motivo de preocupação.)

Essa abordagem pode ser adotada mesmo quando o modelo implementa INotifyPropertyChanged. ViewModel percebe isso e escolhe não encaminhar eventos de alteração de propriedade. Nesse caso, ela os detecta na instância do modelo subjacente e gera os eventos como se fossem seus. No construtor da classe dinâmica ViewModel, executo a verificação e me lembro do resultado:

public DynamicViewModel(DomainObject model) {
  Contract.Requires(model != null, 
    "Cannot encapsulate a null model");
  this.ModelInstance = model;

  // Raises its own property changed events
  if( model is INotifyPropertyChanged ) {
    this.ModelRaisesPropertyChangedEvents = true;
    var raisesPropChangedEvents = 
      model as INotifyPropertyChanged;
    raisesPropChangedEvents.PropertyChanged +=
      (sender,args) => 
      this.RaisePropertyChanged(args.PropertyName);
  }
}

Para evitar que ocorram eventos de alteração de propriedade duplicados, também preciso fazer uma pequena modificação no método TrySetMember.

if( this.ModelRaisesPropertyChangedEvents==false )
  this.RaisePropertyChanged(property.Name);

Portanto, é possível usar uma propriedade de proxy dinâmica para simplificar drasticamente a camada ViewModel eliminando propriedades de proxy padrão. Isso reduz consideravelmente o trabalho de codificação, testes, documentação e manutenção de longo prazo. Para adicionar novas propriedades ao modelo não é mais necessário atualizar a camada ViewModel, a menos que haja uma lógica de View muito especial para a nova propriedade. Além disso, essa abordagem pode resolver problemas difíceis, como propriedades relacionadas. O método comum TrySetMember também pode ajudar você a implementar um recurso de desfazer, pois todas as alterações de propriedade feitas pelo usuário passam por esse método TrySetMember.

Prós e contras

Muitos desenvolvedores são desconfiados da reflexão (e do DLR) devido a questões de desempenho. No meu próprio trabalho não achei que isso fosse um problema. O prejuízo ao desempenho para o usuário quando se define uma única propriedade na interface do usuário provavelmente não será percebido. Isso pode não ser o caso em IUs altamente interativas, como superfícies de design multitoque.

O único grande problema de desempenho está na população inicial da exibição quando existem muitos campos. Questões de usabilidade devem naturalmente limitar o número de campos que você expõe em qualquer tela, por isso o desempenho das vinculações de dados iniciais por meio dessa abordagem de DLR é indetectável.

Contudo, o desempenho sempre deve ser monitorado atentamente e compreendido no que se refere à experiência do usuário. A abordagem simples descrita anteriormente poderia ser reescrita com cache de reflexão. Para obter mais detalhes, consulte o artigo de Joel Pobar na edição de julho de 2005 da MSDN Magazine.

Há uma certa validade no argumento de que a legibilidade e a manutenção do código são negativamente afetadas por essa abordagem, pois parece que a camada View faz referência a propriedades em ViewModel que na verdade não existem. Entretanto, creio que as vantagens de se eliminar a maioria das propriedades de proxy codificadas manualmente são bem maiores do que os problemas, principalmente com a documentação apropriada sobre ViewModel.

A abordagem de propriedades de proxy dinâmicas reduz ou elimina a capacidade de obstruir a camada Model, porque as propriedades de Model agora são referenciadas por nome em XAML. O uso de propriedades de proxy tradicionais não limita a sua capacidade de obstruir Model porque as propriedades são referenciadas diretamente e seriam obstruídas pelo restante do aplicativo. Todavia, como a maioria das ferramentas de obstrução ainda não trabalha com XAML/BAML, isso é muito irrelevante. Em qualquer caso, um cracker de código pode começar com XAML/BAML e se aprofundar na camada Model.

Por fim, essa abordagem poderia ser utilizada indevidamente ao atribuir propriedades de modelo com metadados relacionados a segurança e ao se esperar que ViewModel seja responsável pela imposição da segurança. Não me parece que a segurança é uma responsabilidade específica de View, e creio que isso é colocar muitas responsabilidades sobre ViewModel. Nesse caso, uma abordagem orientada por aspecto aplicada dentro de Model seria mais apropriada.

Coleções

As coleções são um dos aspectos mais difíceis e menos satisfatórios do padrão de design MVVM. Se uma coleção no Model subjacente é alterada por Model, cabe a ViewModel expor de alguma forma a alteração para que View possa se atualizar devidamente.

Infelizmente, é muito provável que Model não expõe coleções que implementem a interface INotifyCollectionChanged. No .NET Framework 3.5, essa interface está em System.Windows.dll, o que desencoraja totalmente seu uso em Model. Felizmente, no .NET Framework 4, essa interface migrou para System.dll, o que torna muito mais natural utilizar coleções observáveis de dentro de Model.

Coleções observáveis em Model abrem novas possibilidades para o desenvolvimento em Model e podem ser usadas em aplicativos do Windows Forms e do Silverlight. No momento, essa é a minha abordagem preferida, pois ela é muito mais simples do que qualquer outra, e estou contente porque a interface INotifyCollectionChanged está mudando para um assembly mais comum.

Sem coleções observáveis em Model, o melhor que se pode fazer é expor algum outro mecanismo — muito provavelmente eventos personalizados — em Model para indicar quando a coleção foi alterada. Isso deve ser feito de uma maneira específica para Model. Por exemplo, se a classe Person tivesse uma coleção de endereços, ela poderia expor eventos da seguinte forma:

public event EventHandler<AddressesChangedEventArgs> 
  NewAddressAdded;
public event EventHandler<AddressesChangedEventArgs> 
  AddressRemoved;

Isso é preferível a gerar um evento de coleção personalizado projetado especificamente para ViewModel do WPF. Porém, ainda é difícil expor alterações de coleções no ViewModel. Do mesmo modo, o único recurso é gerar um evento de alteração de propriedade na propriedade da coleção de ViewModel inteira. Isso é, na melhor das hipóteses, uma solução insatisfatória.

Outro problema das coleções é determinar quando ou se cada instância de Model deve ser encapsulada na coleção dentro de uma instância de ViewModel. No caso de coleções menores, ViewModel pode expor uma nova coleção observável e copiar tudo na coleção de Model subjacente para a coleção observável de ViewModel, encapsulando cada item de Model da coleção em uma instância correspondente de ViewModel. Talvez ViewModel precise detectar eventos de alteração de coleção para transmitir as alterações do usuário de volta para Model subjacente.

Entretanto, no caso de coleções muito grandes que serão expostas em alguma forma de painel de virtualização, a abordagem mais simples e pragmática é apenas expor os objetos Model diretamente.

Instanciando ViewModel

Outro problema do padrão de design MVVM raramente discutido é onde e quando as instâncias de ViewModel devem ser instanciadas. Esse problema também costuma ser negligenciado em discussões sobre padrões de design semelhantes, como o MVC.

Minha preferência é gravar um singleton ViewModel que fornece os principais objetos de ViewModel dos quais View pode facilmente recuperar todos os outros objetos de ViewModel conforme necessário. Muitas vezes esse objeto mestre de ViewModel fornece as implementações de comando, de modo que View permita abrir documentos.

No entanto, a maioria dos aplicativos com que trabalhei até agora fornece uma interface centrada em documento, normalmente usando um espaço de trabalho com guias semelhante ao Visual Studio. Assim, na camada ViewModel, quero pensar em termos de documentos, e os documentos expõem um ou mais objetos de ViewModel que encapsulam determinados objetos de Model. Comandos padrão do WPF na camada ViewModel podem usar a camada de persistência para recuperar os objetos necessários, encapsulá-los em instâncias de ViewModel e criar gerenciadores de documentos de ViewModel para exibi-los.

No aplicativo de exemplo que acompanha este artigo, o comando ViewModel para criar um novo Person é:

internal class OpenNewPersonCommand : ICommand {
...
  // Open a new person in a new window.
  public void Execute(object parameter) {
    var person = new MvvmDemo.Model.Person();
    var document = new PersonDocument(person);
    DocumentManager.Instance.ActiveDocument = document;
  }
}

O gerenciador de documentos de ViewModel mencionado na última linha é um singleton que gerencia todos os documentos de ViewModel abertos. A pergunta é: como a coleção de documentos de ViewModel é exposta em View?

O controle guia interno do WPF não fornece o tipo de interface MDI avançada que os usuários esperam. Felizmente, existem produtos de espaço de trabalho com guias e de encaixe de terceiros disponíveis. A maioria deles se esforça para emular a aparência de documento com guias do Visual Studio, incluindo as janelas de ferramentas encaixáveis, modos divisão, janelas pop-up Ctrl+Tab (com exibições de minidocumento) e muito mais.

Infelizmente, a maioria desses componentes não dá suporte interno para o padrão de design MVVM. Mas isso não tem problema, porque você pode aplicar facilmente o padrão de design Adapter para vincular o gerenciador de documentos de ViewModel a um componente de exibição de terceiros.

Adaptador do gerenciador de documentos

O design de adaptador mostrado na Figura 2 assegura que ViewModel não exija nenhuma referência a View, por isso ele respeita os principais objetivos do padrão de design MVVM. (Contudo, neste caso, o conceito de um documento é definido na camada ViewModel e não na camada Model, pois se trata puramente de um conceito de interface do usuário.)

Figure 2 Document Manager View Adapter

Figura 2 Adaptador de exibição do gerenciador de documentos

O gerenciador de documentos de ViewModel é responsável por manter a coleção de documentos abertos de ViewModel e saber qual documento está ativo. O design permite que a camada ViewModel abra e feche documentos usando o gerenciador e que altere o documento ativo sem nenhum conhecimento de View. O lado de ViewModel dessa abordagem é razoavelmente simples. As classes ViewModel do aplicativo de exemplo estão ilustradas na Figura 3.

Figure 3 The ViewModel Layer’s Document Manager and Document Classes

Figura 3 O gerenciador de documentos e as classes de documento da camada ViewModel

A classe base Document expõe vários métodos de ciclo de vida internos (Activated, LostActivation e DocumentClosed) que são chamados pelo gerenciador de documentos para manter o documento atualizado sobre o que está acontecendo. O documento também implementa uma interface INotifyPropertyChanged para dar suporte à vinculação de dados. Por exemplo, os dados do adaptador vinculam a propriedade Title do documento de exibição à propriedade DocumentTitle de ViewModel.

A parte mais complexa dessa abordagem é a classe de adaptador, e eu forneci uma cópia de trabalho no projeto que acompanha este artigo. O adaptador se inscreve em eventos no gerenciador de documentos e usa esses eventos para manter o controle do espaço de trabalho com guias atualizado. Por exemplo, quando o gerenciador de documentos indica que um novo documento foi aberto, o adaptador recebe um evento, encapsula o documento de ViewModel no controle do WPF exigido e expõe esse controle no espaço de trabalho com guias.

O adaptador tem outra responsabilidade: manter o gerenciador de documentos de ViewModel sincronizado com as ações do usuário. Portanto, ele deve detectar eventos do controle do espaço de trabalho com guias para que, quando o usuário alterar o documento ativo ou fechar um documento, o adaptador possa notificar o gerenciador.

Embora nada nessa lógica seja muito complexo, existem algumas advertências. Há diversos cenários onde o código se torna reentrante, e isso deve ser administrado da melhor maneira possível. Por exemplo, se ViewModel usar o gerenciador de documentos para fechar um documento, o adaptador receberá o evento do gerenciador e fechará a janela do documento físico na exibição. Isso faz com que o controle do espaço de trabalho com guias também gere um evento de fechamento de documento, que o adaptador também receberá, e o manipulador de eventos do adaptador certamente notificará o gerenciador de documentos de que o documento deve ser fechado. O documento já foi fechado, por isso o gerenciador de documentos precisa ser compreensivo o suficiente para permitir isso.

A outra dificuldade é que o adaptador de View deve poder vincular um controle de documento com guia de View a um objeto Document de ViewModel. A solução mais robusta é usar uma propriedade de dependência anexada do WPF. O adaptador declara uma propriedade de dependência anexada particular que é utilizada para vincular o controle de janela de View à respectiva instância do documento de ViewModel.

No projeto de exemplo deste artigo, uso um componente de espaço de trabalho com guias de código-fonte aberto chamado AvalonDock, por isso a minha propriedade de dependência anexada é parecida com o código mostrado na Figura 4.

Figura 4 Vinculando o controle de View e o documento de ViewModel

private static readonly DependencyProperty 
  ViewModelDocumentProperty =
  DependencyProperty.RegisterAttached(
  "ViewModelDocument", typeof(Document),
  typeof(DocumentManagerAdapter), null);

private static Document GetViewModelDocument(
  AvalonDock.ManagedContent viewDoc) {

  return viewDoc.GetValue(ViewModelDocumentProperty) 
    as Document;
}

private static void SetViewModelDocument(
  AvalonDock.ManagedContent viewDoc, Document document) {

  viewDoc.SetValue(ViewModelDocumentProperty, document);
}

Quando cria um novo controle de janela de View, o adaptador define a propriedade anexada no novo controle de janela para o documento de ViewModel subjacente (consulte a Figura 5). Você também vê a vinculação de dados de título que está sendo configurada aqui e como o adaptador está configurando o contexto de dados e o conteúdo do controle de documento de View.

Figura 5 Definindo a propriedade anexada

private AvalonDock.DocumentContent CreateNewViewDocument(
  Document viewModelDocument) {

  var viewDoc = new AvalonDock.DocumentContent();
  viewDoc.DataContext = viewModelDocument;
  viewDoc.Content = viewModelDocument;

  Binding titleBinding = new Binding("DocumentTitle") { 
    Source = viewModelDocument };

  viewDoc.SetBinding(AvalonDock.ManagedContent.TitleProperty, 
    titleBinding);
  viewDoc.Closing += OnUserClosingDocument;
  DocumentManagerAdapter.SetViewModelDocument(viewDoc, 
    viewModelDocument);

  return viewDoc;
}

Ao definir o conteúdo do controle de documento de View, permito que o WPF faça o trabalho pesado de descobrir como exibir esse tipo específico de documento de ViewModel. Os modelos de dados propriamente ditos para os documentos de ViewModel ficam em um dicionário de recursos incluído na principal janela XAML.

Usei essa abordagem de gerenciamento de documentos de ViewModel com o WPF e o Silverlight e deu tudo certo. O único código da camada View é o adaptador, e ele pode ser testado facilmente e depois deixado de lado. Essa abordagem mantém ViewModel completamente independente de View e, em uma ocasião, troquei de fornecedor do componente de espaço de trabalho com guias com apenas alterações mínimas na classe de adaptador e absolutamente nenhuma alteração em ViewModel ou Model.

A capacidade de trabalhar com documentos na camada ViewModel parece tranquila, e implementar comandos de ViewModel como o que demonstrei aqui é fácil. As classes de documento de ViewModel também se tornam lugares óbvios para expor instâncias de ICommand relacionadas ao documento.

View se conecta a esses comandos e a beleza do padrão de design MVVM ressalta. Além disso, a abordagem do gerenciador de documentos de ViewModel também funciona com a abordagem de singleton, caso você precise expor dados antes que o usuário crie qualquer documento (talvez em uma janela de ferramentas recolhível).

Conclusão

O padrão de design MVVM é avançado e útil, mas nenhum padrão de design pode resolver todos os problemas. Como demonstrei aqui, combinar o padrão MVVM e objetivos com outros padrões, como adaptadores e singletons, e, ao mesmo tempo, aproveitar os novos recursos do .NET Framework 4, como a distribuição dinâmica, pode ajudar a resolver muitas questões comuns relacionadas à implementação do padrão de design MVVM. Empregar o MVVM da maneira correta resulta em aplicativos do WPF e do Silverlight muito mais elegantes e fáceis de manter. Para obter outros materiais de leitura sobre o MVVM, consulte o artigo de Josh Smith na edição de fevereiro de 2009 da MSDN Magazine.

Robert McCarter  é desenvolvedor de software canadense autônomo, arquiteto e empreendedor. Leia seu blog em robertmccarter.wordpress.com.

Agradecemos ao seguinte especialista técnicos pela revisão deste artigo:Josh Smith