使用MVVM概念升級您的應用程式
本教學課程系列旨在繼續 建立 .NET MAUI 應用程式 教學課程,該教學課程會建立記事應用程式。 在這部分的系列文章中,您將了解如何:
- 實作model-view-viewmodel (MVVM) 模式。
- 使用額外的查詢字串樣式,在瀏覽期間傳遞數據。
強烈建議您先遵循 建立 .NET MAUI 應用程式 教學課程,因為該教學課程中建立的程式代碼是本教學課程的基礎。 如果您遺失程式代碼,或想要重新開始,請下載 此專案。
瞭解MVVM
.NET MAUI 開發人員體驗通常牽涉到在 XAML 中建立使用者介面,然後新增在使用者介面上運作的程式代碼後置。 隨著應用程式的大小和範圍因修改而擴大,會發生複雜的維護問題。 這些問題包括 UI 控制項與商務邏輯之間的緊密結合,這會增加 UI 修改的成本,以及單元測試這類程式碼的困難。
model-view-viewmodel (MVVM) 模式有助於將應用程式的商務和呈現邏輯與其使用者介面 (UI) 分開。 維護應用程式邏輯與UI之間的全新區隔,有助於解決許多開發問題,並讓應用程式更容易測試、維護和演進。 它也可以大幅改善程式代碼重複使用的機會,並讓開發人員和UI設計工具在開發應用程式各自的部分時更輕鬆地共同作業。
模式
MVVM 模式有三個核心元件:模型、檢視和檢視模型。 每個元件的用途各不相同。 下圖顯示三個元件之間的關聯性。
除了解每個元件的責任之外,請務必也要了解它們的互動方式。 總之,檢視「知道」檢視模型,而檢視模型「知道」模型,但模型不知道檢視模型,而檢視模型不知道檢視。 因此,檢視模型會隔離檢視與模型,讓模型獨立於檢視外演進。
有效使用 MVVM 的關鍵在於了解如何將應用程式程式碼分解成正確的類別,以及類別之間如何互動。
檢視
檢視負責定義使用者在畫面上看到的內容結構、版面配置和外觀。 在理想情況下,每個檢視都是在 XAML 中定義,其中包含不包含商業規則的有限程式代碼後置。 但有時候,程式碼後置可能包含 UI 邏輯,以實作難以利用 XAML 表現的視覺行為,例如動畫。
ViewModel
檢視模型會實作檢視可繫結資料的屬性和命令,並透過變更通知事件通知檢視任何狀態變更。 檢視模型提供的屬性和命令會定義 UI 所提供的功能,但檢視會決定如何顯示該功能。
檢視模型也負責協調檢視與任何所需模型類別的互動。 檢視模型與模型類別之間通常有一對多關聯性。
每個檢視模型都會使用檢視可輕易取用的格式提供模型資料。 為此,檢視模型有時會執行資料轉換。 將此資料轉換放在檢視模型中是個不錯的主意,因為它提供檢視可繫結的屬性。 例如,檢視模型可能會結合兩個屬性的值,方便檢視顯示。
重要
.NET MAUI 會將系結更新封送處理至 UI 線程。 使用MVVM時,這可讓您從任何線程更新數據系結 ViewModel 屬性,並使用 .NET MAUI 的系結引擎將更新帶入 UI 線程。
Model
模型類別是封裝應用程式資料的非視覺類別。 因此,您可以將模型視為代表應用程式定義域的模型,這通常包含資料模型以及商業和驗證邏輯。
更新模型
在本教學課程的第一個部分中,您將實作model-view-viewmodel (MVVM) 模式。 若要開始,請在Visual Studio中開啟 Notes.sln 方案。
清除模型
在上一個教學課程中,模型類型會同時作為模型(數據)和檢視模型(數據準備),而該模型直接對應至檢視。 下表描述模型:
程式碼檔案 | 描述 |
---|---|
Models/About.cs | 模型 About 。 包含描述應用程式本身的唯讀欄位,例如應用程式標題和版本。 |
Models/Note.cs | 模型 Note 。 表示附注。 |
Models/AllNotes.cs | 模型 AllNotes 。 將裝置上的所有附註載至集合。 |
考慮應用程式本身時,應用程式只會使用一段數據,也就是 Note
。 筆記會從裝置載入、儲存至裝置,以及透過應用程式 UI 進行編輯。 和模型確實不需要 About
AllNotes
。 從項目中移除這些模型:
- 尋找 Visual Studio 的 [方案總管] 窗格。
- 以滑鼠右鍵按兩下 Models\About.cs 檔案,然後選取 [ 刪除]。 按 [確定 ] 以刪除檔案。
- 以滑鼠右鍵按兩下 Models\AllNotes.cs 檔案,然後選取 [ 刪除]。 按 [確定 ] 以刪除檔案。
剩餘的唯一模型檔案是 Models\Note.cs 檔案。
更新模型
此 Note
模型包含:
- 唯一標識符,這是儲存在裝置上的記事檔名。
- 附註的文字。
- 指出已建立或上次更新附註的日期。
目前,載入和儲存模型是透過檢視來完成,在某些情況下,是由您剛才移除的其他模型類型所完成。 您為 Note
類型所擁有的程式代碼應該是下列專案:
namespace Notes.Models;
internal class Note
{
public string Filename { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
}
模型 Note
將會展開以處理載入、儲存和刪除筆記。
在 Visual Studio 的 [方案總管] 窗格中,按兩下 Models\Note.cs。
在程式代碼編輯器中,將下列兩種方法新增至
Note
類別。 這些方法是以實例為基礎,並分別處理儲存或刪除裝置的目前附註:public void Save() => File.WriteAllText(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename), Text); public void Delete() => File.Delete(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename));
應用程式需要以兩種方式載入筆記,從檔案載入個別筆記,並在裝置上載入所有筆記。 要處理載入的程式代碼可以是
static
成員,不需要執行類別實例。將下列程式代碼新增至 類別,以依檔名載入附註:
public static Note Load(string filename) { filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename); if (!File.Exists(filename)) throw new FileNotFoundException("Unable to find file on local storage.", filename); return new() { Filename = Path.GetFileName(filename), Text = File.ReadAllText(filename), Date = File.GetLastWriteTime(filename) }; }
此程式代碼會採用檔名做為參數、建置儲存在裝置上筆記的路徑,並在檔案存在時嘗試載入檔案。
載入筆記的第二種方式是列舉裝置上的所有筆記,並將其載入集合中。
將下列程式碼新增至 類別:
public static IEnumerable<Note> LoadAll() { // Get the folder where the notes are stored. string appDataPath = FileSystem.AppDataDirectory; // Use Linq extensions to load the *.notes.txt files. return Directory // Select the file names from the directory .EnumerateFiles(appDataPath, "*.notes.txt") // Each file name is used to load a note .Select(filename => Note.Load(Path.GetFileName(filename))) // With the final collection of notes, order them by date .OrderByDescending(note => note.Date); }
此程式代碼會擷取符合附注檔案模式之裝置上的檔案,以傳回模型類型的可列舉集合
Note
: *.notes.txt。 每個檔名都會傳遞至Load
方法,並載入個別的附註。 最後,筆記的集合會依每個附註的日期排序,並傳回給呼叫端。最後,將建構函式新增至 類別,以設定屬性的預設值,包括隨機檔名:
public Note() { Filename = $"{Path.GetRandomFileName()}.notes.txt"; Date = DateTime.Now; Text = ""; }
類別 Note
程式代碼看起來應該如下所示:
namespace Notes.Models;
internal class Note
{
public string Filename { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
public Note()
{
Filename = $"{Path.GetRandomFileName()}.notes.txt";
Date = DateTime.Now;
Text = "";
}
public void Save() =>
File.WriteAllText(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename), Text);
public void Delete() =>
File.Delete(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename));
public static Note Load(string filename)
{
filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename);
if (!File.Exists(filename))
throw new FileNotFoundException("Unable to find file on local storage.", filename);
return
new()
{
Filename = Path.GetFileName(filename),
Text = File.ReadAllText(filename),
Date = File.GetLastWriteTime(filename)
};
}
public static IEnumerable<Note> LoadAll()
{
// Get the folder where the notes are stored.
string appDataPath = FileSystem.AppDataDirectory;
// Use Linq extensions to load the *.notes.txt files.
return Directory
// Select the file names from the directory
.EnumerateFiles(appDataPath, "*.notes.txt")
// Each file name is used to load a note
.Select(filename => Note.Load(Path.GetFileName(filename)))
// With the final collection of notes, order them by date
.OrderByDescending(note => note.Date);
}
}
Note
現在模型已完成,就可以建立檢視模型。
建立 About viewmodel
將檢視模型新增至專案之前,請先新增MVVM Community Toolkit 的參考。 此連結庫可在 NuGet 上使用,並提供可協助實作 MVVM 模式的類型和系統。
在 Visual Studio 的 [方案總管] 窗格中,以滑鼠右鍵按兩下 [記事] 專案 >[管理 NuGet 套件]。
選取 [瀏覽] 索引標籤。
搜尋 communitytoolkit mvvm 並選取
CommunityToolkit.Mvvm
套件,這應該是第一個結果。請確定已選取至少第8版。 本教學課程是使用8.0.0版撰寫。
接下來,選取 [ 安裝 ],並接受任何顯示的提示。
現在您已準備好藉由新增檢視模型來開始更新專案。
與檢視模型分離
view-to-viewmodel 關聯性嚴重依賴 .NET 多平臺應用程式 UI (.NET MAUI) 所提供的系結系統。 應用程式已在檢視中使用系結來顯示筆記清單,並呈現單一筆記的文字和日期。 應用程式邏輯目前由檢視的程式代碼後置提供,而且會直接系結至檢視。 例如,當使用者編輯筆記並按下 [儲存 ] 按鈕時, Clicked
就會引發按鈕的事件。 然後,事件處理程式的程式代碼後置會將記事文字儲存至檔案,並流覽至上一個畫面。
在檢視的程式代碼後置中擁有應用程式邏輯可能會成為檢視變更時的問題。 例如,如果按鈕取代為不同的輸入控件,或變更控件的名稱,事件處理程式可能會變成無效。 無論檢視的設計方式為何,檢視的目的是叫用某種應用程式邏輯,並將信息呈現給使用者。 針對此應用程式, Save
按鈕會儲存附註,然後流覽回上一個畫面。
viewmodel 會為應用程式提供特定位置,以放置應用程式邏輯,而不論 UI 的設計方式或數據的載入或儲存方式為何。 viewmodel 是代表並代表檢視與數據模型互動的黏附。
檢視模型會儲存在 ViewModels 資料夾中。
- 尋找 Visual Studio 的 [方案總管] 窗格。
- 以滑鼠右鍵按下 Notes 專案,然後選取 [ 新增>資料夾]。 將資料夾 命名為 ViewModels。
- 以滑鼠右鍵按下 ViewModels 資料夾> [新增>類別],並將其命名為AboutViewModel.cs。
- 重複上一個步驟,再建立兩個檢視模型:
- NoteViewModel.cs
- NotesViewModel.cs
您的專案結構應該看起來如下圖:
關於 viewmodel 和 About view
[ 關於] 檢視 會在畫面上顯示一些數據,並選擇性地流覽至具有詳細信息的網站。 由於此檢視沒有任何數據要變更,例如使用文字輸入控件或從清單中選取專案,所以示範新增 viewmodel 是很好的候選專案。 針對 About viewmodel,沒有支援模型。
建立 About viewmodel:
在 Visual Studio 的 [方案總管] 窗格中,按兩下 ViewModels\AboutViewModel.cs。
貼上下列程式碼:
using CommunityToolkit.Mvvm.Input; using System.Windows.Input; namespace Notes.ViewModels; internal class AboutViewModel { public string Title => AppInfo.Name; public string Version => AppInfo.VersionString; public string MoreInfoUrl => "https://aka.ms/maui"; public string Message => "This app is written in XAML and C# with .NET MAUI."; public ICommand ShowMoreInfoCommand { get; } public AboutViewModel() { ShowMoreInfoCommand = new AsyncRelayCommand(ShowMoreInfo); } async Task ShowMoreInfo() => await Launcher.Default.OpenAsync(MoreInfoUrl); }
先前的代碼段包含一些屬性,這些屬性代表應用程式的相關信息,例如名稱和版本。 此代碼段與您稍早刪除的 About 模型 完全相同。 不過,這個 viewmodel 包含新的概念 ShowMoreInfoCommand
命令屬性。
命令是可系結的動作,可叫用程式代碼,而且是放置應用程式邏輯的絕佳位置。 在此範例中,指向 ShowMoreInfoCommand
ShowMoreInfo
方法,此方法會將網頁瀏覽器開啟至特定頁面。 在下一節中,您將深入瞭解命令系統。
關於檢視
[關於] 檢視需要稍微變更,才能將其連結至上一節中建立的 viewModel。 在 Views\AboutPage.xaml 檔案中,套用下列變更:
- 將
xmlns:models
XML 命名空間更新為xmlns:viewModels
,並將目標設為Notes.ViewModels
.NET 命名空間。 - 將
ContentPage.BindingContext
屬性變更為 viewmodel 的新實例About
。 - 拿掉按鈕的
Clicked
事件處理程式,並使用Command
屬性。
更新 [關於] 檢視:
在 Visual Studio 的 [方案總管] 窗格中,按兩下 Views\AboutPage.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" xmlns:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.AboutPage"> <ContentPage.BindingContext> <viewModels:AboutViewModel /> </ContentPage.BindingContext> <VerticalStackLayout Spacing="10" Margin="10"> <HorizontalStackLayout Spacing="10"> <Image Source="dotnet_bot.png" SemanticProperties.Description="The dot net bot waving hello!" HeightRequest="64" /> <Label FontSize="22" FontAttributes="Bold" Text="{Binding Title}" VerticalOptions="End" /> <Label FontSize="22" Text="{Binding Version}" VerticalOptions="End" /> </HorizontalStackLayout> <Label Text="{Binding Message}" /> <Button Text="Learn more..." Command="{Binding ShowMoreInfoCommand}" /> </VerticalStackLayout> </ContentPage>
先前的代碼段會反白顯示此檢視版本中已變更的行。
請注意,按鈕正在使用 Command
屬性。 許多控制件都有 Command
當使用者與控件互動時叫用的屬性。 搭配按鈕使用時,會在使用者按下按鈕時叫用 命令,類似於叫用事件處理程式的方式 Clicked
,不同之處在於您可以系結 Command
至 viewmodel 中的屬性。
在這裡檢視中,當使用者按下按鈕時, Command
會叫用 。 Command
系結至 ShowMoreInfoCommand
viewmodel 中的 屬性,並在叫用時,在方法中ShowMoreInfo
執行程式碼,以將網頁瀏覽器開啟至特定頁面。
清除關於程序代碼後置
ShowMoreInfo
按鈕未使用事件處理程式,因此應該從 Views\AboutPage.xaml.cs 檔案中移除程式LearnMore_Clicked
代碼。 刪除該程式代碼,類別應該只包含建構函式:
在 Visual Studio 的 [方案總管] 窗格中,按兩下 Views\AboutPage.xaml.cs。
提示
您可能需要展開 Views\AboutPage.xaml 以顯示檔案。
使用下列片段取代程式碼:
namespace Notes.Views; public partial class AboutPage : ContentPage { public AboutPage() { InitializeComponent(); } }
建立 Note viewmodel
更新 Note 檢視的目標是將盡可能多的功能移出 XAML 程式代碼後置,並將其放入 Note viewmodel 中。
附註 viewmodel
根據 Note 檢視需要的內容,Note viewmodel 必須提供下列專案:
- 附註的文字。
- 建立筆記或上次更新的日期/時間。
- 儲存附註的命令。
- 刪除附註的命令。
建立 Note viewmodel:
在 Visual Studio 的 [方案總管] 窗格中,按兩下 ViewModels\NoteViewModel.cs。
以下欄代碼段取代此檔案中的程式代碼:
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.ComponentModel; using System.Windows.Input; namespace Notes.ViewModels; internal class NoteViewModel : ObservableObject, IQueryAttributable { private Models.Note _note; }
此程式代碼是空白
Note
viewmodel,您將在其中新增屬性和命令以支持Note
檢視。 請注意,CommunityToolkit.Mvvm.ComponentModel
命名空間正在匯入。 這個命名空間會提供ObservableObject
做為基類的 。 您將在下一個步驟中深入瞭解ObservableObject
。 命名空間CommunityToolkit.Mvvm.Input
也會匯入。 這個命名空間提供一些以異步方式叫用方法的命令類型。模型
Models.Note
會儲存為私人欄位。 這個類別的屬性和方法將會使用此欄位。將下列屬性新增至 類別:
public string Text { get => _note.Text; set { if (_note.Text != value) { _note.Text = value; OnPropertyChanged(); } } } public DateTime Date => _note.Date; public string Identifier => _note.Filename;
Date
和Identifier
屬性只是從模型擷取對應值的簡單屬性。提示
針對屬性,語法
=>
會建立僅限 get 屬性,其中 右邊的=>
語句必須評估為要傳回的值。屬性
Text
會先檢查所設定的值是否為不同的值。 如果值不同,該值會傳遞至模型的 屬性,並OnPropertyChanged
呼叫 方法。方法
OnPropertyChanged
是由ObservableObject
基類所提供。 這個方法會使用呼叫程式代碼的名稱,在此案例中為 Text 的屬性名稱,並引發ObservableObject.PropertyChanged
事件。 這個事件會將屬性的名稱提供給任何事件訂閱者。 .NET MAUI 所提供的系結系統可辨識此事件,並更新 UI 中的任何相關係結。 針對 Note viewmodel,當Text
屬性變更時,就會引發 事件,而系結至Text
屬性的任何 UI 元素會通知屬性已變更。將下列命令屬性新增至 類別,這是檢視可以繫結至的命令:
public ICommand SaveCommand { get; private set; } public ICommand DeleteCommand { get; private set; }
將下列建構函式新增至 類別:
public NoteViewModel() { _note = new Models.Note(); SaveCommand = new AsyncRelayCommand(Save); DeleteCommand = new AsyncRelayCommand(Delete); } public NoteViewModel(Models.Note note) { _note = note; SaveCommand = new AsyncRelayCommand(Save); DeleteCommand = new AsyncRelayCommand(Delete); }
這兩個建構函式可用來建立具有新備份模型的 viewmodel,也就是空的附註,或建立使用指定模型實例的 ViewModel。
建構函式也會設定 viewmodel 的命令。 接下來,新增這些命令的程序代碼。
Save
新增 與Delete
方法:private async Task Save() { _note.Date = DateTime.Now; _note.Save(); await Shell.Current.GoToAsync($"..?saved={_note.Filename}"); } private async Task Delete() { _note.Delete(); await Shell.Current.GoToAsync($"..?deleted={_note.Filename}"); }
這些方法是由相關聯的命令叫用。 他們會在模型上執行相關動作,並讓應用程式巡覽至上一頁。 查詢字串參數會新增至
..
瀏覽路徑,指出已採取哪些動作,以及記事的唯一標識符。接下來,將
ApplyQueryAttributes
方法新增至 類別,以符合 介面的需求 IQueryAttributable :void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { if (query.ContainsKey("load")) { _note = Models.Note.Load(query["load"].ToString()); RefreshProperties(); } }
當頁面或頁面的系結內容實作這個介面時,巡覽中使用的查詢字串參數會傳遞至
ApplyQueryAttributes
方法。 此 viewmodel 會作為附注檢視的系結內容使用。 巡覽至 [附注] 檢視時,檢視的系結內容 (此 viewmodel) 會傳遞巡覽期間所使用的查詢字串參數。此程式代碼會檢查索引鍵是否
load
在字典中query
提供。 如果找到此索引鍵,此值應該是要載入之附注的標識碼(檔名)。 這個附注會載入並設定為這個 viewmodel 實例的基礎模型物件。最後,將這兩個協助程式方法新增至 類別:
public void Reload() { _note = Models.Note.Load(_note.Filename); RefreshProperties(); } private void RefreshProperties() { OnPropertyChanged(nameof(Text)); OnPropertyChanged(nameof(Date)); }
方法是
Reload
協助程式方法,可重新整理支援模型物件,並從裝置記憶體重載它方法
RefreshProperties
是另一個協助程式方法,可確保系結至此物件的任何訂閱者都會收到通知,Text
指出和Date
屬性已變更。 由於在導覽Text
期間載入附註時會變更基礎模型(_note
欄位),因此和Date
屬性實際上不會設定為新的值。 由於這些屬性並未直接設定,因此附加至這些屬性的任何系結都不會收到通知,因為OnPropertyChanged
不會針對每個屬性呼叫 。RefreshProperties
確保重新整理這些屬性的系結。
類別的程式代碼看起來應該像下列代碼段:
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Windows.Input;
namespace Notes.ViewModels;
internal class NoteViewModel : ObservableObject, IQueryAttributable
{
private Models.Note _note;
public string Text
{
get => _note.Text;
set
{
if (_note.Text != value)
{
_note.Text = value;
OnPropertyChanged();
}
}
}
public DateTime Date => _note.Date;
public string Identifier => _note.Filename;
public ICommand SaveCommand { get; private set; }
public ICommand DeleteCommand { get; private set; }
public NoteViewModel()
{
_note = new Models.Note();
SaveCommand = new AsyncRelayCommand(Save);
DeleteCommand = new AsyncRelayCommand(Delete);
}
public NoteViewModel(Models.Note note)
{
_note = note;
SaveCommand = new AsyncRelayCommand(Save);
DeleteCommand = new AsyncRelayCommand(Delete);
}
private async Task Save()
{
_note.Date = DateTime.Now;
_note.Save();
await Shell.Current.GoToAsync($"..?saved={_note.Filename}");
}
private async Task Delete()
{
_note.Delete();
await Shell.Current.GoToAsync($"..?deleted={_note.Filename}");
}
void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.ContainsKey("load"))
{
_note = Models.Note.Load(query["load"].ToString());
RefreshProperties();
}
}
public void Reload()
{
_note = Models.Note.Load(_note.Filename);
RefreshProperties();
}
private void RefreshProperties()
{
OnPropertyChanged(nameof(Text));
OnPropertyChanged(nameof(Date));
}
}
附注檢視
現在已建立 viewmodel,請更新 [附注] 檢視。 在 Views\NotePage.xaml 檔案中,套用下列變更:
xmlns:viewModels
新增以 .NET 命名空間為目標的Notes.ViewModels
XML 命名空間。BindingContext
將 新增至頁面。- 拿掉刪除並儲存按鈕
Clicked
事件處理程式,並以命令取代它們。
更新附 註檢視:
- 在 Visual Studio 的 [方案總管] 窗格中,按兩下 Views\NotePage.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"
xmlns:viewModels="clr-namespace:Notes.ViewModels"
x:Class="Notes.Views.NotePage"
Title="Note">
<ContentPage.BindingContext>
<viewModels:NoteViewModel />
</ContentPage.BindingContext>
<VerticalStackLayout Spacing="10" Margin="5">
<Editor x:Name="TextEditor"
Placeholder="Enter your note"
Text="{Binding Text}"
HeightRequest="100" />
<Grid ColumnDefinitions="*,*" ColumnSpacing="4">
<Button Text="Save"
Command="{Binding SaveCommand}"/>
<Button Grid.Column="1"
Text="Delete"
Command="{Binding DeleteCommand}"/>
</Grid>
</VerticalStackLayout>
</ContentPage>
先前,此檢視並未宣告系結內容,因為它是由頁面本身的程式代碼後置所提供。 直接在 XAML 中設定系結內容提供兩件事:
在運行時間,當頁面流覽至時,它會顯示空白的附註。 這是因為叫用系結內容 viewmodel 的無參數建構函式。 如果您記得正確,Note viewmodel 的無參數建構函式會建立空白的附注。
XAML 編輯器中的 Intellisense 會在您開始輸入
{Binding
語法時顯示可用的屬性。 語法也會經過驗證,並警示您無效的值。 請嘗試將 的SaveCommand
系結語法變更為Save123Command
。 如果您將滑鼠游標停留在文字上方,您會發現工具提示會顯示,通知您 找不到 Save123Command 。 因為系結是動態的,所以此通知不會被視為錯誤,所以這實際上是一個小警告,可協助您在輸入錯誤屬性時注意到。如果您將 SaveCommand 變更為不同的值,請立即還原它。
清除附注程式代碼後置
現在,與檢視的互動已從事件處理程式變更為命令,請開啟 Views\NotePage.xaml.cs 檔案,並將所有程式代碼取代為只包含建構函式的類別:
在 Visual Studio 的 [方案總管] 窗格中,按兩下 Views\NotePage.xaml.cs。
提示
您可能需要展開 Views\NotePage.xaml 以顯示檔案。
使用下列片段取代程式碼:
namespace Notes.Views; public partial class NotePage : ContentPage { public NotePage() { InitializeComponent(); } }
建立 Notes viewmodel
最後一個 viewmodel-view 配對是 Notes viewmodel 和 AllNotes 檢視。 不過,目前檢視會直接系結至本教學課程開始時已刪除的模型。 更新 AllNotes 檢視 的目標是盡可能將功能移出 XAML 程式代碼後置,並將其放入 viewmodel 中。 同樣地,檢視可以變更其設計,對您的程式代碼影響不大的好處。
Notes viewmodel
根據 AllNotes 檢視要顯示的內容,以及使用者將執行的互動,Notes viewmodel 必須提供下列專案:
- 附註的集合。
- 用來處理巡覽至附註的命令。
- 建立新附註的命令。
- 在建立、刪除或變更筆記時更新筆記清單。
建立 Notes viewmodel:
在 Visual Studio 的 [方案總管] 窗格中,按兩下 ViewModels\NotesViewModel.cs。
以下欄程序代碼取代此檔案中的程式代碼:
using CommunityToolkit.Mvvm.Input; using System.Collections.ObjectModel; using System.Windows.Input; namespace Notes.ViewModels; internal class NotesViewModel: IQueryAttributable { }
此程式代碼是空白
NotesViewModel
,您將在其中新增屬性和命令以支持AllNotes
檢視。在類別程式代碼中
NotesViewModel
,新增下列屬性:public ObservableCollection<ViewModels.NoteViewModel> AllNotes { get; } public ICommand NewCommand { get; } public ICommand SelectNoteCommand { get; }
屬性
AllNotes
是ObservableCollection
,會儲存從裝置載入的所有筆記。 檢視會使用這兩個命令來觸發建立附註或選取現有附註的動作。將無參數建構函式新增至 類別,以初始化命令,並從模型載入附註:
public NotesViewModel() { AllNotes = new ObservableCollection<ViewModels.NoteViewModel>(Models.Note.LoadAll().Select(n => new NoteViewModel(n))); NewCommand = new AsyncRelayCommand(NewNoteAsync); SelectNoteCommand = new AsyncRelayCommand<ViewModels.NoteViewModel>(SelectNoteAsync); }
請注意,
AllNotes
集合會使用Models.Note.LoadAll
方法,將可觀察的集合填入附注。 方法會將LoadAll
附注當做Models.Note
型別傳回,但可觀察的集合是型別的ViewModels.NoteViewModel
集合。 程序代碼會Select
使用 Linq 延伸模組,從 從LoadAll
傳回的附注模型建立 viewmodel 實例。建立命令的目標方法:
private async Task NewNoteAsync() { await Shell.Current.GoToAsync(nameof(Views.NotePage)); } private async Task SelectNoteAsync(ViewModels.NoteViewModel note) { if (note != null) await Shell.Current.GoToAsync($"{nameof(Views.NotePage)}?load={note.Identifier}"); }
請注意,
NewNoteAsync
方法在 時不會採用 參數SelectNoteAsync
。 命令可以選擇性地具有叫用命令時所提供的單一參數。SelectNoteAsync
針對方法,參數代表正在選取的附注。最後,實作
IQueryAttributable.ApplyQueryAttributes
方法:void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { if (query.ContainsKey("deleted")) { string noteId = query["deleted"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note exists, delete it if (matchedNote != null) AllNotes.Remove(matchedNote); } else if (query.ContainsKey("saved")) { string noteId = query["saved"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note is found, update it if (matchedNote != null) matchedNote.Reload(); // If note isn't found, it's new; add it. else AllNotes.Add(new NoteViewModel(Note.Load(noteId))); } }
在 上一個教學課程步驟中建立的 Note viewmodel ,會在儲存或刪除附注時使用導覽。 viewmodel 已巡覽回 AllNotes 檢視,此 ViewModel 與此 ViewModel 相關聯。 此程式代碼會偵測查詢字串是否包含
deleted
或saved
索引鍵。 索引鍵的值是附註的唯一標識碼。如果已刪除附注,該附注會由提供的標識符比對集合中
AllNotes
,並移除。儲存附注有兩個可能的原因。 已建立筆記,或已變更現有的附註。 如果附注已經在集合中
AllNotes
,則它是已更新的附註。 在此情況下,集合中的附註實例只需要重新整理。 如果集合中遺漏附注,則它是新的附注,而且必須新增至集合。
類別的程式代碼看起來應該像下列代碼段:
using CommunityToolkit.Mvvm.Input;
using Notes.Models;
using System.Collections.ObjectModel;
using System.Windows.Input;
namespace Notes.ViewModels;
internal class NotesViewModel : IQueryAttributable
{
public ObservableCollection<ViewModels.NoteViewModel> AllNotes { get; }
public ICommand NewCommand { get; }
public ICommand SelectNoteCommand { get; }
public NotesViewModel()
{
AllNotes = new ObservableCollection<ViewModels.NoteViewModel>(Models.Note.LoadAll().Select(n => new NoteViewModel(n)));
NewCommand = new AsyncRelayCommand(NewNoteAsync);
SelectNoteCommand = new AsyncRelayCommand<ViewModels.NoteViewModel>(SelectNoteAsync);
}
private async Task NewNoteAsync()
{
await Shell.Current.GoToAsync(nameof(Views.NotePage));
}
private async Task SelectNoteAsync(ViewModels.NoteViewModel note)
{
if (note != null)
await Shell.Current.GoToAsync($"{nameof(Views.NotePage)}?load={note.Identifier}");
}
void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.ContainsKey("deleted"))
{
string noteId = query["deleted"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note exists, delete it
if (matchedNote != null)
AllNotes.Remove(matchedNote);
}
else if (query.ContainsKey("saved"))
{
string noteId = query["saved"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note is found, update it
if (matchedNote != null)
matchedNote.Reload();
// If note isn't found, it's new; add it.
else
AllNotes.Add(new NoteViewModel(Note.Load(noteId)));
}
}
}
AllNotes 檢視
現在已建立 viewmodel,請更新 AllNotes 檢視 以指向 viewmodel 屬性。 在 Views\AllNotesPage.xaml 檔案中,套用下列變更:
xmlns:viewModels
新增以 .NET 命名空間為目標的Notes.ViewModels
XML 命名空間。BindingContext
將 新增至頁面。- 拿掉工具列按鈕的事件
Clicked
,並使用Command
屬性。 CollectionView
變更變更為將變更為將變更ItemSource
。AllNotes
CollectionView
變更 以在選取的項目變更時使用命令來回應。
更新 AllNotes 檢視:
在 Visual Studio 的 [方案總管] 窗格中,按兩下 Views\AllNotesPage.xaml。
貼上下列程式碼:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.AllNotesPage" Title="Your Notes"> <ContentPage.BindingContext> <viewModels:NotesViewModel /> </ContentPage.BindingContext> <!-- Add an item to the toolbar --> <ContentPage.ToolbarItems> <ToolbarItem Text="Add" Command="{Binding NewCommand}" IconImageSource="{FontImage Glyph='+', Color=Black, Size=22}" /> </ContentPage.ToolbarItems> <!-- Display notes in a list --> <CollectionView x:Name="notesCollection" ItemsSource="{Binding AllNotes}" Margin="20" SelectionMode="Single" SelectionChangedCommand="{Binding SelectNoteCommand}" SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}"> <!-- Designate how the collection of items are laid out --> <CollectionView.ItemsLayout> <LinearItemsLayout Orientation="Vertical" ItemSpacing="10" /> </CollectionView.ItemsLayout> <!-- Define the appearance of each item in the list --> <CollectionView.ItemTemplate> <DataTemplate> <StackLayout> <Label Text="{Binding Text}" FontSize="22"/> <Label Text="{Binding Date}" FontSize="14" TextColor="Silver"/> </StackLayout> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </ContentPage>
工具列不再使用 Clicked
事件,而是使用 命令。
CollectionView
支援使用和 SelectionChangedCommandParameter
屬性的SelectionChangedCommand
命令。 在更新的 XAML 中 SelectionChangedCommand
,屬性會系結至 viewmodel 的 SelectNoteCommand
,這表示當選取的項目變更時會叫用命令。 叫用命令時, SelectionChangedCommandParameter
屬性值會傳遞至 命令。
檢視 用於 的 CollectionView
系結:
<CollectionView x:Name="notesCollection"
ItemsSource="{Binding AllNotes}"
Margin="20"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectNoteCommand}"
SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
屬性 SelectionChangedCommandParameter
會使用 Source={RelativeSource Self}
系結。 參考 Self
目前的物件,也就是 CollectionView
。 請注意,系結路徑是 SelectedItem
屬性。 藉由變更選取的專案來叫用命令時, SelectNoteCommand
會叫用命令,並將選取的專案傳遞至命令做為參數。
清除 AllNotes 程式代碼後置
現在,與檢視的互動已從事件處理程式變更為命令,請開啟 Views\AllNotesPage.xaml.cs 檔案,並將所有程式代碼取代為只包含建構函式的類別:
在 Visual Studio 的 [方案總管] 窗格中,按兩下 Views\AllNotesPage.xaml.cs。
提示
您可能需要展開 Views\AllNotesPage.xaml 以顯示檔案。
使用下列片段取代程式碼:
namespace Notes.Views; public partial class AllNotesPage : ContentPage { public AllNotesPage() { InitializeComponent(); } }
執行應用程式
您現在可以執行應用程式,而且所有專案都正常運作。 不過,應用程式的行為有兩個問題:
- 如果您選取可開啟編輯器的附註,請按 [儲存],然後嘗試選取相同的附註,則無法運作。
- 每當變更或新增附註時,便不會重新排列附註清單,以顯示最上方的最新筆記。
下一個教學課程步驟已修正這兩個問題。
修正應用程式行為
既然應用程式程式代碼可以編譯並執行,您可能會注意到應用程式的行為有兩個缺陷。 應用程式不會讓您重新選取已選取的附註,而且在建立或變更筆記之後,便不會重新排序筆記清單。
取得清單頂端的附註
首先,修正附注清單的重新排序問題。 在 ViewModels\NotesViewModel.cs 檔案中 AllNotes
,集合包含要向用戶呈現的所有附注。 不幸的是,使用 的 ObservableCollection
缺點是必須手動排序。 若要將新的或更新的項目移至清單頂端,請執行下列步驟:
在 Visual Studio 的 [方案總管] 窗格中,按兩下 ViewModels\NotesViewModel.cs。
在方法中
ApplyQueryAttributes
,查看已儲存查詢字串索引鍵的邏輯。matchedNote
當 不是null
時,會更新附註。AllNotes.Move
使用 方法將 移至matchedNote
索引 0,也就是清單頂端。string noteId = query["saved"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note is found, update it if (matchedNote != null) { matchedNote.Reload(); AllNotes.Move(AllNotes.IndexOf(matchedNote), 0); }
方法
AllNotes.Move
會採用兩個參數來移動物件在集合中的位置。 第一個參數是要移動的物件索引,而第二個參數是移動物件的位置索引。 方法AllNotes.IndexOf
會擷取附注的索引。matchedNote
當 為null
時,附注是新的 ,而且正在加入清單中。 與其新增它,而是將附註附加至清單結尾,請將附註插入索引 0,也就是清單頂端。 將AllNotes.Add
方法變更為AllNotes.Insert
。string noteId = query["saved"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note is found, update it if (matchedNote != null) { matchedNote.Reload(); AllNotes.Move(AllNotes.IndexOf(matchedNote), 0); } // If note isn't found, it's new; add it. else AllNotes.Insert(0, new NoteViewModel(Models.Note.Load(noteId)));
方法 ApplyQueryAttributes
看起來應該像下列代碼段:
void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.ContainsKey("deleted"))
{
string noteId = query["deleted"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note exists, delete it
if (matchedNote != null)
AllNotes.Remove(matchedNote);
}
else if (query.ContainsKey("saved"))
{
string noteId = query["saved"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note is found, update it
if (matchedNote != null)
{
matchedNote.Reload();
AllNotes.Move(AllNotes.IndexOf(matchedNote), 0);
}
// If note isn't found, it's new; add it.
else
AllNotes.Insert(0, new NoteViewModel(Models.Note.Load(noteId)));
}
}
允許選取筆記兩次
在 AllNotes 檢視中,會 CollectionView
列出所有附注,但不允許您選取相同的筆記兩次。 有兩種方式可讓專案保持選取:當使用者變更現有的附註,以及用戶強制向後巡覽時。 使用的上一節 AllNotes.Move
中的程式代碼變更修正了用戶儲存附註的情況,因此您不必擔心這種情況。
您現在必須解決的問題與瀏覽有關。 無論 Allnotes 檢視如何巡覽至 ,NavigatedTo
都會引發頁面的事件。 此事件是強制取消選取 中 CollectionView
所選專案的完美位置。
不過,在此套用MVVM模式時,viewmodel 無法直接在檢視上觸發某些專案,例如在儲存附註之後清除選取的專案。 那麼,你如何做到這一點? MVVM 模式的良好實作可將檢視中的程序代碼後置降到最低。 有幾種不同的方式可以解決此問題,以支援MVVM分離模式。 不過,您也可以將程式代碼放在檢視的程式代碼後置中,特別是當它直接系結至檢視時。 MVVM 有許多絕佳的設計和概念,可協助您分割應用程式、改善可維護性,並讓您更輕鬆地新增新功能。 不過,在某些情況下,您可能會發現MVVM鼓勵過度工程。
請勿過度產生此問題的解決方案,而只需使用 NavigatedTo
事件從 清除選取的專案 CollectionView
即可。
在 Visual Studio 的 [方案總管] 窗格中,按兩下 Views\AllNotesPage.xaml。
在 的 XAML 中
<ContentPage>
,新增NavigatedTo
事件:<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.AllNotesPage" Title="Your Notes" NavigatedTo="ContentPage_NavigatedTo"> <ContentPage.BindingContext> <viewModels:NotesViewModel /> </ContentPage.BindingContext>
您可以以滑鼠右鍵按下事件方法名稱,然後選取 [移至定義],
ContentPage_NavigatedTo
以新增預設事件處理程式。 此動作會在 程式代碼編輯器中開啟 Views\AllNotesPage.xaml.cs 。以下欄代碼段取代事件處理程式程式代碼:
private void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e) { notesCollection.SelectedItem = null; }
在 XAML 中,
CollectionView
已指定 的名稱notesCollection
。 此程式代碼會使用該名稱來存取CollectionView
,並將 設定SelectedItem
為null
。 每次巡覽頁面時,都會清除選取的專案。
現在,執行您的應用程式。 嘗試流覽至附注、按 [上一頁] 按鈕,然後再次選取相同的附注。 應用程式行為已修正!
探索本教學課程的程序代碼。 如果您要下載已完成項目的復本來比較您的程式代碼,請下載 此專案。
恭喜!
您的應用程式現在使用MVVM模式!
下一步
下列連結提供您在本教學課程中學到之一些概念的詳細資訊:
在這個區段有遇到問題嗎? 如果有,請提供意見反應,好讓我們可以改善這個區段。