.NET MAUIをMVVMで実装する(MVVM Toolkit)
はじめに
MVVMで.NET MAUIアプリを実装する方法をメモ。
本記事では、Visual Studioでプロジェクト作成時に生成される初期コードをMVVMにするところまでを説明。
環境
Windows 11
Visual Studio 2022
.NET 8.0
CommunityToolkit.Mvvm 8.4.0
プロジェクト作成
デフォルトのプロジェクト名は「MauiApp1」となっている。これを「MauiApp」とすると名前が衝突するので要注意。
今回のプロジェクト名は「MvvmMauiApp」で.NET MAUI アプリのテンプレートを使用する。
ビルド、実行できることを確認。ボタンクリックするとカウントアップする。
方針
現在は以下の通り、コードビハインドに処理が記述されている。
これをMVVMパターンにするため、ViewModelのプロパティとコマンドをバインドする実装に変更していく。
MainPage.xaml
<Button
x:Name="CounterBtn"
Text="Click me"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnCounterClicked"
HorizontalOptions="Fill" />
MainPage.xaml.cs
public partial class MainPage : ContentPage
{
int count = 0;
public MainPage()
{
InitializeComponent();
}
private void OnCounterClicked(object sender, EventArgs e)
{
count++;
if (count == 1)
CounterBtn.Text = $"Clicked {count} time";
else
CounterBtn.Text = $"Clicked {count} times";
SemanticScreenReader.Announce(CounterBtn.Text);
}
}
ディレクトリ作成
以下、3つのディレクトリを作成する。
- Views
- ViewModels
- Models
ソリューションエクスプローラーのプロジェクト名を右クリック > 追加 > 新しいフォルダー
MainPage.xamlをViewsフォルダに移動
ソリューションエクスプローラー上でドラッグ&ドロップで移動。
移動後、名前空間を調整する。
MainPage.xaml
x:Class="MvvmMauiApp.MainPage">
↓
x:Class="MvvmMauiApp.Views.MainPage">
MainPage.xaml.cs
namespace MvvmMauiApp
↓
namespace MvvmMauiApp.Views
AppShell.xamlを修正
MainPageの場所が変わったので調整する。
-
xmlns:views="clr-namespace:MvvmMauiApp.Views"
を追加 - ShellContentのContentTemplateを修正
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="MvvmMauiApp.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MvvmMauiApp"
xmlns:views="clr-namespace:MvvmMauiApp.Views"
Shell.FlyoutBehavior="Disabled"
Title="MvvmMauiApp">
<ShellContent
Title="Home"
ContentTemplate="{DataTemplate views:MainPage}"
Route="MainPage" />
</Shell>
ComunityToolkit.Mvvmをインストール
MVVMを自前で実装すると冗長になるところを自動生成してくれるライブラリ。
NuGetでインストールする。
MainPageViewModel.csを作成
ソリューションエクスプローラーでクラスを作成
プロジェクト名を右クリック > 追加 > クラス
- partial classにする
- ObservavleProperty
付与すると以下のようなコードが生成され双方向バインディングが可能になる。(実際はもう少し複雑)
public int Count
{
get => count;
set => SetProperty(ref count, value);
}
- RelayCommand
付与するとコマンドが自動生成される。
コマンド名はプレフィックスon
が存在する場合は削除され、末尾にCommand
が追加される。
自動生成されるプロパティがまだ存在しないため警告がでているが、ビルドする。
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MvvmMauiApp.ViewModels
{
internal partial class MainPageViewModel : ObservableObject
{
[ObservableProperty]
private int count = 0;
[ObservableProperty]
private string counterText = "Click me";
[RelayCommand]
private void OnCounterClicked()
{
Count++;
if (Count == 1)
CounterText = $"Clicked {Count} time";
else
CounterText = $"Clicked {Count} times";
}
}
}
🔧 自前で実装する場合の例(参考)
namespace MvvmMauiApp.ViewModels
{
internal class MainPageViewModel : INotifyPropertyChanged
{
private int _count = 0;
private string _counterText = "Click me";
public event PropertyChangedEventHandler PropertyChanged;
public MainPageViewModel()
{
CounterCommand = new RelayCommand(OnCounterClicked);
}
public int Count
{
get => _count;
set
{
if (_count != value)
{
_count = value;
OnPropertyChanged();
}
}
}
public string CounterText
{
get => _counterText;
set
{
if (_counterText != value)
{
_counterText = value;
OnPropertyChanged();
}
}
}
public ICommand CounterCommand { get; }
private void OnCounterClicked()
{
Count++;
CounterText = Count == 1
? $"Clicked {Count} time"
: $"Clicked {Count} times";
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private class RelayCommand : ICommand
{
private readonly Action _execute;
public RelayCommand(Action execute)
{
_execute = execute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter) => true;
public void Execute(object parameter) => _execute();
}
}
}
MainPage.xamlを修正
- ViewModelをバインド(ContentPage.BindingContext)
- ボタンの
Text
とコマンド
をバインド。クリックイベントを削除
<?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:vm="clr-namespace:MvvmMauiApp.ViewModels"
x:Class="MvvmMauiApp.Views.MainPage">
<ContentPage.BindingContext>
<vm:MainPageViewModel />
</ContentPage.BindingContext>
<ScrollView>
<VerticalStackLayout
Padding="30,0"
Spacing="25">
<Image
Source="dotnet_bot.png"
HeightRequest="185"
Aspect="AspectFit"
SemanticProperties.Description="dot net bot in a race car number eight" />
<Label
Text="Hello, World!"
Style="{StaticResource Headline}"
SemanticProperties.HeadingLevel="Level1" />
<Label
Text="Welcome to .NET Multi-platform App UI"
Style="{StaticResource SubHeadline}"
SemanticProperties.HeadingLevel="Level2"
SemanticProperties.Description="Welcome to dot net Multi platform App U I" />
<Button
x:Name="CounterBtn"
Text="{Binding CounterText}"
SemanticProperties.Hint="Counts the number of times you click"
Command="{Binding CounterClickedCommand}"
HorizontalOptions="Fill" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
MainPage.xaml.csを修正
コードビハインドの不要なコードを削除する。
namespace MvvmMauiApp.Views
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
}
}
動作確認
ビルド、実行する。
初期コードと同じようにクリックでカウントアップすることを確認。
以上でMVVMパターンへの変更が完了。
コマンドにパラメータを渡してみる
ViewModelのクリック時の処理に引数を追加。
[RelayCommand]
private void OnCounterClicked(bool isDecrement)
{
if (isDecrement)
Count--;
else
Count++;
CounterText = $"Count: {Count}";
}
MainPage.xamlにデクリメントモードのスイッチを追加。
<HorizontalStackLayout HorizontalOptions="Center">
<Label Text="Decrement Mode: " VerticalOptions="Center" />
<Switch x:Name="OperationSwitch" Style="{StaticResource SwitchNoLabelStyle}" IsToggled="False" />
</HorizontalStackLayout>
<Button
x:Name="CounterBtn"
Text="{Binding CounterText}"
SemanticProperties.Hint="Counts the number of times you click"
Command="{Binding CounterClickedCommand}"
CommandParameter="{Binding Source={x:Reference OperationSwitch}, Path=IsToggled}"
HorizontalOptions="Fill" />
実行し動作確認。
スイッチをオンにしてからボタンをクリックするとデクリメントしている。
以上でパラメータを渡すことができるようになった。
Discussion