2016 年 5 月

第 31 卷,第 5 期

本文章是由機器翻譯。

.NET 編譯器平台 - 使用 Roslyn 讓您的 Model-View-ViewModel 體驗達到最佳

Alessandro Del Del

Model View ViewModel (MVVM) 是一種非常受歡迎的架構模式,完全使用 XAML 應用程式平台等 Windows Presentation Foundation (WPF) 和通用 Windows 平台 (UWP)。架構使用 MVVM 應用程式提供,還有許多其他,清楚地分隔資料、 應用程式邏輯和 UI 的優點。這讓您更輕鬆地維護和測試應用程式,可改善程式碼重複使用,並讓設計工具,可用於 UI,而不需要互動的邏輯或資料。多年來,可協助開發人員更輕鬆且有效率地實作 MVVM 已經建置程式庫、 專案範本和架構,例如 Prism 和 MVVM Light Toolkit,數字。不過,在某些情況下,您不能依賴外部程式庫,或只是要能夠快速實作模式,同時保留您的程式碼上的焦點。雖然有很多種 MVVM 實作 (implementation),大部分會分享 Roslyn Api 其產生可以輕鬆地自動化常用的物件數目。在本文中,我將解釋如何建立自訂的 Roslyn 重整功能可讓您輕鬆地產生每個 MVVM 實作通用的項目。因為提供完整的摘要需 MVVM 不可能在此,我假設您已經有基本知識的 MVVM 模式、 相關的術語及 Roslyn 程式碼分析 Api。如果您需要重新整理程式,您可以閱讀下列文章 ︰ 「 模式 — Model View ViewModel 與 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 產生專案時,它會自動加入名為 MVVMRefactoringCodeRefactoringProvider,CodeRefactoringProvider.cs (或 Visual Basic 為.vb) 內定義的類別檔案。類別和檔案 MakeViewModelBaseRefactoring 和 MakeViewModelBaseRefactoring.cs,將分別重新命名。為了清楚起見,移除這兩種自動產生 ComputeRefactoringsAsync 和 ReverseTypeNameAsync 方法 (後者是自動產生供示範之用)。

調查語法節點

您可能已經知道,程式碼重整的主要進入點是 ComputeRefactoringsAsync 方法,也就是負責建立所謂的快速動作,並插入程式碼編輯器的燈泡,如果程式碼分析的語法節點滿足需要的規則。在此案例中,ComputeRefactoringsAsync 方法都必須偵測如果開發人員叫用燈泡透過在類別宣告。語法視覺化檢視工具視窗的說明,請使用您可以輕鬆地了解您要使用的語法元素。更具體來說,在 C# 中您必須偵測語法節點是否為 ClassDeclaration,由 Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax 型別的物件 (請參閱 [圖 5),而在 Visual Basic 中您判斷 ClassStatement 語法節點時,由 Microsoft.CodeAnalysis.VisualBasic.Syntax.ClassStatementSyntax 型別的物件。事實上,在 Visual Basic 中 ClassStatement 是 ClassBlock,代表完整的程式碼類別的子節點。C# 和 Visual Basic 有不同的物件的原因是因為每一個都代表在類別定義的方式 ︰ C# 會 class 關鍵字使用大括號做為分隔符號,以及 Visual Basic 使用 Class 關鍵字做為分隔符號的 End Class 陳述式。

了解在類別宣告
[圖 5 了解在類別宣告

建立動作

第一個程式碼重構我將討論與 ViewModelBase 類別。第一個步驟是撰寫 ComputeRefactoringsAsync 方法 MakeViewModelBaseRefactoring 類別中。使用此方法,您檢查語法的節點是否表示類別宣告。如果是的話,您可以建立及註冊可在燈泡的動作。圖 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 型別會使用許多次,您可能考慮進行靜態匯入,而且因此,可簡化程式碼加入匯入 Microsoft.CodeAnalisys.VisualBasic.SyntaxFactory 指示詞,在 Visual Basic 和使用 C# 中的靜態 Microsoft.CodeAnalysis.CSharp.SyntaxFactory 指示詞。以方便探索一些 SyntaxFactory 提供的方法沒有任何靜態的匯入。

請注意 MakeViewModelBaseAsync 方法會採用三個引數 ︰

  • 文件,表示目前原始程式碼檔
  • ClassDeclarationSyntax (在 Visual Basic 中,它是 ClassStatementSyntax),表示執行的程式碼分析的類別宣告
  • 萬一您必須取消作業使用 CancellationToken

程式碼首先會叫用 SyntaxFactory.ParseSyntaxTree 以取得新的公司 SyntaxTree 執行個體表示的 ViewModelBase 類別的來源文字。GetRoot 的引動過程,才能取得語法樹狀目錄根 SyntaxNode 執行個體。在這個特定案例中,您事先知道,剖析的原始程式文字的只有一個類別定義,讓程式碼會叫用 FirstOrDefault < T > 高於 OfType 擷取所需的型別,也就是 ClassDeclarationSyntax 的一個子系節點的 < T > C# 和 Visual Basic 中的 ClassBlockSyntax。此時,您需要使用 ViewModelBase 類別取代原始的類別定義。若要完成這項作業,程式碼首先會叫用 Document.GetSyntaxRootAsync 以非同步方式擷取文件的語法樹狀目錄中的根節點,然後它會叫用 ReplaceNode 新 ViewModelBase 類別以取代舊的類別定義。請注意,如果使用 (C#) 程式碼偵測到有或如何匯入 (Visual Basic) 指示詞 System.ComponentModel 命名空間分別調查 CompilationUnitSyntax.Usings CompilationUnitSyntax.Imports 集合。如果沒有,則會加入適當的指示詞。這可用於加入程式碼檔案層級指示詞,如果已存在。

請記住,在 Roslyn,物件是固定不變。這是套用至字串類別的概念相同 ︰ 您其實永遠不會修改字串,因此當您編輯的字串,或叫用方法取代修整或子字串,取得新字串,指定的變更。基於這個理由,每當您需要編輯語法節點,您實際上節點建立新的語法與更新的內容。

在 Visual Basic 程式碼也必須擷取目前的語法節點,而不是類型 ClassStatementSyntax 是上層 ClassBlockSyntax。這被必要擷取執行個體將會取代實際 SyntaxNode。提供通用的 RelayCommand < T > 類別實作的運作方式完全相同,但您需要加入新的程式碼重構。若要完成這項作業,以滑鼠右鍵按一下 [方案總管] 中的專案名稱,然後選取 [新增 |新項目。在加入新項目] 對話方塊中,選取 [重整] 範本並將新檔案 MakeRelayCommandRefactoring.cs (或.vb,Visual basic)。重構的邏輯是 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 方法中,您可以取得目前文件的語意模型的執行個體,並接著叫用 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 的非 null 結果表示 WPF 專案中,執行程式碼分析。當然,您可以將合併這兩個檢查,以提供兩種平台的重整作業。

測試程式碼

您可以測試已完成的工作到目前為止在 Visual Studio 的實驗執行個體中按下 F5。例如,建立 WPF 專案,並新增這個非常簡單的類別 ︰

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

類別宣告中,以滑鼠右鍵按一下,然後從內容功能表中選取 [快速動作。此時,燈泡顯示兩個新的重整作業,如預期,並提供適當的建議 (請參閱 [圖 3 參考)。

如果您想要發行自訂的重整作業,程式碼重構 (VSIX) 專案範本會自動產生的 VSIX 套件,才能發行至 Visual Studio 組件庫。而是會發佈您的 NuGet 封裝的工作,如果訣竅是修正程式碼專案中建立分析器,然後加入程式碼修正的項目範本。

產生自訂 ViewModel 與 Roslyn

如果您想知道為什麼使用 Roslyn 可能會更好的方法,只是為了將靜態文字加入至類別,,假設您想要自動產生的 ViewModel 從商務的類別,也就是模型。在此情況下,您可以產生新的 ViewModel 類別,並加入必要的屬性,根據模型所公開的資料。只是為了讓您了解, [圖 9 示範如何產生重整呼叫進行 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 介面在資料模型中實作變更通知類別。比方說,如果您有下列的 「 客戶 」 類別 ︰

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,適用於 C# 和 Visual Basic 中 Src/Roslyn 儲存機制,在 github.com/dotnet/roslyn。如果您編譯並測試範例,您會看到速度並有效率得實作 INotifyPropertyChanged 介面,如所示 [圖 11

實作 INotifyPropertyChanged 介面
[圖 11 實作 INotifyPropertyChanged 介面

這個範例是特別有用,因為它會顯示如何使用 Roslyn Api 來逐步解說物件定義、 剖析特定成員以及對現有的內容中進行編輯,而不需要提供全新的類別定義。若要了解更複雜的程式碼產生案例,建議您明確地研究這個範例的原始程式碼。

總結

之間可能會使用其幾乎無限數目,Roslyn 也使得支援 Model View ViewModel 模式非常簡單。如我在本文中所示範的您可以利用 Roslyn Api 來剖析 MVVM 實作,例如 ViewModelBase 和 RelayCommand < T > 中所需的特定類別的原始程式碼,並產生新的語法節點可取代現有的類別定義。與 Visual Studio 2015 會顯示預覽中的燈泡,另一個令人讚嘆的程式碼撰寫體驗。


Alessandro del Sole2008年之後已經是 Microsoft MVP。獲得一年五倍的 MVP,他著有許多書籍、 電子書,說明影片和使用 Visual Studio.NET 開發相關的文件。Del Sole 大腦 Sys 擔任方案開發人員方面的專家 (大腦 sys.it)、.NET 開發訓練和諮詢焦。您也可以關注他的 Twitter: @progalex

謝謝 閱本篇文章的下列技術專家 ︰ Jason Bock (Magenic)、 Anthony 綠色 (Microsoft)

Jason Bock 是 Magenic 做法會導致 (magenic.com),作者和講師。請造訪他的網站 jasonbock.net