2016 年 5 月

第 31 卷,第 5 期

.NET 编译器平台 - 使用 Roslyn 全面体验“模型-视图-视图模型”

作者 Alessandro Del Del

模型-视图-视图模型 (MVVM) 是一个非常受欢迎的结构模式,与 XAML 应用程序平台(如 Windows Presentation Foundation (WPF) 和通用 Windows 平台 (UWP))配合使用效果绝佳。首先,使用 MVVM 构建应用程序能够在数据、应用程序逻辑和 UI 之间实现清晰分离。这使应用程序更易于维护和测试,提高了代码的重复使用,使设计人员能够对 UI 进行操作,而无需与逻辑或数据进行交互。多年来,已构建了许多库、项目模板和框架(如 Prism 和 MVVM Light Toolkit)用于帮助开发人员更轻松有效地实现 MVVM。然而,在某些情况下,你不能依赖于外部库,或者你可能只是想要在专注于你的代码的同时能够快速实现此模式。虽然 MVVM 有多种实现方式,但大多数都共享一些可通过 Roslyn API 自动生成的公用对象。在本文中,我将解释如何创建自定义 Roslyn 重构,从而轻松地生成可通用于每个 MVVM 实现的元素。因为此处不可能为你提供有关 MVVM 的完整摘要,所以我假设你已经对 MVVM 模式、相关术语和 Roslyn 代码分析 API 有了基本的了解。如果你需要复习,可以阅读以下文章: “模式 - 使用‘模型-视图-视图模型’设计模式构建的 WPF 应用”、“C# 和 Visual Basic: 使用 Roslyn 编写 API 的实时代码分析器” 和“C# - 将代码修补程序添加到 Roslyn 分析器”。

随附的代码可用于 C# 和 Visual Basic 版本。文章中的该版本包括 C# 和 Visual Basic 列表。

通用 MVVM 类

任何典型的 MVVM 实现都需至少具备以下类(在一些情况下名称会稍有不同,具体取决于你所应用的 MVVM 风格):

ViewModelBase - 一个基本的抽象类,反映通用于应用程序中每个 ViewModel 的成员。通用成员可以根据应用程序的体系结构发生相应的改变,但其最基本的实现是为任何派生 ViewModel 提供更改通知。

RelayCommand - 一个表示命令的类,通过它,ViewModels 可以调用方法。RelayCommand 通常有两种风格,分别为:通用和非通用。本文将使用通用风格 (RelayCommand<T>)。

我假设你已经熟悉了这两种风格,所以本文不再赘述。图 1a 表示 ViewModelBase 的相关 C# 代码,图 1b 显示 Visual Basic 代码。

图 1a 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));
  }
}

图 1b 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

这是 ViewModelBase 的最基本的实现;它只提供基于 INotifyPropertyChanged 接口的属性更改通知。当然,你可能会根据自己的具体需求添加更多的成员。图 2a 显示 RelayCommand<T> 的相关 C# 代码,图 2b 显示 Visual Basic 代码。

图 2a 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);
  }
}

图 2b 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

这是 RelayCommand<T> 最常见的实现,且适用于大多数 MVVM 方案。值得一提的是,这个类实现了 System.Windows.Input.ICommand 接口,该接口需要实现一个名为 CanExecute 的方法,目标是告诉调用者某个命令是否可执行。

Roslyn 如何使你的生活简单化

如果你不使用外部框架,Roslyn 可以说是一个真正的生活助手: 你可以创建自定义代码重构,用于替换类定义并自动实现所需的对象,还可以根据模型属性轻松地自动实现 ViewModel 类的生成。图 3 举例说明了在文章的最后你将有何收获。

通过自定义 Roslyn 重构实现 MVVM 对象
图 3 通过自定义 Roslyn 重构实现 MVVM 对象

这种方法的好处是,你可以始终将注意力放在代码编辑器上,并且非常快速地实现所需的对象。此外,如文章后面提供的演示,你可以根据模型类生成自定义 ViewModel。让我们从创建重构项目开始。

创建适用于 Roslyn 重构的项目

第一步是创建一个新的 Roslyn 重构。为此,你可以使用代码重构 (VSIX) 项目模板,它位于你在“新建项目”对话框中所选语言下的扩展节点中。调用新项目 MVVM_Refactoring,如图 4 中所示。

创建 Roslyn 重构项目
图 4 创建 Roslyn 重构项目

准备好之后,单击“确定”。当 Visual Studio 2015 生成该项目时,会自动添加一个在 CodeRefactoringProvider.cs(或 Visual Basic 的 .vb)文件中定义的名为 MVVMRefactoringCodeRefactoringProvider 的类。分别将该类和文件重命名为 MakeViewModelBaseRefactoring 和 MakeViewModelBaseRefactoring.cs。为了清楚起见,同时删除自动生成的 ComputeRefactoringsAsync 和 ReverseTypeNameAsync 方法(后者是为了演示而自动生成的)。

研究语法节点

正如你可能知道的,代码重构的主入口点是 ComputeRefactoringsAsync 方法,如果语法节点的代码分析满足所需的规则,则该方法负责创建一个插入到代码编辑器灯泡中的所谓的快速操作。在这种特殊情况下,ComputeRefactoringsAsync 方法必须检测开发人员是否正在通过类声明调用灯泡。在语法可视化工具窗口的帮助下,你可以很容易地了解你需要使用的语法元素。更具体地说,在 C# 中,你必须检测语法节点是否是 Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax 类型对象所表示的 ClassDeclaration(见图 5),而在 Visual Basic 中,你要确定语法节点是否是Microsoft.CodeAnalysis.VisualBasic.Syntax.ClassStatementSyntax 类型对象所表示的 ClassStatement。实际上,在 Visual Basic 中,ClassStatement 是 ClassBlock 的子节点,它表示某一类的整个代码。C# 和 Visual Basic 会有不同的对象是因为它们表示类定义的方式有所不同: C# 使用“class”关键字并将大括号作为分隔符,而 Visual Basic 使用“Class”关键字并将 End Class 语句作为分隔符。

理解类声明
图 5 理解类声明

创建操作

我将讨论的第一个代码重构涉及 ViewModelBase 类。第一步是在 MakeViewModelBaseRefactoring 类中编写 ComputeRefactoringsAsync 方法。使用此方法,你可以检查语法节点是否表示类声明;如果是的话,你可以创建并注册可在灯泡中使用的操作。图 6a 演示如何在 C# 中完成此操作,图 6b 显示 Visual Basic 代码(请参阅内联注释)。

图 6a 主入口点: 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);
}

图 6b 主入口点: 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

如果这是一个类声明,通过此代码,你已经注册了可以在语法节点上调用的操作。该操作由 MakeViewModelBaseAsync 方法执行,可实现重构逻辑,并提供一种全新的类。

代码生成

Roslyn 不仅提供了一个面向对象的结构化的方式来表示源代码,还允许分析源文本和生成具有全保真度的语法树。为了从纯文本生成新语法树,你需要调用 SyntaxFactory.ParseSyntaxTree 方法。它使用一个包含源代码(你要在其中生成 SyntaxTree)的 System.String 类型参数。

Roslyn 还提供 VisualBasicSyntaxTree.ParseText 和 CSharpSyntaxTree.ParseText 方法来实现相同的结果;然而,在这种情况下,使用 SyntaxFactory.ParseSyntaxTree 是有意义的,因为代码从 SyntaxFactory 调用其他分析方法,这一点你很快就会看到。

在你拥有新的 SyntaxTree 实例后,可以对它执行代码分析以及其他与代码相关的操作。例如,你可以分析整个类的源代码,从中生成语法树,替换类中的语法节点,并返回一个新的类。在使用 MVVM 模式的情况下,由于公共类具有固定的结构,所以分析源文本并用新的类定义去替换某个类定义的过程会非常快捷和容易。通过利用所谓的多行字符串文本,你可以将整个类定义粘贴到 System.String 类型对象中,然后从中获取 SyntaxTree,检索对应于类定义的 SyntaxNode 并使用新类替换树中原来的类。我将首先演示如何对 ViewModelBase 类完成此操作。更具体地说,图 7a 显示 C# 的代码,图 7b 显示 Visual Basic 的代码。

图 7a MakeViewModelBaseAsync: 从源文本 (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;
}

图 7b MakeViewModelBaseAsync: 从源文本 (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

由于 SyntaxFactory 类型可多次使用,所以你可以考虑执行静态导入,这样,通过在 Visual Basic 中添加 Imports Microsoft.CodeAnalisys.VisualBasic.SyntaxFactory 指令并在 C# 中使用静态 Microsoft.CodeAnalysis.CSharp.SyntaxFactory 指令即可简化代码。此处没有任何静态导入能够更容易地发现 SyntaxFactory 提供的方法。

请注意,MakeViewModelBaseAsync 方法有三个参数:

  • Document,它表示当前的源代码文件
  • ClassDeclarationSyntax(在 Visual Basic 中,则为 ClassStatementSyntax),它表示执行代码分析所采用的类声明
  • CancellationToken,它在必须取消操作的情况下使用

代码首先根据表示 ViewModelBase 类的源文本,调用 SyntaxFactory.ParseSyntaxTree 来获取一个新的 SyntaxTree 实例。需要调用 GetRoot 来获取语法树的根 SyntaxNode 实例。在这种特殊情况下,你事先知道已分析的源文本只有一个类定义,所以代码会通过 OfType<T> 调用 FirstOrDefault<T> 来检索所需类型的后代节点,即在 C# 中为 ClassDeclarationSyntax,在 Visual Basic 中则为 ClassBlockSyntax。此时,你需要用 ViewModelBase 类来替换原来的类定义。为此,代码将首先调用 Document.GetSyntaxRootAsync 来异步检索文档语法树的根节点,然后调用 ReplaceNode 将旧的类定义替换为新的 ViewModelBase 类。注意代码如何通过分别研究 CompilationUnitSyntax.Usings 和 CompilationUnitSyntax.Imports 集合检测System.ComponentModel 命名空间是否存在 using (C#) 或 Imports (Visual Basic) 指令。如果不存在,则要添加适当的指令。如果尚不可用,那么在代码文件级添加指令的做法很有用。

请记住,在 Roslyn 中,对象是不可改变的。同样的概念也适用于 String 类: 事实上,你永远无法修改字符串,因此当你编辑字符串或调用诸如 Replace、Trim 或 Substring 之类的方法时,会得到一个包含特定更改的新的字符串。出于这个原因,每次你需要编辑语法节点时,实际上将创建带有更新属性的新的语法节点。

在 Visual Basic 中,代码也需要检索当前语法节点的可替代 ClassStatementSyntax 类型的父 ClassBlockSyntax。这是检索将被替换的 SyntaxNode 实例的必要步骤。提供 RelayCommand<T> 类的普通实现原理是一样的,但你需要添加一个新的代码重构。为此,在解决方案资源管理器中右键单击该项目名称,然后选择“添加 | 新项目”。在“添加新项目”对话框中,选择重构模板,并将新文件命名为 MakeRelayCommandRefactoring.cs(对于 Visual Basic 则为 .vb)。重构逻辑与 ViewModelBase 类是相同的(当然,源文本有所不同)。图 8a 显示新重构的全部 C# 代码,包括 ComputeRefactoringsAsync 和 MakeRelayCommandAsync 方法,图 8b 显示 Visual Basic 代码。

图 8a 实现 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;
  }
}

图 8b 实现 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

你已经成功完成了两个自定义重构操作,现在你已经掌握了实现其他重构的基础知识,具体取决于你的 MVVM 模式的实现方式(如消息代理、服务定位器和服务类)。

作为替代方案,你还可以使用 SyntaxGenerator 类。这可以提供与语言无关的 API,意味着你编写的代码会针对 Visual Basic 和 C# 实现重构。然而,这种方法需要生成每一个对应源文本的语法要素。通过使用 SyntaxFactory.ParseSyntaxTree,你可以分析任何源文本。如果你编写了需要处理你事先不知道的源文本的开发者工具,那么这种做法就特别有用。

可用性: UWP 应用和 WPF

与让自定义重构普遍可用相比,对于那些你在其上使用 MVVM 的平台(如 WPF 和 UWP)而言,将其限制为仅在灯泡中可用更有意义。在 ComputeRefactoringsAsync 方法中,你可以得到当前文档的语义模型的一个实例,然后从 Compilation 属性调用 GetTypeByMetadataName 方法。例如,以下代码演示如何让重构只对 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);
  // ...
}

因为 Windows.UI.Xaml.AdaptiveTrigger 类型只存在于 UWP 应用中,所以如果代码分析引擎检测到该类型已被引用时,重构将变为可用。如果你想让重构对 WPF 可用,则可以编写以下检查:

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

同样,System.Windows.Navigation.JournalEntry 唯一存在于 WPF 中,所以从 GetTypeByMetadataName 中得到非空结果意味着正在对 WPF 项目运行代码分析。当然,你可以结合使用两种检查,以使重构对这两种平台可用。

测试代码

你可以按 F5 测试你在 Visual Studio 实验实例中迄今所做的工作。例如,创建一个 WPF 项目并添加这个非常简单的类:

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

右键单击类的声明,然后从上下文菜单中选择“快速操作”。此时,灯泡按预期显示两个新的重构,并提供适当的建议(请参阅图 3)。

如果你想发布自定义重构,代码重构 (VSIX) 项目模板可自动生成能够发布到 Visual Studio 库中的 VSIX 包。如果你想将自己的作品发布为一个 NuGet 包,窍门是创建一个包含代码修复项目的分析器,然后添加代码修复项目模板。

通过 Roslyn 生成自定义 ViewModel

如果你想知道为什么使用 Roslyn 是将静态文本添加到类中的更好做法,可以想象你想要从业务类中自动生成 ViewModel 模型。在这种情况下,你可以生成一个新的 ViewModel 类,并基于该模型公开的数据添加所需的属性。这里提供一个仅作为参考的示例,图 9 显示如何创建一个名为 Make ViewModel 类的重构,它演示了如何创建 ViewModel 的简化版本。

图 9 生成基于模型的 ViewModel

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

此代码生成一个公开模型类型的 ObservableCollection 的 ViewModel 类,以及一个你应在其中应用逻辑的空的构造函数,如图 10 所示。当然,此代码应当通过你所需的任何其他成员(如数据属性和命令)进行扩展,并应通过更有效的复数算法得到改进。

自动生成 ViewModel 类
图 10 自动生成 ViewModel 类

从 Microsoft 学习: INotifyPropertyChanged 重构

你使用 MVVM 从事的一个重复性工作是,通过 System.ComponentModel.INotifyPropertyChanged 接口向数据模型中的类发出更改通知。举例来说,如果你有以下 Customer 类:

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

你应该实现 INotifyPropertyChanged,以便通知绑定对象对数据所做的任何更改:

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

你可以想像,在一个由许多包含几十个属性的类组成的数据模型中,执行此任务可能需要大量的时间。在 Roslyn 的随附示例中,Microsoft 提供了一个代码重构,只需单击一次,即可自动实现 INotifyPropertyChanged 接口。这就是所谓的 ImplementNotifyPropertyChanged,可用于 Roslyn 存储库(位于 github.com/dotnet/roslyn)的 Src/Samples 子文件夹中的 C# 和 Visual Basic。如果你编译和测试示例,将会看到实现 INotifyPropertyChanged 接口会有多么快速和高效,如图 11 所示。

实现 INotifyPropertyChanged 接口
图 11 实现 INotifyPropertyChanged 接口

此例子非常有用,因为它演示如何使用 Roslyn API 完成对象定义的整个过程、如何分析特定成员以及如何编辑现有属性,而无需提供一个全新的类定义。强烈推荐研究此示例的源代码,以了解更复杂的代码生成方案。

总结

在其几乎无限多个可能的用法中,Roslyn 可极为容易地实现对“模型-视图-视图模型”模式的支持。正如我在本文中所演示,你可以利用 Roslyn API 来分析任何 MVVM 实现所需的某些类的源代码,比如 ViewModelBase 和 RelayCommand<T>,并生成可替换现有类定义的新的语法节点。Visual Studio 2015 将在灯泡中显示预览,提供另一种新奇的编码体验。


Alessandro del Sole自 2008 年起被评为 Microsoft MVP。他已经 5 次获得年度 MVP 这一殊荣,发表过很多关于 Visual Studio .NET 开发的书籍、电子书、指导视频和文章。Del Sole 是 Brain-Sys 的解决方案开发专家,专注于 .NET 开发、培训和咨询。你可以关注他的 Twitter @progalex

衷心感谢以下技术专家对本文的审阅: Jason Bock (Magenic) 和 Anthony Green (Microsoft)

Jason Bock 是 Magenic (magenic.com) 的实践专家,也是作家和演说家。请访问他的网站 jasonbock.net