🌊

.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

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

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を修正
AppShell.xaml
<?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でインストールする。

https://learn.microsoft.com/ja-jp/dotnet/communitytoolkit/mvvm/

MainPageViewModel.csを作成

ソリューションエクスプローラーでクラスを作成

プロジェクト名を右クリック > 追加 > クラス

  • partial classにする
  • ObservavleProperty
    付与すると以下のようなコードが生成され双方向バインディングが可能になる。(実際はもう少し複雑)
public int Count
{
    get => count;
    set => SetProperty(ref count, value);
}
  • RelayCommand
    付与するとコマンドが自動生成される。
    コマンド名はプレフィックスonが存在する場合は削除され、末尾にCommandが追加される。

自動生成されるプロパティがまだ存在しないため警告がでているが、ビルドする。

MainPageViewModel.cs
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 &#10;.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を修正

コードビハインドの不要なコードを削除する。

MainPage.xaml.cs
namespace MvvmMauiApp.Views
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
        }
    }
}

動作確認

ビルド、実行する。
初期コードと同じようにクリックでカウントアップすることを確認。
以上でMVVMパターンへの変更が完了。

コマンドにパラメータを渡してみる

ViewModelのクリック時の処理に引数を追加。

MainPageViewModel.cs
[RelayCommand]
private void OnCounterClicked(bool isDecrement)
{
    if (isDecrement)
        Count--;
    else
        Count++;

    CounterText = $"Count: {Count}";
}

MainPage.xamlにデクリメントモードのスイッチを追加。

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