解析 .NET MAUI 中 Microsoft.Toolkit.Mvvm 的運作方式
在以往進行 Xamarin.Forms 專案開發時期,通常會使用 Prism 開發框架來進行整體專案開發,這是因為 Prism 提供了相當豐富的功能來方便與簡化行動裝置應用程式的開發,然而對於 MVVM 的開發上,進行 ViewModel 類別設計過程中,並沒有使用到繼承 [BindableBase] 這個類別來施做,而是使用了 [PropertyChanged.Fody] 這個套件來進行設計。
底下會是 [BindableBase] 類別的程式碼
public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged ;
protected virtual bool SetProperty <T >(ref T storage , T value , [CallerMemberName ] string propertyName = null )
{
if (EqualityComparer <T >.Default .Equals (storage , value )) return false ;
storage = value ;
RaisePropertyChanged (propertyName );
return true ;
}
protected virtual bool SetProperty <T >(ref T storage , T value , Action onChanged , [CallerMemberName ] string propertyName = null )
{
if (EqualityComparer <T >.Default .Equals (storage , value )) return false ;
storage = value ;
onChanged ? .Invoke ();
RaisePropertyChanged (propertyName );
return true ;
}
protected void RaisePropertyChanged ([CallerMemberName ] string propertyName = null )
{
OnPropertyChanged (new PropertyChangedEventArgs (propertyName ));
}
protected virtual void OnPropertyChanged (PropertyChangedEventArgs args )
{
PropertyChanged ? .Invoke (this , args );
}
}從這個類別中可以看出,其實將會實作 [INotifyPropertyChanged] 介面,也就是在這個類別內需要有 public event PropertyChangedEventHandler PropertyChanged;
這個成員宣告存在。
對於想要能夠讓 資料綁定 Data Binding 機制可以正常運作,需要透過呼叫 [SetProperty] 這個泛型方法。
底下將會是使用繼承一個 [BindableBase] 類別,並且在 ViewModel 內宣告 [Text] 屬性,使得這個屬性可以用於 XAML 中的 {Binding Text}
延伸標記用法,這樣就完成了一個 資料綁定 的設計。
private string _text = " Click me" ;
public string Text
{
get => _text ;
set => SetProperty (ref _text , value );
}對於 [Text] 屬性的設計,將會採用 C# 自動實作的屬性 來設計;一旦當 [Text] 屬性有變更的時候,將會呼叫 [SetProperty] 這個方法,以便可以觸發 [PropertyChanged] 這個事件,如此,有關注或綁定這個事件的物件,將會收到通知,以便進行相對應的處理工作。
以上是對於一般資料型態的資料綁定的設計方式,對於需要綁定到 XAML 內的 [Command] 屬性上的命令綁定,則是透過在 ViewModel 內宣告 public DelegateCommand CountCommand { get; }
這個屬性,並且在建構式內,使用 CountCommand = new DelegateCommand(OnCountCommandExecuted);
敘述,產生一個 [DelegateCommand] 型別的物件,而在建立此物件的時候,至少需要傳入一個委派方法,而當這個命令被觸發的時候,將會來執行這裏所指定的委派方法。
在 PrismLibrary 內,對於 [DelegateCommand] 這個型別,將會繼承 [DelegateCommandBase] 類別,最終需要實作出 [ICommand] 這個介面,如此,才能夠使用這樣的物件於 XAML 內的 Command 來進行命令綁定之用
從這裡可看出,想要讓 MVVM 設計模式正常運作,達到關注點分離與鬆散耦合設計效果,程式設計師需要寫出相當多的程式碼,當然也就造成寫出許多原始碼內容,當然也會造成許多不良的副作用影響。
所以, [MVVM 工具] ,也就是 CommunityToolkit.Mvvm 套件,(也稱為 MVVM Toolkit,先前稱為 Microsoft.Toolkit.Mvvm) 是模組化的 MVVM 程式庫,使用了 [Roslyn] SDK 內提供的 來源產生器 Source Generators,透過這個機制, 來源產生器 ,可讓 C# 開發人員檢查正在編譯的使用者程式碼,來源產生器可以在即時新增至使用者的編譯時建立新的 C# 來源檔案。所得到的效果將會是可以讓整個專案原始碼變得更加簡潔與清爽,因為,Roslyn 編譯器已經把許多繁雜、瑣碎的工作與程式碼,都自動產生出來了。
現在,就來了解看看, [CommunityToolkit.Mvvm] 這個套件,在 .NET MAUI 專案內是如何運行的
建立 .NET MAUI 應用程式 專案開啟 Visual Studio 2022 點選螢幕右下角的 [建立新的專案] 按鈕 切換右上角的 [所有專案類型] 下拉選單控制項 找到並且點選 [MAUI] 這個選項 從清單中找到並選擇 [.NET MAUI 應用程式] 這個專案範本 點選右下角的 [下一步] 按鈕 當出現了 [設定新的專案] 對話窗 在 [專案名稱] 欄位內,輸入 MA52
點選右下角的 [下一步] 按鈕 當出現了 [其他資訊] 對話窗 對於 [架構] 的下拉選單控制項,使用預設值 點選右下角的 [建立] 按鈕 加入 CommunityToolkit.Mvvm 的 NuGet 套件 滑鼠右擊該專案的 [相依性] 節點 從彈出功能表中選擇 [管理 NuGet 套件] 功能選項 此時,[NuGet: csCommunityToolkitMVVM] 視窗將會出現 點選 [瀏覽] 標籤頁次 在左上方的搜尋文字輸入盒內輸入 CommunityToolkit.Mvvm
關鍵字 現在,將會看到 CommunityToolkit.Mvvm 套件出現在清單內 點選這個 CommunityToolkit.Mvvm 套件,並且點選右上方的 [安裝] 按鈕,安裝這個套件到這個專案內。 建立 MainPageViewModel 類別在這個建立好的專案,採用的是 .NET MAUI 預設的專案模板,因此,並沒有 ViewModel 預設建立在這個專案內,所以,在這裡先來建立一個 MainPage 這個 View 要使用的 ViewModel 類別
滑鼠右擊該專案節點 從彈出功能表中選擇 [加入] > [類別] 功能選項 此時,[新增項目] 對話窗將會出現 在對話窗下方的 [名稱] 欄位內,輸入 MainPageViewModel
作為這個類別的名稱 點選右下方 [新增] 按鈕 底下將會這次產生出來的類別檔案內容 using System ;
using System .Collections .Generic ;
using System .Linq ;
using System .Text ;
using System .Threading .Tasks ;
namespace MA52
{
internal class MainPageViewModel
{
}
} 檢查 Rolysn 來源產生器有沒有甚麼程式碼自動產生因為 .NET MAUI 採用單一專案架構,但是可以在不同平來下來執行,因此,請先確認現在的執行平台是哪個
預設來說,將會是 [Windows Machine] ,可以在 Visual Studio 2022 最上方找到 綠色 三角形的工具列按鈕,確認是否如下圖樣貌
滑鼠右擊該專案節點
從彈出功能表中選擇 [重建] 功能選項
現在這個專案將會重新編譯
一旦建置完成後
找到專案節點,參考下圖,依序展開這些節點 [MA52] > [相依性] > [net7.0-windows10.0.19041.0] > [分析器]
可以看到 [分析器] 將會看到 [CommunityToolkit.Mvvm.SourceGenerators]節點存在
展開這個點之後,將會看到更多節點項目,請找到 [CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator] 這個節點項目
請展開這個節點,將會出現 [此產生氣未產生檔案] 訊息,表示這裡尚未產生任何內容,不過,後面的內容,將會可以看到這裡產生出新項目。
不過,另外可以看到有個 [Microsoft.Maui.Controls.SourceGen] 節點
展開此節點將會看到有個 [Microsoft.Maui.Controls.SourceGen.CodeBehindGenerator] 節點存在
請繼續展開此節點,將會如下面節圖
從展開內容名稱可以猜測出來,這些都是 [Rolysn] 來源產生器產生出來的程式碼,而且都是在此專案內找到所有 .xaml 檔案,產生出相對應的 Code Behind 程式碼
有興趣的人,可以打開這些產生檔案名稱,就會看到產生出來的程式碼
底下將會是 [App.xaml.sg.cs] 節點內容
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a .NET MAUI source generator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
// ------------------------------------------------------------------------------
[assembly : global ::Microsoft .Maui .Controls .Xaml .XamlResourceId (" MA52.App.xaml" , " App.xaml" , typeof (global ::MA52 .App ))]
namespace MA52
{
[global ::Microsoft .Maui .Controls .Xaml .XamlFilePath (" App.xaml" )]
public partial class App : global ::Microsoft .Maui .Controls .Application
{
[global ::System .CodeDom .Compiler .GeneratedCode (" Microsoft.Maui.Controls.SourceGen" , " 1.0.0.0" )]
#if NET5_0_OR_GREATER
#endif
private void InitializeComponent ()
{
global ::Microsoft .Maui .Controls .Xaml .Extensions .LoadFromXaml (this , typeof (App ));
}
}
} 簡化預設產生的 MainPage 內容因為預設產生的頁面檔案,有使用到 Code Behind 內容,為了接下來的深入理解內容,故在此先將這個頁面內容簡化 在專案根目錄下,找到並且打開 [MainPage.xaml] 檔案 使用底下 XAML 標記內容,替換掉剛剛打開的檔案內容 <?xml version =" 1.0" encoding =" utf-8" ?>
<ContentPage xmlns =" http://schemas.microsoft.com/dotnet/2021/maui"
xmlns : x =" http://schemas.microsoft.com/winfx/2009/xaml"
x : Class =" MA52.MainPage" >
<ScrollView >
<VerticalStackLayout
Spacing =" 25" Padding =" 30,0"
VerticalOptions =" Center" >
<Image
Source =" dotnet_bot.png"
HeightRequest =" 200" HorizontalOptions =" Center" />
<Label
Text =" Hello, World!"
FontSize =" 32" HorizontalOptions =" Center" />
<Label
Text =" Welcome to .NET Multi-platform App UI"
FontSize =" 18" HorizontalOptions =" Center" />
<Button
Text =" Click me"
HorizontalOptions =" Center" />
</VerticalStackLayout >
</ScrollView >
</ContentPage > 在專案根目錄下,找到並且打開 [MainPage.xaml.cs] 檔案 使用底下 C# 標記內容,替換掉剛剛打開的檔案程式碼 namespace MA52 ;
public partial class MainPage : ContentPage
{
public MainPage ()
{
InitializeComponent ();
}
} 修正 MainPageViewModel 可以使用 CommunityToolkit.MVVM 功能在專案根目錄下,找到並且打開 [MainPageViewModel.cs] 檔案 使用底下 C# 程式碼替換掉這個檔案內容 using CommunityToolkit .Mvvm .ComponentModel ;
namespace MA52 ;
public partial class MainPageViewModel : ObservableObject
{
}這裡展示了一個採用 [CommunityToolkit.MVVM] 套件的 ViewModel 標準類別設計形式
首先, ViewModel 類別需要繼承 [ObservableObject] 這個類別,因為該類別內有實作 [INotifyPropertyChanged] 與 [INotifyPropertyChanging] 這兩個介面,有了實作介面的相關程式碼,便可以實踐出 MVVM 內的 資料綁定 Data Binding 機制了
接下來,還要做個修正,那就是這個類別必須修改使用 [partial] 這個修飾詞,也就是要使用 部分類別 來進行設計
若在此沒有加入 [partial] 這個修飾詞,將會導致等下要加入的程式碼,產生類似這樣的錯誤訊息
錯誤 CS0260 類型 'MainPageViewModel' 的宣告中遺漏 partial 修飾元; 還存在此類型的其他部分宣告 MA52 (net7.0-android), MA52 (net7.0-ios), MA52 (net7.0-maccatalyst), MA52 (net7.0-windows10.0.19041.0) C:\Vulcan\Projects\MA52\MA52\MainPageViewModel.cs 5 作用中
若忘記加入,也沒有關係,編譯器到時候會提醒你要加入回去
使用 CommunityToolkit.MVVM 提供的資料綁定功能 假設這裡需要在 ViewModel 內,設計一個 Text 屬性,可以用於 XAML 中來進行資料綁定之用 當使用 [PrismLibrary] 提供的 [BindableBase] 類別,需要使用底下六行 C# 程式碼來進行設計 要宣告一個 Public 的 屬性 Property private string _text = " Click me" ;
public string Text
{
get => _text ;
set => SetProperty (ref _text , value );
}同樣的需求,對於使用 [CommunityToolkit.MVVM] 方法來設計,就僅需要使用底下的兩行 C# 程式碼就可以完成了 [ObservableProperty ]
string text = " Click me" ; 這裡需要宣告一個類別的 欄位 Field ,而不是 屬性 Property,當然,既然是 欄位 成員,就不需要是 public,這裡將會使用預設 private 存取權限
還有一個特別要注意的事情,那就是這個 欄位 成員的名稱,必須採用 Camel Case (駝峰式) 命名規範,也就是第一個英文字母必須為小寫
若採用 Pascal Case (Pascal式) 命名規範,也就是第一個英文字母必須為大寫,將會造成編譯器發出錯誤通知,背後的理由很單純,因為,編譯器會產生一個使用 Pascal Case 命名方式的 屬性 Property 成員原始碼,並且在這裡會加入更多的程式碼
現在,可以從方案總管視窗內找看到 [分析器] 節點內的 [CommunityToolkit.Mvvm.SourceGenerators],在這個節點內展開 [CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator] 這個節點項目,將會看到如下面畫面截圖
打開 [MA52.MainPageViewModel.g.cs] 這個由編譯器產生出來的原始碼,將會看到底下的內容
// <auto-generated/>
#pragma warning disable
#nullable enable
namespace MA52
{
partial class MainPageViewModel
{
/// <inheritdoc cref =" text" />
[global ::System .CodeDom .Compiler .GeneratedCode (" CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator" , " 8.1.0.0" )]
[global ::System .Diagnostics .CodeAnalysis .ExcludeFromCodeCoverage ]
public string Text
{
get => text ;
set
{
if (! global ::System .Collections .Generic .EqualityComparer <string >.Default .Equals (text , value ))
{
OnTextChanging (value );
OnPropertyChanging (global ::CommunityToolkit .Mvvm .ComponentModel .__Internals .__KnownINotifyPropertyChangingArgs .Text );
text = value ;
OnTextChanged (value );
OnPropertyChanged (global ::CommunityToolkit .Mvvm .ComponentModel .__Internals .__KnownINotifyPropertyChangedArgs .Text );
}
}
}
/// <summary >Executes the logic for when <see cref =" Text" /> is changing.</summary >
[global ::System .CodeDom .Compiler .GeneratedCode (" CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator" , " 8.1.0.0" )]
partial void OnTextChanging (string value );
/// <summary >Executes the logic for when <see cref =" Text" /> just changed.</summary >
[global ::System .CodeDom .Compiler .GeneratedCode (" CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator" , " 8.1.0.0" )]
partial void OnTextChanged (string value );
}
}[Rosyln] 編譯器產生了一個 [MainPageViewModel] 類別,由於這裡也使用到了 [partial] 修飾詞,因此,這兩個 [MainPageViewModel] 類別將會由編譯器編譯到同一個類別內。 如同剛剛說明到的,來源產生器產生了一個 [Text] 屬性,並且在 [set] 屬性存取子內,也產生出許多程式碼,用於完成資料綁定所需要的工作 試想看看,若開發人員在進行 MVVM 專案開發的過程,沒有 [CommunityToolkit.MVVM] 套件的幫助,將會需要自己來寫出這些程式碼,並且也要確保這些自己寫出的程式碼正確性,對於日後要進行維護專案程式碼的時候,將會面臨到自己寫出繁多程式碼,也會造成維護上的負擔。 使用 CommunityToolkit.MVVM 提供的命令綁定功能 假設這裡需要在 ViewModel 內,設計一個型別為 [RelayCommand] 的 [CountCommand] 屬性,可以用於 XAML 中來進行命令綁定之用;例如,可以綁定到按鈕的 [Command] 屬性上,使用 <Button Command{Binding CountCommand}>
[RelayCommand ]
void Count ()
{ } 在這裡請先設計一個這個 ViewModel 類別內的 [Count] 方法成員,該方法名稱命名方式將會依照 .NET C# 內建議的 Pascal Case 命名式 (每個英文字的第一個字母要大小)
最後,僅需要在這個方法的上方,使用 [RelayCommand]
這個屬性宣告即可
現在,可以從方案總管視窗內找看到 [分析器] 節點內的 [CommunityToolkit.Mvvm.SourceGenerators],在這個節點內展開 [CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator] 這個節點項目,將會看到如下面畫面截圖
打開 [MA52.MainPageViewModel.Count.g.cs] 這個由編譯器產生出來的原始碼,將會看到底下的內容
// <auto-generated/>
#pragma warning disable
#nullable enable
namespace MA52
{
partial class MainPageViewModel
{
/// <summary >The backing field for <see cref =" CountCommand" />.</summary >
[global ::System .CodeDom .Compiler .GeneratedCode (" CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator" , " 8.1.0.0" )]
private global ::CommunityToolkit .Mvvm .Input .RelayCommand ? countCommand ;
/// <summary >Gets an <see cref =" global::CommunityToolkit.Mvvm.Input.IRelayCommand" /> instance wrapping <see cref =" Count" />.</summary >
[global ::System .CodeDom .Compiler .GeneratedCode (" CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator" , " 8.1.0.0" )]
[global ::System .Diagnostics .CodeAnalysis .ExcludeFromCodeCoverage ]
public global::CommunityToolkit.Mvvm.Input.IRelayCommand CountCommand => countCommand ??= new global ::CommunityToolkit .Mvvm .Input .RelayCommand (new global ::System .Action (Count ));
}
}從這裡產生的程式碼可以看到,這裡又是產生一個 [MainPageViewModel] 的 [partial] 類別 這裡宣告一個型別為 [RelayCommand] 的 [countCommand] 欄位成員 接著建立一個 屬性之運算式主體成員 的 CountCommand 屬性 有了這個公開的 CountCommand 屬性,便可以在 XAML 頁面內,宣告與使用命令綁定功能