使用 MVVM Toolkit Source Generators

關於 MVVM Toolkit

最近 .NET Community Toolkit 發佈了 8.0.0 preview1,它包含了從 Windows Community Toolkit 遷移過來的以下組件:

CommunityToolkit.Common
CommunityToolkit.Mvvm
CommunityToolkit.Diagnostics
CommunityToolkit.HighPerformance

其中 CommunityToolkit.Mvvm 又名 MVVM Toolkit ,它是一個現代化、快速以及模塊化的 MVVM 庫。Nuget 安裝腳本為:

Install-Package CommunityToolkit.Mvvm -Version 8.0.0-preview1

MVVM Toolkit source generators

Source Generators 是一項 C# 編譯器功能,使 C# 開發人員能夠在編譯用戶代碼時進行檢查,並動態生成新的 C# 源文件,以添加到用戶的編譯中。 通過這種方式,你的代碼可以在編譯過程中運行並檢查你的程序以生成與其餘代碼一起編譯的其他源文件。

新版本的 MVVM Toolkit 包含一個全新的 source generators,現在它是一個增量生成器,性能將會提升很多。這篇文章將簡單介紹一下它的功能。

命令

在 MVVM 模式中,命令的寫法讓人有點煩惱。這是 MVVM Toolkit 中的通常寫法:

private IRelayCommand _displayCommand;

IRelayCommand DisplayCommand => _displayCommand ??= new RelayCommand(new Action(Display), () => HasName);

private void Display()
{

}

首先,代碼就不少。另外,_displayCommandDisplayCommandDisplay() 是寫在一起好呢,還是按字段、屬性、函數的排序分別放在代碼里的不同位置呢?又或者索性用 Partial 類分別放在不同的文件?

用 source generators 就沒這些煩惱了,命令的定義可以簡化成這樣:

[ICommand(CanExecute = nameof(HasName))]
private void Display()
{
}

通過添加 ICommandAttribute,source generators 可以根據 Display() 這個函數名正確地生成 DisplayCommand 及對應的初始化代碼。此外,還可以通過它的 CanExecute 屬性指定將 ICommand 的 CanExecute 關聯到對應的屬性。

屬性

屬性也有和命令一樣的煩惱,通常來說 MVVM 模式中的屬性的寫法如下:

private string name;

public string Name
{
    get => name;
    set => SetProperty(ref name, value);
}

其實還好,不會太多。但如果是這樣呢:

 private string _surname;

 public string Surname
 {
     get
     {
         return _surname;
     }
     set
     {
         if (!EqualityComparer<string>.Default.Equals(_surnam
         {
             _surname = value;
             OnPropertyChanged();
             OnPropertyChanged(nameof(FullName));
             OnPropertyChanged(nameof(HasName));
             DisplayCommand.NotifyCanExecuteChanged();
         }
     }
 }

 public string FullName => $"{Name} {Surname}";

 public bool HasName => !string.IsNullOrWhiteSpace(FullName);

這時候 source generators 的作用就可以很明顯,因為它只需要下面的代碼就可以自動產生與上面等價的代碼:

[ObservableProperty]
[AlsoNotifyChangeFor(nameof(FullName), nameof(HasName))]
[AlsoNotifyCanExecuteFor(nameof(DisplayCommand))]
private string _surname;

public string FullName => $"{Name} {Surname}";

public bool HasName => !string.IsNullOrWhiteSpace(FullName);

從這段代碼可以看到有三個 Attribute 起了作用:

ObservableProperty:自動為 _name 屬性生成對應的屬性。
AlsoNotifyChangeFor:屬性值修改時同時觸發 FullNameHasName 這兩個屬性的 PropertyChanged 事件。
AlsoNotifyCanExecuteFor:屬性值修改時同時通知 DisplayCommand 執行它的 NotifyCanExecuteChanged()

注入到現有類

一般來說,MVVM Toolkit source generators 需要在 ObservableObject 的派生類中使用,例如:

public partial class TestModel: ObservableObject

但如果你的類已經繼承了其它類,MVVM Toolk source generators 也允許你使用它的功能,方法是添加上 INotifyPropertyChangedAttribute,代碼如下:

[INotifyPropertyChanged]
public partial class TestModel: Behaviour

INotifyPropertyChangedAttribute 會自動生成實現 INotifyPropertyChanged 的代碼,而無需更改基類。不過遺憾的是,INotifyPropertyChangedAttribute 目前只能在未實現 INotifyPropertyChanged 接口的類中使用,即下面這種代碼不能編譯通過:

[INotifyPropertyChanged]
public partial class TestModel: ObservableObject

成果

使用了 source generators 可以大幅減少代碼,下面這圖直觀展示了減少的代碼量。

如果需要查看自動生成的代碼,可以在分析器的 CommunityToolkit.Mvvm.SourceGenerators 節點裏找到:

一些小問題

MVVM Toolkit source generators 可以重構你的代碼,但代價是什麼?

首先,部分功能需要 C# 8.0 以上,所以編譯時可能會看到這條錯誤:

The source generator features from the MVVM Toolkit require consuming projects to set the C# language version to at least C# 8.0

解決方法是在項目文件的 PropertyGroup 節點裏添加這段指明 C# 的版本:

<LangVersion>9.0</LangVersion>

另外,MVVM Toolkit source generators 還需要 Visual Studio 2022 才可以使用。

還有一點,我還沒找到為生成的屬性添加註釋的方法,這對一些難以理解的屬性來說十分致命,只好用回傳統方法來處理這種屬性。

最後,沒有 CodeLens,沒法直觀看到屬性的引用、修改等信息,用起來不是很順手。

最後

從上面的例子來看,無論從代碼量、可維護性、可閱讀性來看,source generators 都有巨大的優勢,但在現階段,MVVM Toolkit source generators 用起來還是有不少小問題,不能完全代替原生寫法。不過這是個很符合 80/20 原則的工具:它可以讓用戶用 20% 的投入解決了 80% 的問題。

其它更多的內容,請參考 Github 或其它文檔:

//github.com/CommunityToolkit/dotnet

//github.com/CommunityToolkit/dotnet/releases/tag/v8.0.0-preview1

//docs.microsoft.com/zh-cn/windows/communitytoolkit/mvvm/introduction

Tags: