.NET 編譯器平臺:使用 Roslyn 體驗 MVVM

發表於2016-05-28

模型-檢視-檢視模型 (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 應用”(msdn.com/magazine/dd419663)、“C# 和 Visual Basic: 使用 Roslyn 編寫 API 的實時程式碼分析器”(msdn.com/magazine/dn879356) 和“C# – 將程式碼修補程式新增到 Roslyn 分析器”(msdn.com/magazine/dn904670)。

隨附的程式碼可用於 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#)

圖 1b ViewModelBase 類 (Visual Basic)

這是 ViewModelBase 的最基本的實現;它只提供基於 INotifyPropertyChanged 介面的屬性更改通知。當然,你可能會根據自己的具體需求新增更多的成員。圖 2a 顯示 RelayCommand<T> 的相關 C# 程式碼,圖 2b 顯示 Visual Basic 程式碼。

圖 2a RelayCommand<T> 類 (C#)

圖 2b RelayCommand(Of T) 類 (Visual Basic)

這是 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#)

圖 6b 主入口點: ComputeRefactoringsAsync 方法 (Visual Basic)

如果這是一個類宣告,通過此程式碼,你已經註冊了可以在語法節點上呼叫的操作。該操作由 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#) 生成新的語法樹

圖 7b MakeViewModelBaseAsync: 從源文字 (Visual Basic) 生成新的語法樹

由於 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 呼叫 FirstOrDefault 來檢索所需型別的後代節點,即在 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 類的普通實現原理是一樣的,但你需要新增一個新的程式碼重構。為此,在解決方案資源管理器中右鍵單擊該專案名稱,然後選擇“新增 | 新專案”。在“新增新專案”對話方塊中,選擇重構模板,並將新檔案命名為 MakeRelayCommandRefactoring.cs(對於 Visual Basic 則為 .vb)。重構邏輯與 ViewModelBase 類是相同的(當然,源文字有所不同)。

圖 8a 顯示新重構的全部 C# 程式碼,包括 ComputeRefactoringsAsync 和 MakeRelayCommandAsync 方法,圖 8b 顯示 Visual Basic 程式碼。
圖 8a 實現 RelayCommand 類的程式碼重構 (C#)

圖 8b 實現 RelayCommand(Of T) 類的程式碼重構 (Visual Basic)

你已經成功完成了兩個自定義重構操作,現在你已經掌握了實現其他重構的基礎知識,具體取決於你的 MVVM 模式的實現方式(如訊息代理、服務定位器和服務類)。

作為替代方案,你還可以使用 SyntaxGenerator 類。這可以提供與語言無關的 API,意味著你編寫的程式碼會針對 Visual Basic 和 C# 實現重構。然而,這種方法需要生成每一個對應源文字的語法要素。通過使用 SyntaxFactory.ParseSyntaxTree,你可以分析任何源文字。如果你編寫了需要處理你事先不知道的源文字的開發者工具,那麼這種做法就特別有用。

相關文章