Maio de 2016

Volume 31 - Número 5

Plataforma do Compilador .NET - Maximize sua experiência Model-View-ViewModel com o Roslyn

Por Alessandro Del Del

O Model-View-ViewModel (MVVM) é um padrão arquitetural muito popular que funciona perfeitamente com plataformas de aplicativos XAML como Windows Presentation Foundation (WPF) e a Plataforma Universal do Windows (UWP). Arquitetar um aplicativo usando o MVVM proporciona muitos benefícios, como o benefício de uma separação clara entre os dados, a lógica do aplicativo e a interface do usuário. Isso facilita a manutenção e o teste dos aplicativos, aumenta a reutilização do código e permite que os designers trabalhem com relação à interface do usuário sem interagir com a lógica ou com os dados. Ao longo dos anos, várias bibliotecas, modelos de projeto e estruturas, como Prism e o MVVM Light Toolkit, foram criados para ajudar os desenvolvedores a implementar o MVVM de maneira mais fácil e eficiente. Entretanto, em algumas situações, você não pode contar com bibliotecas externas ou pode simplesmente querer implementar o padrão rapidamente enquanto mantém o foco no seu código. Embora haja uma grande variedade de implementações do MVVM, a maioria compartilha alguns objetos comuns cuja geração pode ser facilmente automatizada com as APIs do Roslyn. Neste artigo, explicarei como criar refatorações personalizadas do Roslyn que ajudam a gerar elementos comuns a cada implementação do MVVM. Como não é possível oferecer um resumo completo sobre o MVVM aqui, pressuponho que você já tem um conhecimento básico do padrão do MVVM, da terminologia relacionada e das APIs de análise de código do Roslyn. Caso precise se atualizar, você pode ler estes artigos: “Padrões—Aplicativos WPF com o padrão de design Model-View-ViewModel”, “C# e Visual Basic: Use o Roslyn para criar um analisador de código ao vivo para sua API” e “C#—Adicionando uma correção ao código para seu analisador Roslyn”.

O código acompanhante está disponível nas versões C# e Visual Basic. Esta versão do artigo inclui as listagens do C# e do Visual Basic.

Classes comuns do MVVM

Uma implementação típica do MVVM exige, no mínimo, as classes a seguir (em alguns casos com nomes um pouco diferentes, dependendo do tipo de MVVM aplicado):

ViewModelBase - Uma classe abstrata básica que expõe membros que são comuns a cada ViewModel no aplicativo. Os membros comuns poderão variar dependendo da arquitetura do aplicativo, mas a implementação mais básica traz a notificação de alterações para qualquer ViewModel derivado.

RelayCommand - Uma classe que representa um comando por meio do qual o ViewModels pode invocar métodos. Normalmente existem dois tipos de RelayCommand, um genérico e um não genérico. Neste artigo, usarei o tipo genérico (RelayCommand<T>).

Pressuponho que você já está familiarizado com ambos; portanto, não entrarei em detalhes. A Figura 1a representa o código do C# para ViewModelBase, enquanto a Figura 1b mostra o código do Visual Basic.

Figura 1a Classe ViewModelBase (C#)

abstract class ViewModelBase : System.ComponentModel.INotifyPropertyChanged
{
  public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
  // Raise a property change notification
  protected virtual void OnPropertyChanged(string propertyName)
  {
    PropertyChanged?.Invoke(this, new System.ComponentModel.
PropertyChangedEventArgs(propertyName));
  }
}

Figura 1b Classe ViewModelBase (Visual Basic)

Public MustInherit Class ViewModelBase
   Implements System.ComponentModel.INotifyPropertyChanged
   Public Event PropertyChanged(sender As Object,
                                e As System.ComponentModel.PropertyChangedEventArgs) _
                                Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
   Protected Sub OnPropertyChanged(propertyName As String)
     RaiseEvent PropertyChanged(Me, New System.ComponentModel.
                                PropertyChangedEventArgs(propertyName))
   End Sub
 End Class

Esta é a implementação mais básica de ViewModelBase; fornece apenas notificação de alteração de propriedade com base na interface INotifyPropertyChanged. Evidentemente, você pode ter membros adicionais dependendo das suas necessidades específicas. A Figura 2a mostra o código do C# para RelayCommand<T>, enquanto a Figura 2b mostra o código do Visual Basic.

Figura 2a Classe RelayCommand<T> (C#)

class RelayCommand<T> : System.Windows.Input.ICommand
{
  readonly Action<T> _execute = null;
  readonly Predicate<T> _canExecute = null;
  public RelayCommand(Action<T> execute)
    : this(execute, null)
  {
  }
  public RelayCommand(Action<T> execute, Predicate<T> canExecute)
  {
    if (execute == null)
        throw new ArgumentNullException(nameof(execute));
    _execute = execute;
    _canExecute = canExecute;
  }
  [System.Diagnostics.DebuggerStepThrough]
  public bool CanExecute(object parameter)
  {
    return _canExecute == null ? true : _canExecute((T)parameter);
  }
  public event EventHandler CanExecuteChanged;
  public void RaiseCanExecuteChanged()
  {
    var handler = CanExecuteChanged;
    if (handler != null)
    {
      handler(this, EventArgs.Empty);
    }
  }
  public void Execute(object parameter)
  {
    _execute((T)parameter);
  }
}

Figura 2b Classe RelayCommand(Of T) (Visual Basic)

Class RelayCommand(Of T)
   Implements System.Windows.Input.ICommand
   Private ReadOnly _execute As Action(Of T)
   Private ReadOnly _canExecute As Predicate(Of T)
   Public Sub New(execute As Action(Of T))
     Me.New(execute, Nothing)
   End Sub
   Public Sub New(execute As Action(Of T), canExecute As Predicate(Of T))
     If execute Is Nothing Then
       Throw New ArgumentNullException(NameOf(execute))
     End If
     _execute = execute
     _canExecute = canExecute
   End Sub
   <System.Diagnostics.DebuggerStepThrough>
   Public Function CanExecute(parameter As Object) As Boolean _
     Implements System.Windows.Input.ICommand.CanExecute
     Return If(_canExecute Is Nothing, True, _canExecute(parameter))
   End Function
   Public Event CanExecuteChanged As EventHandler Implements _
     System.Windows.Input.ICommand.CanExecuteChanged
   Public Sub RaiseCanExecuteChanged()
     RaiseEvent CanExecuteChanged(Me, EventArgs.Empty)
   End Sub
   Public Sub Execute(parameter As Object) Implements ICommand.Execute
     _execute(parameter)
   End Sub
 End Class

Esta é a implementação mais comum do RelayCommand<T> e é adequada para a maioria dos cenários do MVVM. Vale a pena mencionar que essa classe implementa a interface System.Windows.Input.ICommand, que exige a implementação de um método chamado CanExecute, cuja meta é informar ao chamador se um comando está disponível para execução.

Como o Roslyn pode simplificar sua vida

Se você não trabalha com estruturas externas, o Roslyn poderá ser uma verdadeira mão na roda: É possível criar refatorações personalizadas de código que substituem uma definição de classe e implementam automaticamente um objeto necessário; você também pode automatizar facilmente a geração de classes do ViewModel com base nas propriedades do modelo. A Figura 3 mostra um exemplo do que você alcançará até o fim do artigo.

Como implementar objetos do MVVM com uma refatoração personalizada do Roslyn
Figura 3 Como implementar objetos do MVVM com uma refatoração personalizada do Roslyn

O benefício dessa abordagem é que você sempre mantém o foco no editor de códigos e pode implementar os objetos necessários com muita rapidez. Ademais, é possível gerar um ViewModel personalizado com base na classe do modelo, conforme demonstrado posteriormente no artigo. Vamos começar criando um projeto de refatoração.

Como criar um projeto para refatorações do Roslyn

A primeira etapa é criar uma nova refatoração do Roslyn. Para fazer isso, utilize o modelo de projeto Refatoração de Código (VSIX), disponível no nó Extensibilidade no idioma escolhido por você na caixa de diálogo Novo projeto. Chame o novo projeto de MVVM_Refactoring, como mostrado na Figura 4.

Como criar um projeto de refatoração do Roslyn
Figura 4 Como criar um projeto de refatoração do Roslyn

Clique em OK quando estiver pronto. Quando o Visual Studio 2015 gera o projeto, adiciona automaticamente uma classe chamada MVVMRefactoringCodeRefactoringProvider, definida dentro do arquivo CodeRefactoringProvider.cs (ou .vb para o Visual Basic). Renomeie a classe e o arquivo como MakeViewModelBaseRefactoring e MakeViewModelBaseRefactoring.cs, respectivamente. Por uma questão de clareza, remova os métodos ComputeRefactoringsAsync e ReverseTypeNameAsync gerados automaticamente (o último é gerado automaticamente para fins de demonstração).

Como investigar um nó de sintaxe

Talvez você saiba que o principal ponto de entrada para uma refatoração de código é o método ComputeRefactoringsAsync, que é responsável por criar a chamada ação rápida que será conectada à lâmpada do editor de código, caso a análise de código de um nó de sintaxe cumpra as regras necessárias. Nesse caso específico, o método ComputeRefactoringsAsync precisa detectar se o desenvolvedor está chamando a lâmpada por uma declaração de classe. Com a ajuda da janela de ferramentas Visualizador de sintaxe, você pode entender facilmente os elementos de sintaxe com os quais precisa trabalhar. Mais especificamente, no C#, você precisa detectar se o nó de sintaxe é um ClassDeclaration, representado por um objeto do tipo Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax (veja a Figura 5); já no Visual Basic, determina se o nó de sintaxe é um ClassStatement, representado por um objeto do tipo Microsoft.CodeAnalysis.VisualBasic.Syntax.ClassStatementSyntax. Na verdade, no Visual Basic, o ClassStatement é um nó filho do ClassBlock, que representa o código inteiro para uma classe. O C# e o Visual Basic têm objetos diferentes por causa da maneira em que cada um representa uma definição de classe: O C# usa a palavra-chave classe com chaves como delimitadores, enquanto o Visual Basic usa a palavra-chave Classe com a instrução Fim da Classe como delimitador.

Como entender uma declaração de classe
Figura 5 Como entender uma declaração de classe

Como criar uma ação

A primeira refatoração de código que discutirei está relacionada à classe ViewModelBase. A primeira etapa é gravar o método ComputeRefactoringsAsync na classe MakeViewModelBaseRefactoring. Com esse método, você verifica se o nó de sintaxe representa uma declaração de classe; em caso afirmativo, pode criar e registrar uma ação que estará disponível na lâmpada. A Figura 6a mostra como fazer isso no C#, enquanto a Figura 6b mostra o código do Visual Basic (veja os comentários embutidos).

Figura 6a O principal ponto de entrada: Método ComputeRefactoringsAsync (C#)

private string Title = "Make ViewModelBase class";
public async sealed override Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
  // Get the root node of the syntax tree
  var root = await context.Document.
    GetSyntaxRootAsync(context.CancellationToken).
    ConfigureAwait(false);
  // Find the node at the selection.
  var node = root.FindNode(context.Span);
  // Is this a class statement node?
  var classDecl = node as ClassDeclarationSyntax;
  if (classDecl == null)
  {
    return;
  }
  // If so, create an action to offer a refactoring
  var action = CodeAction.Create(title: Title,
    createChangedDocument: c =>
    MakeViewModelBaseAsync(context.Document,
      classDecl, c), equivalenceKey: Title);
  // Register this code action.
  context.RegisterRefactoring(action);
}

Figura 6b O principal ponto de entrada: Método ComputeRefactoringsAsync (Visual Basic)

Private Title As String = "Make ViewModelBase class"
 Public NotOverridable Overrides Async Function _
   ComputeRefactoringsAsync(context As CodeRefactoringContext) As Task
   ' Get the root node of the syntax tree
   Dim root = Await context.Document.
     GetSyntaxRootAsync(context.CancellationToken).
     ConfigureAwait(False)
   ' Find the node at the selection.
   Dim node = root.FindNode(context.Span)
   ' Is this a class statement node?
   Dim classDecl = TryCast(node, ClassStatementSyntax)
   If classDecl Is Nothing Then Return
   ' If so, create an action to offer a refactoring
   Dim action = CodeAction.Create(title:=Title,
                                  createChangedDocument:=Function(c) _
                                  MakeViewModelBaseAsync(context.
                                  Document, classDecl, c),
                                  equivalenceKey:=Title)
   ' Register this code action.
   context.RegisterRefactoring(action)
 End Function

Com esse código, você registrou uma ação que pode ser chamada no nó de sintaxe se este for uma declaração de classe. A ação é realizada pelo método MakeViewModelBaseAsync, que implementa a lógica de refatoração e fornece uma classe totalmente nova.

Geração de código

Além de oferecer uma maneira estruturada e orientada ao objeto de representar o código-fonte, o Roslyn também permite analisar o texto de origem e gerar uma árvore de sintaxe com fidelidade total. Para gerar uma nova árvore de sintaxe de texto puro, você invoca o método SyntaxFactory.ParseSyntaxTree. Ele pega um argumento do tipo System.String que contém o código-fonte do qual você deseja gerar uma SyntaxTree.

O Roslyn também oferece os métodos VisualBasicSyntaxTree.ParseText e CSharpSyntaxTree.ParseText para alcançar o mesmo resultado; contudo, nesse caso, faz sentido usar o SyntaxFactory.ParseSyntaxTree porque o código chama outros métodos de Análise da SyntaxFactory, como você verá em breve.

Depois que tiver uma nova instância de SyntaxTree, é possível realizar análise de código e outras operações relacionadas ao código nela. Você pode, por exemplo, analisar o código-fonte de uma classe inteira, gerar uma árvore de sintaxe dela, substituir um nó de sintaxe na classe e devolver uma classe nova. No caso do padrão do MVVM, as classes comuns têm uma estrutura fixa; portanto, o processo de analisar o texto de origem e substituir uma definição de classe por uma nova é rápido e fácil. Ao tirar proveito dos chamados literais de cadeia de caracteres de várias linhas, você pode colar uma definição de classe inteira em um objeto do tipo System.String, obter uma SyntaxTree dele, recuperar a SyntaxNode que corresponde à definição de classe e substituir a classe original na árvore pela nova. Primeiramente, demonstrarei como fazer isso em relação à classe ViewModelBase. Mais especificamente, a Figura 7a mostra o código para C#, enquanto a Figura 7b mostra o código para Visual Basic.

Figura 7a MakeViewModelBaseAsync: Como gerar uma nova árvore de sintaxe do texto de origem (C#)

private async Task<Document> MakeViewModelBaseAsync(Document document,
  ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken)
{
  // The class definition represented as source text
  string newImplementation = @"abstract class ViewModelBase : INotifyPropertyChanged
{
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
// Raise a property change notification
protected virtual void OnPropertyChanged(string propertyName)
{
  PropertyChanged?.Invoke(this, new System.ComponentModel.
    PropertyChangedEventArgs(propertyName));
}
}
";
  // 1. ParseSyntaxTree() gets a new SyntaxTree from the source text
  // 2. GetRoot() gets the root node of the tree
  // 3. OfType<ClassDeclarationSyntax>().FirstOrDefault()
  //    retrieves the only class definition in the tree
  // 4. WithAdditionalAnnotations() is invoked for code formatting
  var newClassNode = SyntaxFactory.ParseSyntaxTree(newImplementation).
    GetRoot().DescendantNodes().
    OfType<ClassDeclarationSyntax>().
    FirstOrDefault().
    WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);
  // Get the root SyntaxNode of the document
  var root = await document.GetSyntaxRootAsync();
  // Generate a new CompilationUnitSyntax (which represents a code file)
  // replacing the old class with the new one
  CompilationUnitSyntax newRoot = (CompilationUnitSyntax)root.
    ReplaceNode(classDeclaration, newClassNode).NormalizeWhitespace();
  // Detect if a using System.ComponentModel directive already exists.
  if ((newRoot.Usings.Any(u => u.Name.ToFullString() ==
    "System.ComponentModel"))== false)
  {
    // If not, add one
    newRoot = newRoot.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.
      QualifiedName(SyntaxFactory.IdentifierName("System"),
                    SyntaxFactory.IdentifierName("ComponentModel"))));
  }
  // Generate a new document based on the new SyntaxNode
  var newDocument = document.WithSyntaxRoot(newRoot);
  // Return the new document
  return newDocument;
}

Figura 7b MakeViewModelBaseAsync: Como gerar uma nova árvore de sintaxe do texto de origem (Visual Basic)

Private Async Function MakeViewModelBaseAsync(document As Document,
   classDeclaration As ClassStatementSyntax,
   cancellationToken As CancellationToken) As Task(Of Document)
   ' The class definition represented as source text
   Dim newImplementation = "Public MustInherit Class ViewModelBase
 Implements INotifyPropertyChanged
 Public Event PropertyChanged(sender As Object,
                              e As PropertyChangedEventArgs) _
                              Implements INotifyPropertyChanged.PropertyChanged
 Protected Sub OnPropertyChanged(propertyName As String)
   RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
 End Sub
 End Class
 "
   ' 1. ParseSyntaxTree() gets a New SyntaxTree from the source text
   ' 2. GetRoot() gets the root node of the tree
   ' 3. OfType(Of ClassDeclarationSyntax)().FirstOrDefault()
   '    retrieves the only class definition in the tree
   ' 4. WithAdditionalAnnotations() Is invoked for code formatting
   Dim newClassNode = SyntaxFactory.ParseSyntaxTree(newImplementation).
     GetRoot().DescendantNodes().
     OfType(Of ClassBlockSyntax)().
     FirstOrDefault().
     WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation)
   Dim parentBlock = CType(classDeclaration.Parent, ClassBlockSyntax)
   ' Get the root SyntaxNode of the document
   Dim root = Await document.GetSyntaxRootAsync(cancellationToken)
   ' Generate a new CompilationUnitSyntax (which represents a code file)
   ' replacing the old class with the new one
   Dim newRoot As CompilationUnitSyntax = root.ReplaceNode(parentBlock,
                                                           newClassNode).
                                                           NormalizeWhitespace()
   ' Detect if an Imports System.ComponentModel directive already exists
  ' If Not newRoot.Imports.Any(Function(i) i.ImportsClauses.
    Where(Function(f) f.ToString = "System.ComponentModel").Any) Then            
  ' If not, add one
     Dim newImp = SyntaxFactory.
       ImportsStatement(SyntaxFactory.
       SingletonSeparatedList(Of ImportsClauseSyntax)(SyntaxFactory.
       SimpleImportsClause(SyntaxFactory.
       ParseName("System.ComponentModel"))))
     newRoot = newRoot.AddImports(newImp)
   End If
   ' Generate a new document based on the new SyntaxNode
   Dim newDocument = document.WithSyntaxRoot(newRoot)
 ' Return the new document
   Return newDocument
 End Function

Como o tipo SyntaxFactory é usado muitas vezes, você poderia fazer uma importação estática e, consequentemente, simplificar seu código adicionando uma diretiva Importa Microsoft.CodeAnalisys.VisualBasic.SyntaxFactory no Visual Basic e uma diretiva estática usando Microsoft.CodeAnalysis.CSharp.SyntaxFactory no C#. Aqui, não há importação estática para facilitar a descoberta de alguns dos métodos que a SyntaxFactory oferece.

Observe que o método MakeViewModelBaseAsync pega três argumentos:

  • Um Documento, que representa o arquivo atual do código-fonte
  • Um ClassDeclarationSyntax (no Visual Basic, é ClassStatementSyntax), que representa a declaração de classe com a qual a análise de código é executada
  • Um CancellationToken, que é utilizado caso a operação precise ser cancelada

O código chama primeiro o SyntaxFactory.ParseSyntaxTree para obter uma nova instância de SyntaxTree com base no texto de origem que representa a classe ViewModelBase. A invocação de GetRoot é necessária para obter a instância raiz de SyntaxNode para a árvore de sintaxe. Nesse cenário específico, você sabe de antemão que o texto de origem analisado tem apenas uma definição de classe; portanto, o código chama FirstOrDefault<T> em OfType<T> para recuperar o único nó descendente do tipo necessário, que é ClassDeclarationSyntax no C# e ClassBlockSyntax no Visual Basic. Nesse ponto, você precisa substituir a definição de classe original pela classe ViewModelBase. Para fazer isso, o código chama primeiramente Document.GetSyntaxRootAsync para recuperar o nó raiz de modo assíncrono para a árvore de sintaxe do documento; a seguir, chama ReplaceNode para substituir a definição de classe antiga pela nova classe ViewModelBase. Observe como o código detecta se existe uma diretiva usando (C#) ou Importa (Visual Basic) para o namespace System.ComponentModel investigando as coleções CompilationUnitSyntax.Usings e CompilationUnitSyntax.Imports, respectivamente. Em caso negativo, é adicionada uma diretiva adequada. Isso é útil para adicionar uma diretiva no nível do arquivo de código, caso ainda não esteja disponível.

Lembre-se: os objetos são imutáveis no Roslyn. É o mesmo conceito que se aplica à classe String: Na verdade, você nunca modifica uma cadeia de caracteres; logo, quando edita uma cadeia de caracteres ou invoca métodos como Replace, Trim ou Substring, obtém uma nova cadeia de caracteres com as alterações especificadas. Por esse motivo, sempre que você precisa editar um nó de sintaxe, na verdade cria um novo nó de sintaxe com as propriedades atualizadas.

No Visual Basic, o código também precisa recuperar o pai ClassBlockSyntax para o nó de sintaxe atual, que está no lugar do tipo ClassStatementSyntax. Isso é necessário para recuperar a instância de SyntaxNode real que será substituído. O fornecimento de uma implementação comum da classe RelayCommand<T> funciona exatamente do mesmo modo, mas você precisa adicionar uma nova refatoração de código. Para fazer isso, clique com o botão direito do mouse no nome do projeto no Gerenciador de Soluções e selecione Adicionar | Novo item. Na caixa de diálogo Adicionar novo item, selecione o modelo Refatoração e nomeie o novo arquivo como MakeRelayCommandRefactoring.cs (ou .vb, para o Visual Basic). A lógica de refatoração é igual à da classe ViewModelBase (obviamente, com um texto de origem diferente). A Figura 8a mostra o código completo do C# para a nova refatoração, que inclui os métodos ComputeRefactoringsAsync e MakeRelayCommandAsync; a Figura 8b mostra o código do Visual Basic.

Figura 8a Refatoração de código que implementa a classe RelayCommand<T> (C#)

[ExportCodeRefactoringProvider(LanguageNames.CSharp,
  Name = nameof(MakeRelayCommandRefactoring)), Shared]
internal class MakeRelayCommandRefactoring : CodeRefactoringProvider
{
  private string Title = "Make RelayCommand<T> class";
  public async sealed override Task
    ComputeRefactoringsAsync(CodeRefactoringContext context)
  {
    var root = await context.Document.GetSyntaxRootAsync(context.
      CancellationToken).
        ConfigureAwait(false);
    // Find the node at the selection.
    var node = root.FindNode(context.Span);
    // Only offer a refactoring if the selected node is
    // a class statement node.
    var classDecl = node as ClassDeclarationSyntax;
    if (classDecl == null)
    {
      return;
    }
    var action = CodeAction.Create(title: Title,
      createChangedDocument: c =>
      MakeRelayCommandAsync(context.Document,
      classDecl, c), equivalenceKey: Title);
    // Register this code action.
    context.RegisterRefactoring(action);
  }
  private async Task<Document>
    MakeRelayCommandAsync(Document document,
    ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken)
  {
    // The class definition represented as source text
    string newImplementation = @"
class RelayCommand<T> : ICommand
{
  readonly Action<T> _execute = null;
  readonly Predicate<T> _canExecute = null;
  public RelayCommand(Action<T> execute)
    : this(execute, null)
  {
  }
  public RelayCommand(Action<T> execute, Predicate<T> canExecute)
  {
    if (execute == null)
      throw new ArgumentNullException(""execute"");
      _execute = execute;
        _canExecute = canExecute;
  }
  [System.Diagnostics.DebuggerStepThrough]
  public bool CanExecute(object parameter)
  {
    return _canExecute == null ? true : _canExecute((T)parameter);
  }
  public event EventHandler CanExecuteChanged;
  public void RaiseCanExecuteChanged()
  {
    var handler = CanExecuteChanged;
    if (handler != null)
    {
      handler(this, EventArgs.Empty);
    }
  }
  public void Execute(object parameter)
  {
    _execute((T)parameter);
  }
}
";
    // 1. ParseSyntaxTree() gets a new SyntaxTree from the source text
    // 2. GetRoot() gets the root node of the tree
    // 3. OfType<ClassDeclarationSyntax>().FirstOrDefault() retrieves the only class
    //      definition in the tree
    // 4. WithAdditionalAnnotations() is invoked for code formatting
    var newClassNode = SyntaxFactory.ParseSyntaxTree(newImplementation).
      GetRoot().DescendantNodes().
      OfType<ClassDeclarationSyntax>().
      FirstOrDefault().
      WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);
    // Get the root SyntaxNode of the document
    var root = await document.GetSyntaxRootAsync(cancellationToken);
    // Generate a new CompilationUnitSyntax (which represents a code file)
    // replacing the old class with the new one
    CompilationUnitSyntax newRoot = (CompilationUnitSyntax) root.
      ReplaceNode(classDeclaration,
      newClassNode).NormalizeWhitespace();
    if ((newRoot.Usings.Any(u => u.Name.ToFullString() == "System.Windows.Input"))
      == false)
    {
      newRoot = newRoot.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.
        QualifiedName(SyntaxFactory.IdentifierName("System"),
          SyntaxFactory.IdentifierName("Windows.Input"))));
    }
    // Generate a new document based on the new SyntaxNode
    var newDocument = document.WithSyntaxRoot(newRoot);
    // Return the new document
    return newDocument;
  }
}

Figura 8b Refatoração de código que implementa a classe RelayCommand(Of T) (Visual Basic)

<ExportCodeRefactoringProvider(LanguageNames.VisualBasic,
                                Name:=NameOf(MakeRelayCommandRefactoring)), [Shared]>
 Friend Class MakeRelayCommandRefactoring
   Inherits CodeRefactoringProvider
   Public NotOverridable Overrides Async Function _
     ComputeRefactoringsAsync(context As CodeRefactoringContext) As Task
     Dim root = Await context.Document.
       GetSyntaxRootAsync(context.CancellationToken).
       ConfigureAwait(False)
     ' Find the node at the selection.
     Dim node = root.FindNode(context.Span)
     ' Only offer a refactoring if the selected node is a class statement node.
     Dim classDecl = TryCast(node, ClassStatementSyntax)
     If classDecl Is Nothing Then Return
     Dim action = CodeAction.Create("Make RelayCommand(Of T) class",
                                    Function(c) _
                                    MakeRelayCommandAsync(context.Document,
                                                          classDecl, c))
     ' Register this code action.
     context.RegisterRefactoring(action)
  End Function
   Private Async Function MakeRelayCommandAsync(document As Document, _
    classDeclaration As ClassStatementSyntax, cancellationToken As CancellationToken) _
    As Task(Of Document)
     ' The class definition represented as source text
     Dim newImplementation = "Class RelayCommand(Of T)
   Implements ICommand
   Private ReadOnly _execute As Action(Of T)
   Private ReadOnly _canExecute As Predicate(Of T)
   Public Sub New(ByVal execute As Action(Of T))
     Me.New(execute, Nothing)
   End Sub
   Public Sub New(ByVal execute As Action(Of T), ByVal canExecute As Predicate(Of T))
     If execute Is Nothing Then
       Throw New ArgumentNullException(""execute"")
     End If
     _execute = execute
     _canExecute = canExecute
   End Sub
   <DebuggerStepThrough> _
   Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
     Return If(_canExecute Is Nothing, True, _canExecute(CType(parameter, T)))
   End Function
   Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
   Public Sub RaiseCanExecuteChanged()
     RaiseEvent CanExecuteChanged(Me, EventArgs.Empty)
   End Sub
   Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute
     _execute(CType(parameter, T))
   End Sub
 End Class"
   ' 1. ParseSyntaxTree() gets a new SyntaxTree from the source text
   ' 2. GetRoot() gets the root node of the tree
   ' 3. OfType(Of ClassDeclarationSyntax)().FirstOrDefault()
   '    retrieves the only class definition in the tree
   ' 4. WithAdditionalAnnotations() Is invoked for code formatting
   Dim newClassNode = SyntaxFactory.ParseSyntaxTree(newImplementation).
   GetRoot().DescendantNodes().
     OfType(Of ClassBlockSyntax)().
     FirstOrDefault().
     WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation)
   Dim parentBlock = CType(classDeclaration.Parent, ClassBlockSyntax)
   Dim root = Await document.GetSyntaxRootAsync(cancellationToken)
   ' Generate a new CompilationUnitSyntax (which represents a code file)
   ' replacing the old class with the new one
   Dim newRoot As CompilationUnitSyntax =
     root.ReplaceNode(parentBlock, newClassNode)
   'Detect if an Imports System.Windows.Input directive already exists
   If Not newRoot.Imports.Any(Function(i) i.ToFullString.
     Contains("System.Windows.Input")) Then
       'If not, add one
       Dim newImp = SyntaxFactory.
         ImportsStatement(SyntaxFactory.
         SingletonSeparatedList(Of ImportsClauseSyntax) _
         (SyntaxFactory.
         SimpleImportsClause(SyntaxFactory.
         ParseName("System.Windows.Input"))))
       newRoot = newRoot.AddImports(newImp)
     End If
     Dim newDocument = document.WithSyntaxRoot(newRoot)
     Return newDocument
   End Function
 End Class

Você concluiu com êxito duas refatorações personalizadas e, agora, tem o básico para implementar refatorações adicionais, dependendo da sua implementação do padrão do MVVM (como um agente de mensagem, um localizador de serviço e classes de serviço).

Como alternativa, você poderia usar a classe SyntaxGenerator. São oferecidas APIs independentes de linguagem, o que significa que o código escrito por você resulta em uma refatoração direcionada para Visual Basic e C#. Todavia, essa abordagem exige a geração de cada elemento de sintaxe único para o texto de origem. Ao usar SyntaxFactory.ParseSyntaxTree, é possível analisar qualquer texto de origem. Isso é especialmente útil se você escreve ferramentas de desenvolvedor que precisam manipular um texto de origem que não sabe com antecedência.

Disponibilidade: Aplicativos da UWP e WPF

Em vez de disponibilizar refatorações personalizadas universalmente, faz sentido restringir sua disponibilidade na lâmpada apenas para as plataformas com as quais você realmente usa o MVVM, tais como WPF e UWP. No método ComputeRefactoringsAsync, você pode obter uma instância do modelo semântico para o documento atual e, depois, invocar o método GetTypeByMetadataName da propriedade Compilação. Por exemplo, o código a seguir demonstra como disponibilizar a refatoração apenas para os aplicativos da UWP:

// Restrict refactoring availability to Windows 10 apps only
var semanticModel = await context.Document.GetSemanticModelAsync();
var properPlatform = semanticModel.Compilation.
  GetTypeByMetadataName("Windows.UI.Xaml.AdaptiveTrigger");
if (properPlatform != null)
{
  var root = await context.Document.
    GetSyntaxRootAsync(context.CancellationToken).
    ConfigureAwait(false);
  // ...
}

Como o tipo Windows.UI.Xaml.AdaptiveTrigger existe apenas em aplicativos UWP, a refatoração ficará disponível se o mecanismo de análise de código detectar que foi feita uma referência a esse tipo. Se você quisesse disponibilizar a refatoração para a WPF, poderia escrever a verificação a seguir:

// Restrict refactoring availability to WPF apps only
var semanticModel = await context.Document.GetSemanticModelAsync();
var properPlatform = semanticModel.Compilation.
  GetTypeByMetadataName("System.Windows.Navigation.JournalEntry");

Da mesma forma, o System.Windows.Navigation.JournalEntry existe exclusivamente na WPF; portanto, um resultado não nulo de GetTypeByMetadataName significa que a análise de código é executada com relação a um projeto da WPF. Evidentemente, as duas verificações poderiam ser combinadas para disponibilizar as refatorações para as duas plataformas.

Como testar o código

Pressione F5 para testar o trabalho que você fez até o momento na instância experimental do Visual Studio. Por exemplo, crie um projeto da WPF e adicione esta classe muito simples:

class EmptyClass
{
  string aProperty { get; set; }
}

Clique com o botão direito do mouse na declaração de classe e selecione Ações rápidas no menu de contexto. Nesse momento, a lâmpada mostra as duas refatorações novas, como esperado, e fornece a sugestão adequada (veja a Figura 3 para referência).

Se você quiser publicar refatorações personalizadas, o modelo de projeto Refatoração de Código (VSIX) automatizará a geração de um pacote VSIX que pode ser publicado na Galeria do Visual Studio. Caso prefira publicar seu trabalho como um pacote NuGet, o truque é criar um Analisador com o projeto Correção de Código e, em seguida, adicionar os modelos de item Correção de Código.

Como gerar um ViewModel personalizado com o Roslyn

Se você quer saber por que usar o Roslyn pode ser uma abordagem melhor do que apenas adicionar texto estático a uma classe, imagine que deseja automatizar a geração de um ViewModel de uma classe de negócios, que é o modelo. Nesse caso, é possível gerar uma nova classe ViewModel e adicionar as propriedades necessárias com base nos dados expostos pelo modelo. Para você ter uma ideia, a Figura 9 mostra como produzir uma refatoração chamada Fazer classe ViewModel, que demonstra como criar uma versão simplificada de um ViewModel.

Figura 9 Como gerar um ViewModel com base em um modelo

private async Task<Document> MakeViewModelAsync(Document document,
  ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken)
{
  // Get the name of the model class
  var modelClassName = classDeclaration.Identifier.Text;
  // The name of the ViewModel class
  var viewModelClassName = $"{modelClassName}ViewModel";
  // Only for demo purposes, pluralizing an object is done by
  // simply adding the "s" letter. Consider proper algorithms
  string newImplementation = $@"class {viewModelClassName} : INotifyPropertyChanged
{{
public event PropertyChangedEventHandler PropertyChanged;
// Raise a property change notification
protected virtual void OnPropertyChanged(string propname)
{{
  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}}
private ObservableCollection<{modelClassName}> _{modelClassName}s;
public ObservableCollection<{modelClassName}> {modelClassName}s
{{
  get {{ return _{modelClassName}s; }}
  set
  {{
    _{modelClassName}s = value;
    OnPropertyChanged(nameof({modelClassName}s));
  }}
}}
public {viewModelClassName}() {{
// Implement your logic to load a collection of items
}}
}}
";
    var newClassNode = SyntaxFactory.ParseSyntaxTree(newImplementation).
      GetRoot().DescendantNodes().
      OfType<ClassDeclarationSyntax>().
      FirstOrDefault().
      WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);
    // Retrieve the parent namespace declaration
    var parentNamespace = (NamespaceDeclarationSyntax) classDeclaration.Parent;
    //Add the new class to the namespace
    var newParentNamespace =
      parentNamespace.AddMembers(newClassNode).NormalizeWhitespace();
    var root = await document.GetSyntaxRootAsync(cancellationToken);
    CompilationUnitSyntax newRoot = (CompilationUnitSyntax)root;
    newRoot = newRoot.
              ReplaceNode(parentNamespace, newParentNamespace).NormalizeWhitespace();
    newRoot = newRoot.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.
      QualifiedName(SyntaxFactory.IdentifierName("System"),
        SyntaxFactory.IdentifierName("Collections.ObjectModel"))),
        SyntaxFactory.UsingDirective(SyntaxFactory.
      QualifiedName(SyntaxFactory.IdentifierName("System"),
        SyntaxFactory.IdentifierName("ComponentModel"))));
    // Generate a new document based on the new SyntaxNode
    var newDocument = document.WithSyntaxRoot(newRoot);
    // Return the new document
    return newDocument;
  }

Esse código gera uma classe ViewModel que expõe uma ObservableCollection do tipo de modelo, mais um construtor vazio no qual você deve implementar sua lógica, conforme mostrado na Figura 10. Evidentemente, esse código deve ser ampliado com os membros adicionais de que você pode precisar, tais como propriedades de dados e comandos, e deve ser melhorado com um algoritmo de pluralização mais eficiente.

Como automatizar a geração de uma classe ViewModel
Figura 10 Como automatizar a geração de uma classe ViewModel

Aprenda com a Microsoft: A refatoração INotifyPropertyChanged

Uma das tarefas repetitivas feitas com o MVVM é implementar a notificação de alterações em classes no seu modelo de dados por meio da interface System.ComponentModel.INotifyPropertyChanged. Por exemplo, se você tiver a classe Customer a seguir:

class Customer 
{
  string CompanyName { get; set; }
}

Deve implementar a INotifyPropertyChanged para que os objetos associados sejam notificados em caso de alterações nos dados:

class Customer : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;
  string _companyName;
  string CompanyName
  {
    get { return _companyName; }
    set {
      _companyName = value;
      PropertyChanged?.Invoke(this,
        new PropertyChangedEventArgs(nameof(CompanyName))); }
  }
}

Como é possível imaginar, em um modelo de dados composto por muitas classes com dezenas de propriedades, essa tarefa pode demorar bastante. Entre as amostras que acompanham o Roslyn, a Microsoft enviou uma refatoração de código que automatiza a implementação da interface INotifyPropertyChanged com apenas um clique. Chama-se ImplementNotifyPropertyChanged e está disponível para C# e Visual Basic na subpasta Src/Samples do repositório do Roslyn em github.com/dotnet/roslyn. Se compilar e testar o exemplo, você verá como a implementação da interface INotifyPropertyChanged se torna rápida e eficiente, como mostrado na Figura 11.

Como implementar a interface INotifyPropertyChanged
Figura 11 Como implementar a interface INotifyPropertyChanged

Esse exemplo é particularmente útil porque mostra como usar as APIs do Roslyn para demonstrar uma definição de objeto, como analisar membros específicos e como fazer edições em propriedades existentes sem fornecer uma definição de classe completamente nova. Sem dúvida, recomenda-se estudar o código-fonte para esse exemplo a fim de entender cenários mais complexos de geração de código.

Conclusão

Além do número quase infinito de usos possíveis, o Roslyn também torna incrivelmente fácil o suporte para o padrão Model-View-ViewModel. Como mostrei neste artigo, você pode aproveitar as APIs do Roslyn para analisar o código-fonte de determinadas classes necessárias em qualquer implementação do MVVM, como ViewModelBase e RelayCommand<T>, e gerar um novo nó de sintaxe capaz de substituir uma definição de classe existente. O Visual Studio 2015 também mostrará uma visualização na lâmpada, proporcionando outra experiência fantástica de codificação.


Alessandro del Soleé Microsoft MVP desde 2008. Premiado cinco vezes como MVP of the Year, é autor de muitos livros, eBooks, vídeos instrutivos e artigos sobre o desenvolvimento em .NET com o Visual Studio. Del Sole trabalha como Solution Developer Expert para a Brain-Sys, com foco em desenvolvimento, treinamento e consultoria em .NET. Você pode segui-lo no Twitter: @progalex.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Jason Bock (Magenic) e Anthony Green (Microsoft)

Jason Bock é Practice Lead para a Magenic (magenic.com), escritor e palestrante. Visite seu site em jasonbock.net.