MVVM の概念を使用してアプリをアップグレードする
このチュートリアル シリーズは、ノート作成アプリを作成した「.NET MAUI アプリを作成する」のチュートリアルを続行するように設計されています。 シリーズのこのパートで学習する内容は次のとおりです。
- Model-View-ViewModel (MVVM) パターンを実装します。
- ナビゲーション中にデータを渡すために、追加のスタイルのクエリ文字列を使用します。
そのチュートリアルで作成したコードがこのチュートリアルの基礎であるため、最初に「.NET MAUI アプリを作成する」のチュートリアルに従うことをお勧めします。 コードを紛失した場合、または新規に開始する場合は、このプロジェクトをダウンロードします。
MVVM について
.NET MAUI 開発者エクスペリエンスでは、通常、XAML でユーザー インターフェイスを作成してから、そのユーザー インターフェイスで動作する分離コードを追加します。 アプリが変更され、サイズとスコープが拡大すると、複雑なメンテナンスの問題が発生する可能性があります。 これらの問題には、UI コントロールとビジネス ロジックとの密結合が含まれます。これにより、UI の変更を行うコストが増加し、このようなコードを単体テストすることが困難になります。
Model-View-ViewModel (MVVM) パターンは、アプリケーションのビジネスとプレゼンテーション ロジックをそのユーザー インターフェイス (UI) からクリーンに分離するのに役立ちます。 アプリケーション ロジックと UI の間でクリーンな分離を維持することは、多くの開発の問題に対処するのに役立ち、アプリケーションのテスト、維持、改良が容易になります。 また、コード再利用の機会を大幅に向上させ、開発者や UI デザイナーがアプリのそれぞれの部分を開発するときに共同作業をより容易にできます。
パターン
MVVM パターンには、モデル、ビュー、ビュー モデルの 3 つの主要なコンポーネントがあります。 それぞれが異なる目的を果たします。 次の図は、3 つのコンポーネント間の関係を示します。
各コンポーネントの役割を理解するだけでなく、それらがどのようにやりとりするかを理解することも重要です。 大まかに言うと、ビューはビュー モデルを "認識" し、ビュー モデルはモデルを "認識" しますが、モデルはビュー モデルを認識しておらず、ビュー モデルはビューを認識していません。 したがって、ビュー モデルではビューをモデルから分離させ、ビューとは独立してモデルを進化させることができます。
MVVM を効果的に使用する鍵は、アプリ コードを正しいクラスに組み込む方法と、クラスがどのようにやりとりするかを理解することにあります。
表示
ビューには、ユーザーが画面に表示するものの構造、レイアウト、外観を定義する役割があります。 各ビューが XAML で定義され、ビジネス ロジックを含まない分離コードが限られていることが理想的です。 しかし、場合によっては、分離コードには、アニメーションなどの XAML で表現するのが困難な視覚的なビヘイビアーを実装する UI ロジックが含まれていることがあります。
ViewModel
ビュー モデルでは、ビューでデータ バインドできるプロパティとコマンドを実装し、変更通知イベントを通じて状態の変更をビューに通知します。 ビュー モデルで提供されるプロパティとコマンドでは、UI によって提供される機能を定義しますが、その機能の表示方法はビューで決定されます。
ビュー モデルには、必要なモデル クラスとビューのやりとりを調整する役割もあります。 通常、ビュー モデルとモデル クラスの間には一対多の関係があります。
各ビュー モデルでは、ビューで簡単に使用できる形式のモデルからのデータが提供されます。 これを実現するために、ビュー モデルではデータ変換を行うことがあります。 ビュー モデルにこのデータ変換を配置することをお勧めします。これにより、ビューがバインドできるプロパティが提供されるためです。 たとえば、ビュー モデルでは、2 つのプロパティの値を組み合わせて、ビューで表示しやすくすることができます。
重要
.NET MAUI は、UI スレッドへのバインディングの更新をマーシャリングします。 MVVM を使用すると、.NET MAUI のバインディング エンジンによって UI スレッドに更新が適用され、データバインドされた ViewModel プロパティを任意のスレッドから更新できます。
モデル
モデル クラスは、アプリのデータをカプセル化する非ビジュアル クラスです。 したがって、このモデルはアプリのドメイン モデルを表すものと考えることができます。通常、これにはビジネスおよび検証ロジックと共にデータ モデルが含まれます。
モデルを更新する
このチュートリアルの最初の部分では、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] ファイルを右クリックし、[削除] を選択します。 [OK] をクリックしてファイルを削除します。
- [Models\AllNotes.cs] ファイルを右クリックし、[削除] を選択します。 [OK] をクリックしてファイルを削除します。
残っているモデル ファイルは 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] をダブルクリックします。
コード エディタで、次の 2 つのメソッドを
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));
アプリでは、2 つの方法でメモを読み込む必要があります。ファイルから個々のメモを読み込む方法と、デバイス上のすべてのメモを読み込む方法です。 読み込みを処理するコードを
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) }; }
このコードは、ファイル名をパラメータとして受け取り、デバイス上でメモが格納されている場所へのパスを構築して、ファイルが存在する場合は読み込もうとします。
メモを読み込む 2 つ目の方法は、デバイス上のすべてのメモを列挙し、コレクションに読み込む方法です。
以下のコードを クラスに追加します。
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); }
このコードでは、メモ ファイル パターン *.notes.txt に一致するデバイス上のファイルを取得して、
Note
モデル型の列挙可能なコレクションを返します。 各ファイル名が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 ビューモデルを作成する
ビュー モデルをプロジェクトに追加する前に、MVVM Community Toolkit への参照を追加します。 このライブラリは NuGet で使用でき、MVVM パターンの実装に役立つ型とシステムを提供します。
Visual Studio の [ソリューション エクスプローラー] ウィンドウで、[メモ] プロジェクト >[NuGet パッケージの管理] を右クリックします。
[参照] タブを選択します。
communitytoolkit mvvm を検索し、最初の結果である
CommunityToolkit.Mvvm
パッケージを選択します。バージョン 8 以上が選択されていることを確認します。 このチュートリアルは、バージョン 8.0.0 を使用して作成されました。
次に、[インストール] を選択し、表示されるプロンプトをすべて受け入れます。
これで、ビュー モデルを追加してプロジェクトの更新を開始する準備ができました。
ビュー モデルを使用して分離する
ビューとビューモデルの関係は、.NET Multi-platform App UI (.NET MAUI) によって提供されるバインディング システムに大きく依存します。 このアプリでは、ビューでバインドを既に使用して、メモの一覧を表示し、1 つのノートのテキストと日付を表示しています。 アプリ ロジックは現在、ビューの分離コードによって提供され、ビューに直接関連付けられています。 たとえば、ユーザーがメモを編集しているときに [保存] ボタンを押すと、ボタンの Clicked
イベントが発生します。 次に、イベント ハンドラーの分離コードによってメモ テキストがファイルに保存され、前の画面に移動します。
ビューの分離コードにアプリ ロジックを含めると、ビューが変更されたときに問題になる場合があります。 たとえば、ボタンが別の入力コントロールに置き換えられたり、コントロールの名前が変更されたりすると、イベント ハンドラーが無効になる場合があります。 ビューの設計方法に関係なく、ビューの目的は、何らかの種類のアプリ ロジックを呼び出し、ユーザーに情報を提示することです。 このアプリでは、Save
ボタンでメモを保存し、前の画面に戻ります。
ビューモデルは、UI の設計方法やデータの読み込みまたは保存方法に関係なく、アプリ ロジックを配置するための特定な場所をアプリに提供します。 ビューモデルは、ビューの代わりにデータ モデルを表し、操作する接着剤になります。
ビュー モデルは ViewModels フォルダーに格納されます。
- Visual Studio で、[ソリューション エクスプローラー] ウィンドウを見つけます。
- [メモ] プロジェクトで右クリックし、[追加] > [新しいフォルダー] を選択します。 フォルダーに「ViewModels」という名前を付けます。
- [ViewModels] フォルダー > [追加] > [クラス] を右クリックし、「AboutViewModel.cs」という名前を付けます。
- 前の手順を繰り返し、さらに 2 つのビュー モデルを作成します。
- NoteViewModel.cs
- NotesViewModel.cs
プロジェクト構造は次の画像のようになります。
About ビューモデル と About ビュー
About ビュー では、画面にいくつかのデータが表示され、必要に応じて詳細情報を含む Web サイトに移動します。 このビューには、テキスト 入力コントロールやリストから項目を選択する場合など、変更するデータがないため、ビューモデルの追加を示すのが適しています。 About ビューモデル の場合、バッキング モデルはありません。
About ビューモデル を作成する
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 モデル とまったく同じです。 ただし、このビューモデルには、ShowMoreInfoCommand
コマンド プロパティという新しい概念が含まれています。
コマンドは、コードを呼び出すバインド可能なアクションであり、アプリ ロジックを配置するのに最適な場所です。 この例では、ShowMoreInfoCommand
は、Web ブラウザーに特定のページを開く ShowMoreInfo
メソッドを指しています。 コマンド システムについては、次のセクションで詳しく説明します。
About ビュー
前のセクションで作成したビューモデルに接続するには、About ビューを少し変更する必要があります。 Views\AboutPage.xaml ファイル内で、次のように変更します。
xmlns:models
XML 名前空間をxmlns:viewModels
に更新し、Notes.ViewModels
.NET 名前空間をターゲットにします。ContentPage.BindingContext
プロパティをAbout
ビューモデルの新しいインスタンスに変更します。- ボタンの
Clicked
イベント ハンドラーを削除し、Command
プロパティを使用します。
About ビューを次のように更新します。
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
をビューモデル内のプロパティにバインドできる点が異なります。
このビューでは、ユーザーがボタンを押すと、Command
が呼び出されます。 Command
はビューモデルの ShowMoreInfoCommand
プロパティにバインドされており、呼び出されると ShowMoreInfo
メソッドのコードが実行されて、Web ブラウザーに特定のページが表示されます。
About 分離コードをクリーンアップする
ShowMoreInfo
ボタンでイベント ハンドラーを使用していないので、LearnMore_Clicked
コードを Views\AboutPage.xaml.cs ファイルから削除する必要があります。 そのコードを削除すると、次のように、クラスはコンストラクターだけになります。
Visual Studio の [ソリューション エクスプローラー] ウインドウで、Views\AboutPage.xaml.cs をダブルクリックします。
ヒント
場合によっては、ファイルを表示するのに、Views\AboutPage.xaml を展開する必要があります。
コードを次のスニペットに置き換えます。
namespace Notes.Views; public partial class AboutPage : ContentPage { public AboutPage() { InitializeComponent(); } }
Note ビューモデルを作成する
メモ ビューを更新する目的は、XAML 分離コードからメモ ビューモデルにできるだけ多くの機能を移動することです。
メモ ビューモデル
メモ ビューに必要な情報に応じて、メモ ビューモデルは、次の項目を提供する必要があります。
- メモのテキスト。
- メモが作成または最後に更新された日付/時刻。
- メモを保存するコマンド。
- メモを削除するコマンド。
次の手順でメモ ビューモデルを作成します。
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
ビューをサポートするプロパティとコマンドを追加する空の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
プロパティは、対応する値をモデルから取得するだけの簡易なプロパティです。ヒント
プロパティの場合、
=>
構文は取得専用のプロパティを作成します。=>
の右側にあるステートメントの評価結果が戻り値になる必要があります。Text
プロパティは、設定される値が異なる値がどうかをまず確認します。 値が異なる場合、その値はモデルのプロパティに渡され、OnPropertyChanged
メソッドが呼び出されます。OnPropertyChanged
メソッドは、ObservableObject
基本クラスによって提供されます。 このメソッドは呼び出しコードの名前、この場合は Text のプロパティ名を使用し、ObservableObject.PropertyChanged
イベントを発生させます。 このイベントは、すべてのイベント サブスクライバーにプロパティの名前を提供します。 .NET MAUI によって提供されるバインド システムは、このイベントを認識し、UI 内の関連するバインディングを更新します。 メモ ビューモデルの場合、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); }
これら 2 つのコンストラクターを使用して、新しいバッキング モデル (空のメモ) でビューモデルを作成するか、指定したモデル インスタンスを使用するビューモデルを作成します。
コンストラクターも、ビューモデルのコマンドを設定します。 次に、これらのコマンドのコードを追加します。
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}"); }
これらのメソッドは、関連付けられているコマンドによって呼び出されます。 モデルに対して関連するアクションを実行し、アプリを前のページに移動します。 クエリ文字列パラメータが
..
ナビゲーション パスに追加され、実行されたアクションとメモの一意の識別子が示されます。次に、IQueryAttributable インターフェイスの要件を満たす
ApplyQueryAttributes
メソッドをクラスに追加します。void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { if (query.ContainsKey("load")) { _note = Models.Note.Load(query["load"].ToString()); RefreshProperties(); } }
ページまたはページのバインディング コンテキストがこのインターフェースを実装すると、ナビゲーションで使用されるクエリ文字列パラメーターが
ApplyQueryAttributes
メソッドに渡されます。 このビューモデルは、メモ ビューのバインディング コンテキストとして使用されます。 メモ ビューに移動すると、ビューのバインディング コンテキスト (このビューモデル) に、ナビゲーション中に使用されるクエリ文字列パラメーターが渡されます。このコードは、
load
キーがquery
ディクショナリに指定されているかどうかを確認します。 このキーが見つかった場合、その値は読み込むメモの識別子 (ファイル名) である必要があります。 このメモは、このビューモデル インスタンスの基になるモデル オブジェクトとして読み込まれ、設定されます。最後に、これら 2 つのヘルパー メソッドをクラスに追加します。
public void Reload() { _note = Models.Note.Load(_note.Filename); RefreshProperties(); } private void RefreshProperties() { OnPropertyChanged(nameof(Text)); OnPropertyChanged(nameof(Date)); }
Reload
メソッドは、バッキング モデル オブジェクトを更新し、デバイス ストレージから再読み込みするヘルパー メソッドです。RefreshProperties
メソッドは、このオブジェクトにバインドされているすべてのサブスクライバーに、Text
プロパティとDate
プロパティが変更されたことが確実に通知されるようにする別のヘルパー メソッドです。 基になるモデル (_note
フィールド) は、ナビゲーション中にメモが読み込まれるときに変更されるため、Text
プロパティと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));
}
}
メモ ビュー
ビュー モデルが作成されたので、メモ ビューを更新します。 Views\NotePage.xaml ファイルに次の変更を適用します。
Notes.ViewModels
.NET 名前空間をターゲットとするxmlns: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 でバインディング コンテキストを直接設定すると、次の 2 つのことが行われます。
実行時にページに移動すると、空白のメモが表示されます。 これは、バインディング コンテキストのパラメーターなしのコンストラクターであるビューモデルが呼び出されるためです。 正しく覚えている場合は、メモ ビューモデル のパラメーターなしのコンストラクターによって空白のメモが作成されます。
XAML エディターのインテリセンスでは、
{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 ビューモデルを作成する
ビューモデルとビューの最後のペアは、Notes ビューモデルと AllNotes ビューです。 ただし、現在、ビューはモデルに直接バインドされており、このチュートリアルの開始時に削除されました。 AllNotes ビューを更新する目的は、XAML 分離コードからビューモデルにできるだけ多くの機能を移動することです。 ここでも、ビューがコードにほとんど影響を与えずにデザインを変更できるという利点があります。
Notes ビューモデル
AllNotes ビューの表示内容とユーザーが行う対話的操作に応じて、Notes ビューモデルは次の項目を提供する必要があります。
- メモのコレクション。
- メモへの移動を処理するコマンド。
- 新しいメモを作成するコマンド。
- ノートの作成時、削除時、変更時に、ノートのリストを更新します。
次の手順で Notes ビューモデルを作成します。
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
です。 この 2 つのコマンドは、ノートの作成または既存ノートの選択のアクションをビューでトリガーするために使用されます。このクラスに、パラメーターのないコンストラクターを追加します。このコンストラクターは、コマンドが初期化し、モデルからメモを読み込みます。
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
から返されたメモ モデルからビューモデル インスタンスを作成します。次の手順で、コマンドによってターゲットされるメソッドを作成します。
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
メソッドはパラメーターを受け取ることに注意してください。 コマンドには、呼び出し時に指定されるオプション パラメーターが 1 つだけあります。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 ビューモデルでは、メモの保存時または削除時にナビゲーションを使用しました。 このビューモデルが関連付けられている AllNotes ビューに戻りました。 このコードでは、クエリ文字列に
deleted
またはsaved
のいずれかのキーが含まれているかどうかを検出します。 キーの値はメモの一意識別子です。メモが削除された場合、そのメモは
AllNotes
コレクション内で指定の識別子と照合され、削除されます。メモが保存される理由は 2 つ考えられます。 メモが作成されたばかりであるか、既存のノートが変更されたからです。 メモが既に
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 ビュー
これでビューモデルが作成されたので、ビューモデル プロパティを指すように AllNotes ビューを更新します。 Views\AllNotesPage.xaml ファイルで、次の変更を適用します。
Notes.ViewModels
.NET 名前空間をターゲットとするxmlns:viewModels
XML 名前空間を追加します。- ページに
BindingContext
を追加します。 - ツール バー ボタンの
Clicked
イベントを削除し、Command
プロパティを使用します。 ItemSource
をAllNotes
にバインドするようにCollectionView
を変更します。- 選択した項目が変更されたときに対応するためにコマンド実行を使用するように
CollectionView
を変更します。
次の手順で AllNotes View を更新します。
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
では SelectionChangedCommand
と SelectionChangedCommandParameter
のプロパティを使用したコマンド実行がサポートされています。 更新された XAML では、SelectionChangedCommand
プロパティはビューモデルの 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(); } }
アプリを実行する
これでアプリを実行できるようになり、すべてが機能しています。 ただし、アプリの動作には 2 つの問題があります。
- メモを選択すると、エディターが開き、[保存] を押してから同じメモを選択しようとすると、機能しません。
- ノートが変更または追加されるたびに、ノートの一覧が並べ替えられていないので、最新のノートが上部に表示されます。
この 2 つの問題は、次のチュートリアルの手順で修正されます。
アプリの動作を修正する
アプリ コードをコンパイルして実行できるようになったので、アプリの動作に 2 つの欠陥があることに気付いたでしょう。 このアプリでは、既に選択されているノートを再選択することはできません。また、ノートの作成後または変更後にノートの一覧が並べ替えられません。
メモをリストの先頭に表示する
まず、ノートのリストの並べ替えの問題を修正します。 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
メソッドは、コレクション内のオブジェクトの位置を移動する 2 つのパラメーターを受け取ります。 最初のパラメーターは移動するオブジェクトのインデックスであり、2 番目のパラメーターはオブジェクトを移動する場所のインデックスです。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)));
}
}
ノートの 2 回の選択を許可する
AllNotes ビューでは、CollectionView
はすべてのノートが一覧表示されますが、同じノートを 2 回選択することはできません。 項目を選択したままにする方法は 2 つあります。ユーザーが既存のメモを変更したときと、ユーザーが強制的に逆方向に移動したときです。 ユーザーがメモを保存するケースは、前のセクションで AllNotes.Move
を使用したコード変更で修正されるため、そのケースについて心配する必要はありません。
ここで解決する必要がある問題は、ナビゲーションに関連しています。 Allnotes ビューの移動方法を問わず、ページに対して NavigatedTo
イベントが発生します。 このイベントは、CollectionView
で選択されている項目を強制的に選択解除するのに最適な場所です。
ただし、MVVM パターンがここで適用されている場合、ビュー モデルは、メモの保存後に選択した項目をクリアするなど、ビューで直接何かをトリガーすることはできません。 では、トリガーするにはどうすればよいでしょうか。 MVVM パターンを適切に実装すると、ビューの分離コードが最小限に抑えられます。 MVVM 分離パターンをサポートするために、この問題を解決するには、いくつかの方法があります。 ただし、特にビューに直接関連付けられている場合は、ビューの分離コードにコードを配置することもできます。 MVVM には、アプリを区分化し、保守性を向上させ、新しい機能を簡単に追加できるようにする優れた設計と概念が数多くあります。 ただし、場合によっては、MVVM が過剰なエンジニアリングを促すこともあります。
この問題の解決策を過度に設計せず、NavigatedTo
イベントを使用して CollectionView
から選択した項目をクリアします。
Visual Studio の [ソリューション エクスプローラー] ウィンドウで、Views\AllNotesPage.xaml をダブルクリックします。
<ContentPage>
の XAML で、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 パターンが使用されるようになりました。
次のステップ
次のリンクは、このチュートリアルで学習した概念の一部に関する詳しい情報を記載しています。
このセクションに問題がある場合 このセクションを改善できるよう、フィードバックをお送りください。
.NET MAUI