使用 ViewModel

已完成

了解 Model-View-ViewModel (MVVM) 模式的組成元件之後,您可能會發現定義模型和檢視很簡單。 讓我們探索如何使用 ViewModel,以在該模式中更妥善地定義其角色。

將屬性公開給使用者介面

如同先前的範例,ViewModel 通常依賴於模型的大部分資料及所有商務邏輯。 但卻是 ViewModel 依目前檢視需要的方式來格式化、轉換和擴充資料。

使用 ViewModel 格式化

您已看到格式化休假時間的範例。 日期格式設定、字元編碼和序列化都是 ViewModel 可能格式化模型資料的範例。

使用 ViewModel 轉換

通常,模型會以間接方式提供資訊。 不過,ViewModel 可以修正此問題。 例如,假設您想要在螢幕上顯示員工是否為主管。 但我們的 Employee 模型不能直接告訴我們。 您反而必須根據是否有其他人向此人報告,以推斷出此一事實。 假設模型具有此屬性:

public IList<Employee> DirectReports
{
    get
    {
        ...
    }
}

如果清單是空的,您可以推斷 Employee 不是主管。 在此案例中,EmployeeViewModel 包含提供該邏輯的屬性 IsSupervisor

public bool IsSupervisor => _model.DirectReports.Any();

使用 ViewModel 擴充

有時候模型可能只提供相關資料的識別碼。 或者,您可能需要數個模型類別,才能相互關聯單一畫面所需的資料。 ViewModel 也提供執行這些工作的理想位置。 假設您想要顯示某位員工目前正在管理的所有專案。 此資料不屬於 Employee 模型類別。 但可透過查看 CompanyProjects 模型類別來存取此資料。 我們的 EmployeeViewModel 一如既往地公開其工作作為公用屬性:

public IEnumerable<string> ActiveProjects => CompanyProjects.All
    .Where(p => p.Owner == _model.Id && p.IsActive)
    .Select(p => p.Name);

使用傳遞屬性搭配 ViewModel

ViewModel 經常需要「完全」由此模型提供的屬性。 針對那些屬性,ViewModel 只會傳遞資料:

public string Name
{
    get => _model.Name;
    set => _model.Name = value;
}

設定 ViewModel 的範圍

您可以在有檢視的任何層級使用 ViewModel。 頁面通常「有」ViewModel,但頁面的子檢視也可能有。 巢狀 ViewModel 的一個常見原因,是該頁面會在頁面上顯示 ListView。 此清單具有代表該集合的 ViewModel,例如 EmployeeListViewModel。 清單中的每個元素都是 EmployeeViewModel

包含數個 EmployeeViewModel 子物件的 EmployeeListViewModel 圖表。

通常也會有最上層的 ViewModel,可保存整個應用程式資料和狀態,但不與任何特定頁面建立關聯。 這種 ViewModel 經常用於維持「使用中」項目。 請考慮之前提到的 ListView 範例。 當使用者選取員工資料列時,該員工代表「目前的項目」。 如果使用者巡覽至詳細資料頁面,或在該資料列選取的狀態下選取工具列按鈕,則動作或顯示畫面應該是針對該員工。 處理此案例的巧妙方法,是將 ListView.SelectItem 資料繫結至工具列或詳細資料頁面亦可存取的屬性。 將該屬性放在中央 ViewModel 上效果很好。

識別何時要重複使用 ViewModel 與檢視

您如何定義 ViewModel 和模型之間,以及 ViewModel 和檢視之間的關係,應用程式需求決定的部分大於任何規則所決定部分。 ViewModel 旨在提供檢視所需要的結構和資料。 該目的應該引導 viewmodel 範圍有「多大」的決策。

ViewModel 通常會密切反映模型類別的結構,並與該類別有一對一的關聯性。 您之前看到了 EmployeeViewModel 的範例,它包裝並增強了單一 Employee 執行個體。 但它未必一律是一對一的關係。 如果 ViewModel 的設計目的是要提供檢視所需的內容,您最終可能會得到類似 HRDashboardViewModel 的結果,以提供 HR 部門的概觀,該部門與所有模型都沒有明確的關聯性,但可以使用來自任何模型類別的資料。

同樣地,您可能會發現 ViewModel 和「檢視」通常有一對一的關聯性。 但並非總是如此。 讓我們再次考慮針對每位員工顯示一行資料列的 ListView。 當選取其中一筆資料列時,即會前往員工詳細資料頁面。

清單頁面有具有集合的 ViewModel。 如前所述,該集合「可能」EmployeeViewModel 物件的集合。 當使用者選取一列時,EmployeeViewModel 執行個體可以傳遞給 EmployeeDetailPage。 詳細資料頁面可以使用 EmployeeViewModel 作為其 BindingContext

此案例「可能」是重複使用 ViewModel 的絕佳機會。 但請記住,ViewModel 旨在提供檢視所需的內容。 在某些情況下,即使它們是以相同的模型類別為基礎,您也可能需要個別的 ViewModel。 在此範例中,ListView 資料列需要的資訊可能比完整詳細資料頁面少得多。 從詳細資料頁面擷取資料會增加很多額外負荷,建議您改用 EmployeeListRowViewModelEmployeeDetailViewModel 模型,為其各自的檢視提供服務。

ViewModel 物件模型

使用實作的 INotifyPropertyChanged 基底類別,表示您不需要在每個 ViewModel 上重新實作介面。 請如此訓練課程模組的前一部分所述,考慮 HR 應用程式。 EmployeeViewModel 類別實作 INotifyPropertyChanged 介面,並提供名為 OnPropertyChanged 的協助程式方法以引發 PropertyChanged 事件。 專案中的其他 ViewModel (例如描述指派給員工之資源的 ViewModel) 也需要 INotifyPropertyChanged,才能與檢視完全整合。

MVVM 工具組程式庫是 .NET Community Toolkit 的一部分,是標準、獨立、輕量型類型的集合,可提供使用 MVVM 模式建置新式應用程式的起始實作。

比起自行撰寫 ViewModel 基底類別,而是繼承自工具組的 ObservableObject 類別,這麼做提供 ViewModel 基底類別所需的一切。 可以透過下列情況簡化 EmployeeViewModel

using System.ComponentModel;

public class EmployeeViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    private Employee _model;

    public string Name
    {
        get {...}
        set
        {
            _model.Name = value;
            OnPropertyChanged(nameof(Name))
        }
    }

    protected void OnPropertyChanged(string propertyName) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

變更為下列程式碼:

using Microsoft.Toolkit.Mvvm.ComponentModel;

public class EmployeeViewModel : ObservableObject
{
    private string _name;

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

您可以使用 MVVM 工具組所提供的來源產生器,進一步簡化程式碼。 藉由將類別設為 partial 並將 [ObservableProperty] 新增至 private 變數,就會產生具有適當屬性變更通知的公用屬性 Name

using Microsoft.Toolkit.Mvvm.ComponentModel;

public partial class EmployeeViewModel : ObservableObject
{
    [ObservableProperty]
    private string _name;
}

MVVM 工具組會透過 CommunityToolkit.Mvvm NuGet 套件散發。

檢定您的知識

1.

搭配 .NET MAUI 使用 Model-View-ViewModel (MVVM) 模式時,模型、檢視和 viewmodel 彼此都不會完全取消耦合。 哪個選項描述了 MVVM 之間一種常見的相依性?

2.

哪一個最可能與平台緊密結合,卻很難建立模型、檢視或 ViewModel 的單元測試?