Mayo de 2016

Volumen 31, número 5

Plataforma del compilador de .NET: maximice su experiencia con el patrón Model-View-ViewModel con Roslyn

Por Alessandro Del Del

Model-View-ViewModel (MVVM) es un patrón arquitectónico muy popular que funciona a la perfección con las plataformas de aplicaciones XAML, como Windows Presentation Foundation (WPF) y la Plataforma universal de Windows (UWP). El diseño de una aplicación mediante MVVM proporciona, entre muchas otras cosas, la ventaja de una separación limpia de los datos, la lógica de la aplicación y la UI. Ello hace que las aplicaciones sean más fáciles de mantener y probar, mejora la reutilización de código y permite a los diseñadores trabajar con UI sin interactuar con la lógica o los datos. Durante años, varias bibliotecas y plantillas de proyecto, así como varios marcos, como Prism y MVVM Light Toolkit, se han compilado para ayudar a los desarrolladores a implementar MVVM de manera más fácil y eficiente. No obstante, en algunas situaciones, no puede depender de bibliotecas externas o quizás quiere poder implementar el patrón rápidamente sin dejar de centrarse en el código. Aunque existe una gran variedad de implementaciones de MVVM, la mayoría comparten varios objetos comunes, cuya generación se puede automatizar fácilmente con las API de Roslyn. En este artículo, explicaré cómo crear refactorizaciones de Roslyn personalizadas que faciliten la generación de elementos comunes para todas las implementaciones de MVVM. Dado que no es posible ofrecer un resumen completo sobre MVVM aquí, supongo que tiene los conocimientos básicos del patrón MVVM, de la terminología relacionada y de las API de análisis de código Roslyn. Si necesita ponerse al día, puede leer los artículos siguientes: "Patrones: Aplicaciones WPF con el patrón de diseño Model-View-ViewModel", "C# y Visual Basic: uso de Roslyn para escribir un analizador de código en directo para su API" y "C# - Adding a Code Fix to Your Roslyn Analyzer" (C#: incorporación de correcciones de código al analizador de Roslyn).

El código que lo acompaña está disponible en las versiones de C# y Visual Basic. Esta versión del artículo incluye las listas de C# y Visual Basic.

Clases MVVM comunes

Cualquier implementación MVVM típica requiere como mínimo las clases siguientes (en algunos casos, con nombres ligeramente diferentes, según el tipo de MVVM que aplique):

ViewModelBase: clase base abstracta que expone los miembros comunes para todas las clases ViewModel de la aplicación. Los miembros comunes pueden variar según la arquitectura de la aplicación, pero su implementación más básica incorpora la notificación de cambios a cualquier clase ViewModel derivada.

RelayCommand: clase que representa un comando a través del cual las clases ViewModel pueden invocar métodos. En general, existen dos tipos de clase RelayCommand, uno genérico y otro no genérico. En este artículo, usaré el tipo genérico (RelayCommand<T>).

Supongo que ambos le resultan familiares, por lo que no entraré en detalles. La Figura 1a representa el código de C# de la clase ViewModelBase y la Figura 1b muestra el código de Visual Basic.

Figura 1a Clase 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 Clase 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 es la implementación más básica de la clase ViewModelBase; solo proporciona la notificación de cambios de propiedades basada en la interfaz INotifyPropertyChanged. Por supuesto, puede tener miembros adicionales según sus necesidades específicas. La Figura 2a muestra el código de C# de RelayCommand<T> y la Figura 2b muestra el código de Visual Basic.

Figura 2a Clase 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 Clase 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 es la implementación más común de RelayCommand<T> y resulta adecuada para la mayoría de los escenarios de MVVM. Vale la pena mencionar que esta clase implementa la interfaz System.Windows.Input.ICommand, que requiere la implementación de un método denominado CanExecute, cuyo objetivo es indicar al autor de la llamada si un comando está disponible para ejecutarse.

Cómo Roslyn puede simplificarle la vida

Si no trabaja con marcos externos, Roslyn puede ser un verdadero salvavidas: Puede crear refactorizaciones de código personalizadas que reemplacen una definición de clase e implementen automáticamente un objeto necesario, y puede automatizar fácilmente la generación de clases ViewModel basada en las propiedades del modelo. La Figura 3 muestra un ejemplo de lo que habrá conseguido al final de este artículo.

Implementación de objetos MVVM con una refactorización personalizada de Roslyn
Figura 3 Implementación de objetos MVVM con una refactorización personalizada de Roslyn

La ventaja de este enfoque es que siempre mantiene el foco en el editor de código y puede implementar los objetos necesarios con gran rapidez. Además, puede generar una clase ViewModel personalizada basada en la clase del modelo, como se demuestra más adelante en este artículo. Comencemos creando un proyecto de refactorización.

Creación de un proyecto para las refactorizaciones de Roslyn

El primer paso es crear una nueva refactorización de Roslyn. Para hacerlo, debe usar la plantilla de proyecto Code Refactoring (VSIX), disponible en el nodo Extensibilidad, debajo del lenguaje que ha elegido en el cuadro de diálogo Nuevo proyecto. Asigne al nuevo proyecto el nombre MVVM_Refactoring, como se muestra en la Figura 4.

Creación de un proyecto de refactorización de Roslyn
Figura 4 Creación de un proyecto de refactorización de Roslyn

Haga clic en Aceptar cuando esté preparado. Cuando Visual Studio 2015 genera el proyecto, agrega automáticamente una clase denominada MVVMRefactoringCodeRefactoringProvider, que se define dentro del archivo CodeRefactoringProvider.cs (o .vb para Visual Basic). Cambie el nombre de la clase y del archivo por MakeViewModelBaseRefactoring y MakeViewModelBaseRefactoring.cs, respectivamente. Por motivos de claridad, quite los métodos ComputeRefactoringsAsync y ReverseTypeNameAsync generados automáticamente (el último se genera automáticamente con fines de demostración).

Investigación de un nodo de sintaxis

Como debe saber, el punto de entrada principal de una refactorización de código es el método ComputeRefactoringsAsync, que es el responsable de crear una acción rápida con el mismo nombre, que se conectará a la bombilla del editor de código, si el análisis de código de un nodo de sintaxis cumple las reglas necesarias. En este caso concreto, el método ComputeRefactoringsAsync debe detectar si el desarrollador invoca la bombilla sobre una declaración de clase. Con la ayuda de la ventana de la herramienta Visualizador de sintaxis, puede comprender fácilmente los elementos de la sintaxis con los que debe trabajar. Más específicamente, en C# debe detectar si el nodo de sintaxis es una clase ClassDeclaration, representada por un objeto de tipo Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax (consulte la Figura 5), mientras que en Visual Basic debe determinar si el nodo de sintaxis es una clase ClassStatement, representada por un objeto de tipo Microsoft.CodeAnalysis.VisualBasic.Syntax.ClassStatementSyntax. En realidad, en Visual Basic, la clase ClassStatement es un nodo secundario de ClassBlock, que representa el código completo de una clase. El motivo por el que C# y Visual Basic tienen objetos diferentes es la manera en que cada uno representa una definición de clase: C# usa la palabra clave Class con llaves como delimitadores y Visual Basic usa la palabra clave Class con la instrucción End Class como delimitador.

Explicación de una declaración de clase
Figura 5 Explicación de una declaración de clase

Creación de una acción

La primera refactorización de código que trataré está relacionada con la clase ViewModelBase. El primer paso es escribir el método ComputeRefactoringsAsync en la clase MakeViewModelBaseRefactoring. Con este método, se comprueba si el nodo de sintaxis representa una declaración de clase; en ese caso, puede crear y registrar una acción que estará disponible en la bombilla. En la Figura 6a se muestra como conseguirlo en C#, y en la Figura 6b se muestra el código de Visual Basic (vea los comentarios en línea).

Figura 6a Punto de entrada principal: 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 Punto de entrada principal: 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

Con este código, ha registrado una acción que puede invocarse en el nodo de sintaxis si se trata de una declaración de clase. La acción la lleva a cabo el método MakeViewModelBaseAsync, que implementa la lógica de refactorización y proporciona una clase totalmente nueva.

Generación de código

Roslyn no solo proporciona un método estructurado orientado a objetos para representar el código de origen, sino que también permite analizar texto de origen y generar un árbol de sintaxis con toda fidelidad. Para generar un nuevo árbol de sintaxis a partir de texto puro, debe invocar el método SyntaxFactory.ParseSyntaxTree. Este toma un argumento de tipo System.String que contiene el código fuente a partir del cual quiere generar una clase SyntaxTree.

Roslyn también ofrece los métodos VisualBasicSyntaxTree.ParseText y CSharpSyntaxTree.ParseText para conseguir el mismo resultado; no obstante, en este caso tiene sentido usar el método SyntaxFactory.ParseSyntaxTree, porque el código invoca otros métodos Parse desde SyntaxFactory, como podrá ver en breve.

Cuando tenga una nueva instancia SyntaxTree, podrá realizar en esta el análisis de código y otras operaciones relacionadas con el código. Por ejemplo, podrá analizar el código fuente de toda una clase, generar un árbol de sintaxis a partir de esta, reemplazar el nodo de sintaxis en la clase y devolver una nueva clase. En el caso del patrón MVVM, las clases comunes tienen una estructura fija, por lo que el proceso de análisis de texto de origen y reemplazo de una definición de clase por una nueva es fácil y rápido. Al aprovechar lo que se conoce como literales de cadena de varias líneas, puede pegar una definición de clase completa en un objeto de tipo System.String y, después, obtener de esta una instancia SyntaxTree, recuperar la instancia SyntaxNode que corresponde a la definición de clase y reemplazar la clase original en el árbol por la nueva. En primer lugar, demostraré como hacerlo en relación con la clase ViewModelBase. De manera más específica, en la Figura 7a se muestra el código de C# y en la Figura 7b, el código de Visual Basic.

Figura 7a MakeViewModelBaseAsync: generación de un nuevo árbol de sintaxis a partir del texto de origen (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: generación de un nuevo árbol de sintaxis a partir del texto de origen (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

Dado que el tipo SyntaxFactory se usa muchas veces, podría considerar la posibilidad de realizar una importación estática y, así, simplificar su código mediante la adición de una directiva Imports Microsoft.CodeAnalisys.VisualBasic.SyntaxFactory en Visual Basic y una directiva using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory en C#. Aquí no existe ninguna importación estática para facilitar la detección de algunos de los métodos que ofrece SyntaxFactory.

Observe que el método MakeViewModelBaseAsync toma tres argumentos:

  • Document, que representa el archivo de código fuente actual.
  • ClassDeclarationSyntax (ClassStatementSyntax en Visual Basic), que representa la declaración de clase sobre la que se ejecuta el análisis de código.
  • CancellationToken, que se usa en caso de que sea necesario cancelar la operación.

El código invoca primero el método SyntaxFactory.ParseSyntaxTree para obtener una nueva instancia SyntaxTree basada en el texto de origen que representa la clase ViewModelBase. La invocación de GetRoot es necesaria para obtener la instancia raíz SyntaxNode para el árbol de sintaxis. En este escenario concreto, sabe de antemano que el texto de origen analizado solo tiene una definición de clase, de modo que el código invoca FirstOrDefault<T> sobre OfType<T> para recuperar el nodo descendiente del tipo necesario, que es ClassDeclarationSyntax en C# y ClassBlockSyntax en Visual Basic. En este punto, debe reemplazar la definición de clase original por la clase ViewModelBase. Para hacerlo, el código invoca primero el método Document.GetSyntaxRootAsync para recuperar de forma asincrónica el nodo raíz del árbol de sintaxis del documento y, después, invoca el método ReplaceNode para reemplazar la definición de clase antigua por la nueva clase ViewModelBase. Observe cómo el código detecta si existe una directiva using (C#) o Imports (Visual Basic) para el espacio de nombres System.ComponentModel. Para ello, investigue las colecciones CompilationUnitSyntax.Usings y CompilationUnitSyntax.Imports, respectivamente. De lo contrario, se agrega una directiva adecuada. Resulta útil para agregar una directiva en el nivel del archivo de código si aún no está disponible.

Recuerde que, en Roslyn, los objetos son inmutables. Este es el mismo concepto que se aplica a la clase String: En realidad, nunca modifica una cadena, por lo que al editar una cadena o invocar métodos como Replace, Trim o Substring, obtiene una nueva cadena con los cambios especificados. Por este motivo, cada vez que necesita editar un nodo de sintaxis, en realidad crea un nuevo nodo de sintaxis con las propiedades actualizadas.

En Visual Basic, el código también debe recuperar la clase ClassBlockSyntax principal del nodo de sintaxis actual, que es en su lugar del tipo ClassStatementSyntax. Esto es necesario para recuperar la instancia de la clase SyntaxNode actual que se va a reemplazar. Para proporcionar una implementación común de la clase RelayCommand<T> debe hacer exactamente lo mismo, pero debe agregar una nueva refactorización de código. Para hacerlo, haga clic con el botón derecho en el nombre del proyecto en el Explorador de soluciones y seleccione Agregar | Nuevo elemento. En el cuadro de diálogo Agregar nuevo elemento, seleccione la plantilla Refactoring y asigne al nuevo archivo el nombre MakeRelayCommandRefactoring.cs (o .vb para Visual Basic). La lógica de refactorización es la misma que para la clase ViewModelBase class (por supuesto, con un texto de origen diferente). En la Figura 8a se muestra el código de C# completo para la nueva refactorización, que incluye los métodos ComputeRefactoringsAsync y MakeRelayCommandAsync, y en la Figura 8b se muestra el código de Visual Basic.

Figura 8a Refactorización de código que implementa la clase 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 Refactorización de código que implementa la clase 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

Ha completado correctamente dos refactorizaciones personalizadas y ahora conoce los conceptos básicos para implementar otras refactorizaciones, según su implementación del patrón MVVM (como un agente de mensajes, un localizador de servicios y clases de servicio).

Como alternativa, puede usar la clase SyntaxGenerator. Ofrece interfaces API escépticas del lenguaje, lo que significa que el código que escribe tiene como resultado una refactorización orientada tanto a Visual Basic como a C#. No obstante, este enfoque requiere la generación de todos los elementos de sintaxis del texto de origen. SyntaxFactory.ParseSyntaxTree permite analizar cualquier texto de origen. Resulta especialmente útil si escribe herramientas de desarrollo que deben manipular texto de origen que no conoce de antemano.

Disponibilidad: Aplicaciones para UWP y WPF

En lugar de poner las refactorizaciones personalizadas a disposición de todo el mundo, es lógico restringir su disponibilidad en la bombilla solo a aquellas plataformas con las que realmente use MVVM, como WPF y UWP. En el método ComputeRefactoringsAsync, puede obtener una instancia del modelo semántico del documento actual y, después, invocar el método GetTypeByMetadataName desde la propiedad Compilation. Por ejemplo, en el código siguiente se demuestra cómo hacer que la refactorización esté disponible solo para las aplicaciones para 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);
  // ...
}

Dado que el tipo Windows.UI.Xaml.AdaptiveTrigger existe solo en las aplicaciones para UWP, la refactorización estará disponible si el motor de análisis de código detecta que se ha hecho referencia a este tipo. Si quisiera que la refactorización estuviese disponible para WPF, podría escribir la comprobación siguiente:

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

De manera similar, el tipo System.Windows.Navigation.JournalEntry existe de manera exclusiva en WPF, por lo que un resultado no nulo de GetTypeByMetadataName indica que el análisis de código se ejecuta en un proyecto de WPF. Por supuesto, puede combinar ambas comprobaciones para que las refactorizaciones estén disponibles en ambas plataformas.

Pruebas del código

Puede probar el trabajo que ha realizado hasta el momento en la instancia experimental de Visual Studio. Para ello, presiones F5. Por ejemplo, cree un proyecto de WPF y agregue esta clase simple:

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

Haga clic con el botón derecho en la declaración de clase y, después, seleccione Acciones rápidas en el menú contextual. En este punto, la bombilla muestra las dos nuevas refactorizaciones según lo previsto y proporciona la sugerencia adecuada (véase la Figura 3 como referencia).

Si quiere publicar refactorizaciones personalizadas, la plantilla del proyecto Code Refactoring (VSIX) automatiza la generación de un paquete VSIX que puede publicarse en la Galería de Visual Studio. Si prefiere publicar su trabajo como un paquete NuGet, el truco consiste en crear un proyecto Analyzer with Code Fix y, luego, agregar plantillas de elemento Code Fix.

Generación de una clase ViewModel personalizada con Roslyn

Si se pregunta por qué el uso de Roslyn puede ser un enfoque mejor simplemente para agregar texto estático a una clase, imagine que quiere automatizar la generación de una clase ViewModel a partir de una clase empresarial, que es el modelo. En este caso, puede generar una nueva clase ViewModel y agregar las propiedades necesarias según los datos expuestos por el modelo. Para que se haga una idea, en la Figura 9 se muestra cómo producir una refactorización denominada clase Make ViewModel, que demuestra cómo crear una versión simplificada de una clase ViewModel.

Figura 9 Generación de una clase ViewModel basada en un 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;
  }

Este código genera una clase ViewModel, que expone un objeto ObservableCollection del tipo de modelo, además de un constructor vacío donde debería implementar su lógica, como se muestra en la Figura 10. Por supuesto, este código debe ampliarse con los miembros adicionales que pueda necesitar, como propiedades de datos y comandos, y debe mejorarse con un algoritmo de pluralización más eficaz.

Automatización de la generación de una clase ViewModel
Figura 10 Automatización de la generación de una clase ViewModel

Aprenda de Microsoft: refactorización de la interfaz INotifyPropertyChanged

Una de las tareas más repetitivas que lleva a cabo con MVVM es implementar la notificación de cambios a las clases del modelo de datos a través de la interfaz System.ComponentModel.INotifyPropertyChanged. Por ejemplo, si tiene la siguiente clase Customer:

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

Debería implementar la interfaz INotifyPropertyChanged para que los objetos enlazados reciban la notificación de cualquier cambio en los datos:

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 puede imaginar, en un modelo de datos formado por muchas clases con decenas de propiedades, esta tarea puede requerir mucho tiempo. Entre los ejemplos complementarios de Roslyn, Microsoft envió una refactorización de código que automatiza la implementación de la interfaz INotifyPropertyChanged con un simple clic. Se denomina ImplementNotifyPropertyChanged y está disponible para C# y Visual Basic en la subcarpeta Src/Samples del repositorio de Roslyn en github.com/dotnet/roslyn. Si compila y prueba el ejemplo, verá lo rápido y eficaz que resulta implementar la interfaz INotifyPropertyChanged, como se muestra en la Figura 11.

Implementación de la interfaz INotifyPropertyChanged
Figura 11 Implementación de la interfaz INotifyPropertyChanged

Este ejemplo resulta especialmente útil porque muestra cómo usar las API de Roslyn para revisar una definición de objeto, cómo analizar miembros específicos y cómo editar las propiedades existentes sin tener que proporcionar una definición de clase completamente nueva. Se recomienda encarecidamente estudiar el código fuente de este ejemplo para comprender escenarios de generación de código más complejos.

Resumen

Entre su cantidad prácticamente infinita de usos posibles, Roslyn también facilita de manera increíble la compatibilidad con el patrón Model-View-ViewModel. Como he mostrado en este artículo, puede aprovechar las API de Roslyn para analizar el código fuente de determinadas clases necesarias en cualquier implementación de MVVM, como ViewModelBase y RelayCommand<T>, así como para generar un nuevo nodo de sintaxis que pueda reemplazar una definición de clase existente. Visual Studio 2015 mostrará una vista previa en la bombilla, lo que proporcionará otra experiencia de codificación sorprendente.


Alessandro del Solees MVP de Microsoft desde 2008. Alessandro ha sido proclamado MVP del año en cinco ocasiones y es el autor de numerosos libros, eBooks, vídeos didácticos y artículos sobre desarrollo .NET con Visual Studio. Del Sole trabaja como experto en desarrollo de soluciones de Brain-Sys, especializado en las áreas de desarrollo .NET, aprendizaje y asesoramiento. Puede seguirlo en Twitter: @progalex.

Gracias a los siguientes expertos técnicos por revisar este artículo: Jason Bock (Magenic) y Anthony Green (Microsoft)

Jason Bock es director de prácticas de Magenic (magenic.com), además de autor y orador. Visite su sitio web en jasonbock.net.