訓練
模組
設計適用於 .NET MAUI 的 MVVM ViewModel - Training
探索 MVVM 設計模式如何協助區隔商務邏輯和使用者介面程式碼。 了解 ViewModel 的設計方式,以及為何其如此重要。
提示
本內容節錄自《Enterprise Application Patterns Using .NET MAUI》電子書,可以從 .NET Docs 取得,也可以免費下載 PDF 離線閱讀。
.NET MAUI 開發人員經驗通常涉及使用 XAML 建立使用者介面,然後新增可在此使用者介面上運作的程式碼後置。 隨著應用程式的大小和範圍因修改而擴大,會發生複雜的維護問題。 這些問題包括 UI 控制項與商務邏輯之間的緊密結合,這會增加 UI 修改的成本,以及單元測試這類程式碼的困難。
MVVM 模式有助清楚區隔應用程式的商務和呈現邏輯與使用者介面 (UI)。 維護應用程式邏輯與 UI 之間的全新區隔有助於解決許多開發問題,更容易測試、維護及演進應用程式。 也可以大幅改善程式碼重複使用的機會,讓開發人員和 UI 設計人具在開發應用程式的各自部分時更容易共同作業。
MVVM 模式有三個核心元件:模型、檢視和檢視模型。 每個元件的用途各不相同。 下圖顯示三個元件之間的關聯性。
除了解每個元件的責任之外,請務必也要了解它們的互動方式。 總之,檢視「知道」檢視模型,而檢視模型「知道」模型,但模型不知道檢視模型,而檢視模型不知道檢視。 因此,檢視模型會隔離檢視與模型,讓模型獨立於檢視外演進。
使用 MVVM 模式的優點如下:
有效使用 MVVM 的關鍵在於了解如何將應用程式程式碼分解成正確的類別,以及類別之間如何互動。 下列各節討論 MVVM 模式中每個類別的責任。
檢視負責定義使用者在畫面上看到的內容結構、版面配置和外觀。 理想情況下,每個檢視都是使用 XAML 定義,具有不包含商務邏輯的有限程式碼後置。 但有時候,程式碼後置可能包含 UI 邏輯,以實作難以利用 XAML 表現的視覺行為,例如動畫。
在 .NET MAUI 應用程式中,檢視通常是 ContentPage
衍生或 ContentView
衍生類別。 不過,檢視也可以用資料範本表示,指定顯示時要以視覺化方式呈現物件所用的 UI 元素。 以資料範本為檢視沒有任何程式碼後置,旨在繫結至特定的檢視模型類型。
提示
避免在程式碼後置中啟用與停用 UI 元素。
請確定檢視模型負責定義的邏輯狀態變更,會影響檢視顯示的某些層面,例如命令是否可用,或指出暫止的作業。 因此,請透過繫結至檢視模型屬性來啟用與停用 UI 元素,不要在程式碼後置中啟用與停用 UI 元素。
有數個選項可在檢視模型上執行程式碼,以在檢視上回應互動,例如按一下的按鈕或選取的項目。 如果控制項支援命令,則控制項的 Command 屬性就可以在檢視模型上的 ICommand 屬性繫結資料。 叫用控制項的命令時,會執行檢視模型中的程式碼。 除了命令之外,行為也可以附加至檢視的物件,並接聽要叫用的命令或要引發的事件。 接著,行為即可叫用檢視模型上的 ICommand,或檢視模型上的方法。
檢視模型會實作檢視可繫結資料的屬性和命令,並透過變更通知事件通知檢視任何狀態變更。 檢視模型提供的屬性和命令會定義 UI 所提供的功能,但檢視會決定如何顯示該功能。
提示
使用非同步作業讓 UI 保持回應。
多平台應用程式應該讓 UI 執行緒保持在解除封鎖的狀態,以改善使用者對效能的認知。 因此,在檢視模型中,針對 I/O 作業使用非同步方法,並引發事件以非同步方式通知檢視屬性變更。
檢視模型也負責協調檢視與任何所需模型類別的互動。 檢視模型與模型類別之間通常有一對多關聯性。 檢視模型可能選擇直接向檢視公開模型類別,讓檢視中的控制項可以直接在類別上繫結資料。 在此情況下,模型類別即必須設計為支援資料繫結和變更通知事件。
每個檢視模型都會使用檢視可輕易取用的格式提供模型資料。 為此,檢視模型有時會執行資料轉換。 將此資料轉換放在檢視模型中是個不錯的主意,因為它提供檢視可繫結的屬性。 例如,檢視模型可能會結合兩個屬性的值,方便檢視顯示。
提示
將資料轉換集中在轉換層。
您也可以使用轉換器作為坐落在檢視模型與檢視之間的另一個資料轉換層。 例如,當資料需要檢視模型未提供的特殊格式時,這就很有必要了。
為使檢視模型加入到與檢視的雙向資料繫結,其屬性必須引發 PropertyChanged
事件。 檢視模型實作 INotifyPropertyChanged
介面,並在屬性變更時引發 PropertyChanged
事件,藉以滿足此需求。
若為集合,則提供了檢視易讀取的 ObservableCollection<T>
。 此集合會實作集合變更通知,讓開發人員不必對集合實作 INotifyCollectionChanged
介面。
模型類別是封裝應用程式資料的非視覺類別。 因此,您可以將模型視為代表應用程式定義域的模型,這通常包含資料模型以及商業和驗證邏輯。 模型物件範例包括資料傳輸物件 (DTO)、簡單的 CLR 物件 (POCO),以及產生的實體和 Proxy 物件。
模型類別通常會與封裝資料存取和快取的服務或存放庫搭配使用。
檢視模型可以使用 .NET MAUI 的資料繫結功能連線到檢視。 有許多方法可用來建構檢視和檢視模型,並在執行階段建立它們彼此的關聯性。 這些方法分為兩個類別,稱為檢視優先組合,以及檢視模型優先組合。 檢視優先組合和檢視模型優先組合該如何選擇,是喜好設定和複雜度的問題。 不過,所有方法都有共同的目標,亦即讓檢視將檢視模型指派給其 BindingCoNtext 屬性。
使用檢視優先組合,應用程式在概念上是由連線到其所相依之檢視模型的檢視所組成。 此方法的最大優點是容易建構結合不緊密的可單元測試應用程式,因為檢視模型本身和檢視不相依。 而遵循應用程式的視覺結構,也很容易了解應用程式的結構,不需追蹤程式碼執行,就能了解類別的建立和關聯方式。 此外,檢視優先建構會與在瀏覽發生時負責建構頁面的 Microsoft Maui 瀏覽系統保持一致,這會讓檢視模型優先組合變得複雜,且與平台不一致。
使用檢視模型優先組合,應用程式在概念上是由檢視模型組成,包含負責尋找檢視模型之檢視的服務。 檢視模型優先組合對部分開發人員而言更自然,因為他們可以忽略檢視建立,專注於應用程式的邏輯非 UI 結構。 此外,這種組合還允許其他檢視模型建立檢視模型。 只不過這種方法通常很複雜,而且很難了解應用程式各個組件的建立和關聯方式。
提示
讓檢視模型和檢視保持獨立。
將檢視繫結至資料來源的屬性,應該是檢視在其對應檢視模型上的主體相依性。 具體而言,不要參考檢視模型的檢視類型,例如 Button 和 ListView。 遵循這裡列述的原則,即可隔離測試檢視模型,從而限制範圍以降低軟體瑕疵的可能性。
下列各節會討論連線檢視模型和檢視的主要方法。
最簡單的方法是讓檢視以宣告方式具現化其在 XAML 中的對應檢視模型。 建構檢視時,也會建構對應的檢視模型物件。 下列程式碼範例會示範這個方法:
<ContentPage xmlns:local="clr-namespace:eShop">
<ContentPage.BindingContext>
<local:LoginViewModel />
</ContentPage.BindingContext>
<!-- Omitted for brevity... -->
</ContentPage>
建立 ContentPage
時,會自動建構 LoginViewModel
執行個體,並設定為檢視的 BindingContext
。
依檢視排列的這種檢視模型宣告式建構和指派,優點就是簡單,但缺點則是檢視模型中需要有預設 (無參數) 建構函式。
檢視可在程式碼後置檔案中擁有程式碼,以將檢視模型指派給其 BindingContext
屬性。 這通常會在檢視的建構函式中完成,如下列程式碼範例所示:
public LoginView()
{
InitializeComponent();
BindingContext = new LoginViewModel(navigationService);
}
在檢視的程式碼後置中以程式設計方式建構及指派檢視模型,優點就是簡單。 不過,此方法的主要缺點是檢視需要為檢視模型提供所有必要相依性。 使用相依性插入容器有助於維護檢視和檢視模型之間的鬆散結合。 如需詳細資訊,請參閱相依性插入。
檢視可存取的所有檢視模型和模型類別都應該實作 INotifyPropertyChanged 介面。 在檢視模型或模型類別中實作這個介面,可讓類別在基礎屬性值變更時,將變更通知提供給檢視中的任何資料繫結控制項。
應用程式應根據下列需求架構,以正確使用屬性變更通知:
PropertyChanged
事件。 不要因為知道 XAML 繫結如何發生,就假設引發 PropertyChanged
事件可予以忽略。PropertyChanged
事件。PropertyChanged
事件。 引發事件會透過同步叫用事件的處理常式來中斷作業。 如果在作業中途發生這種情況,物件可能會在不安全的部分更新狀態下,向回呼函式公開。 此外,PropertyChanged
事件可能會觸發串聯變更。 串聯變更通常需要先完成更新,才能安全執行串聯變更。PropertyChanged
事件。 這表示您必須先比較新舊兩值,再引發 PropertyChanged
事件。PropertyChanged
事件。 檢視中的資料繫結控制項目前尚未訂閱接收變更通知。PropertyChanged
事件。 例如,假設 NumberOfItems
屬性的備份存放區是 _numberOfItems
欄位,如果方法在執行迴圈期間將 _numberOfItems
遞增 50 次,在所有工作完成後,應該只會在 NumberOfItems
屬性上引發一次屬性變更通知。 若是非同步方法,請在非同步接續鏈的每個同步區段中,針對指定的屬性名稱引發 PropertyChanged
事件。提供這項功能的簡單方式就是建立 BindableObject
類別的延伸模組。 在本範例中,ExtendedBindableObject
類別會提供變更通知,如下列程式碼範例所示:
public abstract class ExtendedBindableObject : BindableObject
{
public void RaisePropertyChanged<T>(Expression<Func<T>> property)
{
var name = GetMemberInfo(property).Name;
OnPropertyChanged(name);
}
private MemberInfo GetMemberInfo(Expression expression)
{
// Omitted for brevity ...
}
}
.NET MAUI 的 BindableObject
類別會 實作 INotifyPropertyChanged
介面,並提供 OnPropertyChanged
方法。 ExtendedBindableObject
類別提供 RaisePropertyChanged
方法以叫用屬性變更通知,且因而使用 BindableObject
類別所提供的功能。
然後,檢視模型類別即可衍生自 ExtendedBindableObject
類別。 因此,每個檢視模型類別都會使用 ExtendedBindableObject
類別中的 RaisePropertyChanged
方法來提供屬性變更通知。 下列程式碼範例示範 eShop 多平台應用程式如何使用 lambda 運算式叫用屬性變更通知:
public bool IsLogin
{
get => _isLogin;
set
{
_isLogin = value;
RaisePropertyChanged(() => IsLogin);
}
}
以這種方式使用 lambda 運算式所涉及的效能成本較低,因為每個呼叫都必須評估 lambda 運算式。 雖然效能成本很低,而且通常不會影響應用程式,但當變更通知很多時,積累的成本不可輕忽。 不過,此方法的優點是能在重新命名屬性時,提供編譯階段類型安全性和重構支援。
MVVM 模式已使用 .NET 妥善建立,而社群建立的許多架構有助於簡化此開發。 每個架構都會提供一組不同的功能,但其標準皆是提供可實作 INotifyPropertyChanged
介面的通用檢視模型。 MVVM 架構的其他功能包括自訂命令、瀏覽協助程式、相依性插入/服務定位器元件及 UI 平台整合。 雖然您不一定需要使用這些架構,但它們可以加速並標準化您的開發。 eShop 多平台應用程式使用 .NET Community MVVM 工具組。 選擇架構時,建議您考慮應用程式的需求和小組的長項。 以下清單包含一些較常見的 .NET MAUI MVVM 架構。
多平台應用程式通常會在程式碼後置檔案中建立事件處理常式,叫用可實作的動作以回應使用者動作,例如按一下按鈕。 不過,在 MVVM 模式中,實作動作的責任在於檢視模型,並應避免將程式碼放在程式碼後置中。
命令提供了方便的方式,以表示可繫結至 UI 控制項的動作。 它們會封裝實作動作的程式碼,並協助保持其與檢視中視覺表示的分離狀態。 如此一來,您的檢視模型就更容易移植到新的平台,因為它們與平台 UI 架構所提供的事件沒有直接相依性。 .NET MAUI 包含可以宣告方式連線到命令的控制項,而且這些控制項會在使用者與控制項互動時叫用命令。
行為也允許控制項以宣告方式連線到命令。 不過,您也可以使用行為叫用與控制項所引發之一系列事件相關聯的動作。 因此,行為會處理許多與啟用命令控制項相同的案例,同時提供更大的彈性和控制力度。 此外,針對未特別設計與命令互動的控制項,您還可以使用行為建立其與命令物件或方法的關聯性。
檢視模型通常會公開公用屬性以從檢視繫結,這會實作 ICommand
介面。 許多 .NET MAUI 控制項和手勢都會提供 Command
屬性,這可以是繫結至檢視模型所提供之 ICommand
物件的資料。 按鈕控制項是最常使用的控制項之一,提供可在按一下按鈕時執行的命令屬性。
注意
雖然您可以公開檢視模型所用 ICommand
介面的實際實作 (例如 Command<T>
或 RelayCommand
),但建議您將命令公開為 ICommand
。 如此一來,如果以後需要變更實作,就可以輕鬆地交換。
ICommand
介面會定義可封裝作業本身的 Execute
方法、指出是否可以叫用命令的 CanExecute
方法,以及變更發生時所出現影響命令是否應執行的 CanExecuteChanged
事件。 大部分情況下,我們只會為自已的命令提供 Execute
方法。 如需 ICommand
的詳細概觀,請參閱 .NET MAUI 的命令文件。
隨附於 .NET MAUI 的 Command
和 Command<T>
類別,可實作 ICommand
介面,其中 T
是 Execute
和 CanExecute
的引數類型。 Command
和 Command<T>
是基本實作,可提供 ICommand
介面所需的基本功能集。
注意
許多 MVVM 架構提供更多功能豐富的 ICommand
介面實作。
Command
或 Command<T>
建構函式需要有叫用 ICommand.Execute
方法時所呼叫的 Action 回呼物件。 CanExecute
方法是選擇性的建構函式參數,而且是會傳回 bool 的 Func。
eShop 多平台應用程式使用 RelayCommand 和 AsyncRelayCommand。 新式應用程式的主要優點是 AsyncRelayCommand
為非同步作業提供了更好的功能。
下列程式碼示範如何經由指定 Register 檢視模型方法的委派,建構表示註冊命令的 Command
執行個體:
public ICommand RegisterCommand { get; }
此命令會透過傳回 ICommand
參考的屬性向檢視公開。 對 Command
物件呼叫 Execute
方法時,只要透過 Command
建構函式中所指定的委派,將呼叫轉送至檢視模型中的方法即可。 指定命令的 Execute
委派時,命令可以使用 async 和 await 關鍵字叫用非同步方法。 這表示回呼是 Task
,而且應等候。 例如,下列程式碼示範如何經由指定 SignInAsync
檢視模型方法的委派,建構表示登入命令的 ICommand
執行個體:
public ICommand SignInCommand { get; }
...
SignInCommand = new AsyncRelayCommand(async () => await SignInAsync());
您可以使用 AsyncRelayCommand<T>
類別將命令具現化,將參數傳遞至 Execute
和 CanExecute
動作。 例如,下列程式碼示範如何使用 AsyncRelayCommand<T>
執行個體指出 NavigateAsync
方法需要類型字串的引數:
public ICommand NavigateCommand { get; }
...
NavigateCommand = new AsyncRelayCommand<string>(NavigateAsync);
在 RelayCommand
和 RelayCommand<T>
類別中,每個建構函式中的 CanExecute
方法委派都是選擇性的。 如未指定委派,Command
即會針對 CanExecute
傳回 true。 不過,檢視模型可以對 Command
物件呼叫 ChangeCanExecute
方法,指出命令的 CanExecute
狀態變更。 這會引發 CanExecuteChanged
事件。 然後,繫結至命令的任何 UI 控制項都會更新其啟用狀態,以反映繫結到命令的資料可用性。
下列程式碼範例示範如何使用 TapGestureRecognizer
執行個體將 LoginView
中的 Grid
繫結至 LoginViewModel
類別的 RegisterCommand
:
<Grid Grid.Column="1" HorizontalOptions="Center">
<Label Text="REGISTER" TextColor="Gray"/>
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding RegisterCommand}" NumberOfTapsRequired="1" />
</Grid.GestureRecognizers>
</Grid>
您也可以使用 CommandParameter
屬性選擇是否定義命令參數。 預期引數的類型是在 Execute
和 CanExecute
目標方法中指定。 當使用者與附加控制項互動時,TapGestureRecognizer
會自動叫用目標命令。 如果提供,CommandParameter
就會當成引數傳遞至命令的 Execute 委派。
行為可讓功能新增至 UI 控制項,卻不需要將其子類別化。 相反地,功能會在行為類別中實作並附加至控制項,如同控制項本身的一部分。 行為可讓您實作一般必須撰寫為程式碼後置的程式碼,因為此程式碼直接與控制項的 API 互動,所以透過此方式可以精確附加至控制項並封裝,以供多個檢視或應用程式重複使用。 在 MVVM 內容中,行為是連線控制項與命令的實用方法。
透過附加屬性附加至控制項的行為稱為附加行為。 然後,行為可以使用其所附加元素的公開 API,在檢視的視覺化樹狀結構中,將功能新增至該控制項或其他控制項。
.NET MAUI 行為是衍生自 Behavior
或 Behavior<T>
類別的類別,其中 T 是應該套用行為的控制項類型。 這些類別提供 OnAttachedTo
和 OnDetachingFrom
方法,其應該被覆寫,才能提供行為附加至控制項以及中斷連接時所執行的邏輯。
在 eShop 多平台應用程式中,BindableBehavior<T>
類別衍生自 Behavior<T>
類別。 BindableBehavior<T>
類別的用途旨在為需要將行為的 BindingContext
設為附加控制項之 .NET MAUI 行為提供基底類別。
BindableBehavior<T>
類別會提供可覆寫的 OnAttachedTo
方法以設定行為的 BindingContext
,和可覆寫的 OnDetachingFrom
方法以清除 BindingContext
。
eShop 多平台應用程式包含 MAUI Community 工具組提供的 EventToCommandBehavior 類別。 EventToCommandBehavior
會執行命令以回應發生的事件。 這個類別衍生自 BaseBehavior<View>
類別,所以當行為被取用時,其可繫結至 Command
屬性指定的 ICommand
,並予以執行。 下列程式碼範例顯示 EventToCommandBehavior
類別:
/// <summary>
/// The <see cref="EventToCommandBehavior"/> is a behavior that allows the user to invoke a <see cref="ICommand"/> through an event. It is designed to associate Commands to events exposed by controls that were not designed to support Commands. It allows you to map any arbitrary event on a control to a Command.
/// </summary>
public class EventToCommandBehavior : BaseBehavior<VisualElement>
{
// Omitted for brevity...
/// <inheritdoc/>
protected override void OnAttachedTo(VisualElement bindable)
{
base.OnAttachedTo(bindable);
RegisterEvent();
}
/// <inheritdoc/>
protected override void OnDetachingFrom(VisualElement bindable)
{
UnregisterEvent();
base.OnDetachingFrom(bindable);
}
static void OnEventNamePropertyChanged(BindableObject bindable, object oldValue, object newValue)
=> ((EventToCommandBehavior)bindable).RegisterEvent();
void RegisterEvent()
{
UnregisterEvent();
var eventName = EventName;
if (View is null || string.IsNullOrWhiteSpace(eventName))
{
return;
}
eventInfo = View.GetType()?.GetRuntimeEvent(eventName) ??
throw new ArgumentException($"{nameof(EventToCommandBehavior)}: Couldn't resolve the event.", nameof(EventName));
ArgumentNullException.ThrowIfNull(eventInfo.EventHandlerType);
ArgumentNullException.ThrowIfNull(eventHandlerMethodInfo);
eventHandler = eventHandlerMethodInfo.CreateDelegate(eventInfo.EventHandlerType, this) ??
throw new ArgumentException($"{nameof(EventToCommandBehavior)}: Couldn't create event handler.", nameof(EventName));
eventInfo.AddEventHandler(View, eventHandler);
}
void UnregisterEvent()
{
if (eventInfo is not null && eventHandler is not null)
{
eventInfo.RemoveEventHandler(View, eventHandler);
}
eventInfo = null;
eventHandler = null;
}
/// <summary>
/// Virtual method that executes when a Command is invoked
/// </summary>
/// <param name="sender"></param>
/// <param name="eventArgs"></param>
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
protected virtual void OnTriggerHandled(object? sender = null, object? eventArgs = null)
{
var parameter = CommandParameter
?? EventArgsConverter?.Convert(eventArgs, typeof(object), null, null);
var command = Command;
if (command?.CanExecute(parameter) ?? false)
{
command.Execute(parameter);
}
}
}
OnAttachedTo
和 OnDetachingFrom
方法可用來註冊及取消註冊 EventName
屬性中所定義事件的事件處理常式。 然後,當事件發生時,會叫用執行命令的 OnTriggerHandled
方法。
在事件發生時使用 EventToCommandBehavior
執行命令的優點,是可以建立命令與未設計和命令互動之控制項的關聯性。 此外,這會將事件處理程式碼移至檢視模型,在此進行單元測試。
EventToCommandBehavior
特別適合用於將命令附加到不支援命令的控制項上。 例如,當使用者變更密碼值時,LoginView 會使用 EventToCommandBehavior
執行 ValidateCommand
,如下列程式碼所示:
<Entry
IsPassword="True"
Text="{Binding Password.Value, Mode=TwoWay}">
<!-- Omitted for brevity... -->
<Entry.Behaviors>
<mct:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding ValidateCommand}" />
</Entry.Behaviors>
<!-- Omitted for brevity... -->
</Entry>
在執行階段,EventToCommandBehavior
會回應與 Entry
的互動。 當使用者鍵入 Entry
欄位時,會引發 TextChanged
事件,這會在 LoginViewModel
中執行 ValidateCommand
。 事件的事件引數預設會傳遞給命令。 如有需要,您可使用 EventArgsConverter
屬性將事件提供的 EventArgs
轉換成命令預期的輸入值。
如需行為的詳細資訊,請參閱 .NET MAUI 開發人員中心的行為。
Model-View-ViewModel (MVVM) 模式有助清楚區隔應用程式的商務和呈現邏輯與使用者介面 (UI)。 維護應用程式邏輯與 UI 之間的全新區隔有助於解決許多開發問題,更容易測試、維護及演進應用程式。 也可以大幅改善程式碼重複使用的機會,讓開發人員和 UI 設計人具在開發應用程式的各自部分時更容易共同作業。
使用 MVVM 模式時,應用程式 UI、基礎呈現方式和商務邏輯會分成三個不同的類別:封裝 UI 和 UI 邏輯的檢視,封裝呈現方式邏輯和狀態的檢視模型,以及封裝應用程式商務邏輯和資料的模型。
訓練
模組
設計適用於 .NET MAUI 的 MVVM ViewModel - Training
探索 MVVM 設計模式如何協助區隔商務邏輯和使用者介面程式碼。 了解 ViewModel 的設計方式,以及為何其如此重要。