🌊

.NET MAUI Community Toolkitを使ってMVVM実装(1)

に公開

はじめに

前回の記事に続いてMVVMで.NET MAUIアプリを実装する方法をメモ。
今回は「.NET MAUI Community Toolkit(CommunityToolkit.Maui)」を使ってみる。

環境

Windows 11
Visual Studio 2022
.NET 8.0
CommunityToolkit.Mvvm 8.4.0 -> 前回導入
CommunityToolkit.Maui 9.1.1 -> 今回導入

.NET MAUI Community Toolkit とは

Microsoft公式支援のオープンソースプロジェクトで、MAUI開発をより便利・強力にする目的で提供されている。
標準の .NET MAUI ではちょっとしたUI操作をするのにコードが必要、また一部の機能が足りないといったとき、XAMLやViewModelだけで完結できるケースが多くなる。
以下のような機能の詰め合わせ。

  • ビヘイビア(Behavior)
  • コンバーター(Converters)
  • アニメーション
  • エフェクト(Effects)
  • レイアウト
  • UIコントロールの拡張
  • コミュニティによる便利ユーティリティ集

https://github.com/CommunityToolkit/Maui
https://learn.microsoft.com/ja-jp/dotnet/communitytoolkit/maui/get-started?source=recommendations&tabs=CommunityToolkitMaui

.NET MAUI Community Toolkit をインストール

NuGetでCommunityToolkit.Mauiをインストールする。

.NET 8.0をサポートするバージョンを選択する。今回は9.1.1を選択。

初期設定

MauiProgram.csのスタートアップ処理にCommunityToolkit.Maui の初期化処理.UseMauiCommunityToolkit()を追加。

MauiProgram.cs
using CommunityToolkit.Maui;
using Microsoft.Extensions.Logging;

namespace MvvmMauiApp;

public static class MauiProgram
{
	public static MauiApp CreateMauiApp()
	{
		var builder = MauiApp.CreateBuilder();
		builder
			.UseMauiApp<App>()
			.UseMauiCommunityToolkit()
			.ConfigureFonts(fonts =>
			{
				fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
				fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
			});

#if DEBUG
		builder.Logging.AddDebug();
#endif

		return builder.Build();
	}
}

Behaviors

UIコントロールに機能を追加する。

EventToCommandBehavior

Commandプロパティを持たないUIコントロールに対して、任意のイベントとコマンドをマッピングして呼び出すことができる。

使用例:ユーザー一覧画面の初期表示イベントで一覧データを取得する。
ユーザーデータ

User.cs
namespace MvvmMauiApp.Models
{
    public class User
    {
        public int UserId { get; set; }
        public string Name { get; set; } = string.Empty;
        public string Email { get; set; } = string.Empty;
    }
}

ViewModel

UserListPageViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MvvmMauiApp.Models;
using System.Collections.ObjectModel;

namespace MvvmMauiApp.ViewModels
{
    internal partial class UserListPageViewModel : ObservableObject
    {
        [ObservableProperty]
        private ObservableCollection<User> users = new();

        [RelayCommand]
        private async void OnPageLoaded()
        {
            // データ取得をシミュレートするために待機
            await Task.Delay(2000);
            Users = new ObservableCollection<User>
            {
                new User { UserId = 1, Name = "Alice", Email = "mail_Alice@mail.com" },
                new User { UserId = 2, Name = "Bob", Email = "mail_Bob@mail.com" },
                new User { UserId = 3, Name = "Charlie", Email = "mail_Charlie@mail.com" }
            };
        }
    }
}

View
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"を追加する必要がある。
Behaviorsを使用して、初期表示イベント(Loaded)にデータ取得用のコマンドをバインド。

UserListPage.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"  
            x:Class="MvvmMauiApp.Views.UserListPage"  
            xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"   
            xmlns:vm="clr-namespace:MvvmMauiApp.ViewModels"  
            Title="UserListPage">  
    <ContentPage.BindingContext>  
        <vm:UserListPageViewModel />  
    </ContentPage.BindingContext>  

    <!--BehaviorsでLoadedイベントにコマンドをバインド-->  
    <ContentPage.Behaviors>  
        <toolkit:EventToCommandBehavior Command="{Binding PageLoadedCommand}" EventName="Loaded" />  
    </ContentPage.Behaviors>  

    <VerticalStackLayout>  
        <!--Userの一覧-->  
        <CollectionView ItemsSource="{Binding Users}">  
            <CollectionView.ItemTemplate>  
                <DataTemplate>  
                    <Frame Padding="10" Margin="5" BorderColor="Gray" CornerRadius="5">  
                        <HorizontalStackLayout>  
                            <Label Text="{Binding UserId, StringFormat='{}{0:D8}'}" Margin="0,0,20,0" />  
                            <Label Text="{Binding Name}" Margin="0,0,20,0" />  
                            <Label Text="{Binding Email}" />  
                        </HorizontalStackLayout>  
                    </Frame>  
                </DataTemplate>  
            </CollectionView.ItemTemplate>  
        </CollectionView>  
    </VerticalStackLayout>  
</ContentPage>

画面遷移後、ユーザー一覧データを取得・表示

Converters

バインディングした値を変換するようなコンバータが提供されている。
下記の例以外にもInvertedBoolConverter(真偽値を逆にする)やIsStringNullOrWhiteSpaceConverter(バインド値が空か判断して真偽値を返す)などのConverterが用意されている。

BoolToObjectConverter

バインドされた値の真偽値によって指定のオブジェクトに変換する。

ユーザークラスにbool型のプロパティIsAdminを追加。

User.cs
public bool IsAdmin { get; set; }

初期表示イベント時の処理で作成するデータ1件目のIsAdminをtrueにする。

UserListPageViewModel.cs
[RelayCommand]
private async void OnPageLoaded()
{
    // データ取得をシミュレートするために待機
    await Task.Delay(2000);
    Users = new ObservableCollection<User>
    {
        new User { UserId = 1, Name = "Alice", Email = "mail_Alice@mail.com", IsAdmin = true },
        new User { UserId = 2, Name = "Bob", Email = "mail_Bob@mail.com" },
        new User { UserId = 3, Name = "Charlie", Email = "mail_Charlie@mail.com" }
    };
}

UserListPage.xamlを変更。

  • ResourcesにConverterを設定
    • 入力値がtrueの場合、文字列[Admin]、falseの時は空文字に変換するように設定
    • 文字列以外にもSolidColorBlushに変換して背景色をバインド値によって決定するような使い方もできる
  • 一覧項目を追加
    • 真偽値のバインド時に設定済のConverterを指定する
    • 名前は設定時に指定したBoolToTextConverter
UserListPage.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"  
            x:Class="MvvmMauiApp.Views.UserListPage"  
            xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"   
            xmlns:vm="clr-namespace:MvvmMauiApp.ViewModels"  
            Title="UserListPage">  
    <ContentPage.BindingContext>  
        <vm:UserListPageViewModel />  
    </ContentPage.BindingContext>

    <!--ResourcesにConverterを設定-->
    <ContentPage.Resources>
        <x:String x:Key="IsAdminText">[Admin]</x:String>
        <x:String x:Key="NotAdminText"></x:String>
        <toolkit:BoolToObjectConverter x:Key="BoolToTextConverter"
                                       TrueObject="{StaticResource IsAdminText}"  
                                       FalseObject="{StaticResource NotAdminText}" />
    </ContentPage.Resources> 

    <!--BehaviorsでLoadedイベントにコマンドをバインド-->
    <ContentPage.Behaviors>  
        <toolkit:EventToCommandBehavior Command="{Binding PageLoadedCommand}" EventName="Loaded" />  
    </ContentPage.Behaviors>  

    <VerticalStackLayout>  
        <!--Userの一覧-->  
        <CollectionView ItemsSource="{Binding Users}">  
            <CollectionView.ItemTemplate>  
                <DataTemplate>  
                    <Frame Padding="10" Margin="5" BorderColor="Gray" CornerRadius="5">  
                        <HorizontalStackLayout>  
                            <Label Text="{Binding UserId, StringFormat='{}{0:D8}'}" Margin="0,0,20,0" />  
                            <Label Text="{Binding Name}" Margin="0,0,20,0" />  
                            <Label Text="{Binding Email}" Margin="0,0,20,0" />  
                            <!--Converterを使用して表示-->  
                            <Label Text="{Binding IsAdmin, Converter={StaticResource BoolToTextConverter}}" />  
                        </HorizontalStackLayout>  
                    </Frame>  
                </DataTemplate>  
            </CollectionView.ItemTemplate>  
        </CollectionView>  
    </VerticalStackLayout>  
</ContentPage>

1件目のみ[Admin]と表示される。

【参考】初期表示イベントとコマンドのバインドを自分で実装する方法

方法1

Behavior<T>を継承したクラスを作る。
既存のUI要素に振る舞いを追加することができる。Toolkitの実装に近い方法。

🔧 実装例
LoadedCommandBehavior.cs
using System.Windows.Input;

namespace MvvmMauiApp.Behaviors
{
    public class LoadedCommandBehavior : Behavior<Page>
    {
        public static readonly BindableProperty CommandProperty =
            BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(LoadedCommandBehavior), null);

        public ICommand? Command
        {
            get => (ICommand?)GetValue(CommandProperty);
            set => SetValue(CommandProperty, value);
        }

        protected override void OnAttachedTo(Page bindable)
        {
            base.OnAttachedTo(bindable);
            AssociatedObject = bindable;

            bindable.Loaded += OnLoaded;
            bindable.BindingContextChanged += OnBindingContextChanged;

            // 明示的に初期バインディングを実行
            BindingContext = bindable.BindingContext;
        }

        protected override void OnDetachingFrom(Page bindable)
        {
            base.OnDetachingFrom(bindable);
            bindable.Loaded -= OnLoaded;
            bindable.BindingContextChanged -= OnBindingContextChanged;
        }

        public Page? AssociatedObject { get; private set; }

        private void OnBindingContextChanged(object? sender, EventArgs e)
        {
            BindingContext = AssociatedObject?.BindingContext;
        }

        private void OnLoaded(object? sender, EventArgs e)
        {
            if (Command?.CanExecute(null) == true)
            {
                Command.Execute(null);
            }
        }
    }
}

作成したBahaviorをxamlで追加する。

UserListPage.xaml
    ...
        xmlns:behaviors="clr-namespace:MvvmMauiApp.Behaviors"  
    ...
    <!--BehaviorsでLoadedイベントにコマンドをバインド-->
    <ContentPage.Behaviors>
        <behaviors:LoadedCommandBehavior Command="{Binding PageLoadedCommand}" />
        <!--
        <toolkit:EventToCommandBehavior Command="{Binding PageLoadedCommand}" EventName="Loaded" />  
        -->
     </ContentPage.Behaviors>  
    ...

https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/behaviors?view=net-maui-9.0#net-maui-behaviors

方法2

添付プロパティを使用する。
プロパティ変更時(下記の例ではxamlのバインドが適用された時)のコールバックOnCommandChangedでイベントハンドラを登録。

🔧 実装例
LoadedCommandProperty.cs
using System.Windows.Input;

namespace MvvmMauiApp.Behaviors;
public static class LoadedCommandProperty
{
    public static readonly BindableProperty CommandProperty =
        BindableProperty.CreateAttached(
            "Command",
            typeof(ICommand),
            typeof(LoadedCommandProperty),
            null,
            propertyChanged: OnCommandChanged);

    public static ICommand GetCommand(BindableObject view)
        => (ICommand)view.GetValue(CommandProperty);

    public static void SetCommand(BindableObject view, ICommand value)
        => view.SetValue(CommandProperty, value);

    private static void OnCommandChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is VisualElement page)
        {
            page.Loaded -= OnLoaded;
            if (newValue is ICommand)
            {
                page.Loaded += OnLoaded;
            }
        }
    }

    private static void OnLoaded(object? sender, EventArgs e)
    {
        if (sender is BindableObject bindable)
        {
            var command = GetCommand(bindable);
            if (command?.CanExecute(null) == true)
            {
                command.Execute(null);
            }
        }
    }
}

ContentPageに添付プロパティを追加。

UserListPage.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"  
            x:Class="MvvmMauiApp.Views.UserListPage"  
            xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"   
            xmlns:vm="clr-namespace:MvvmMauiApp.ViewModels"  
            xmlns:behaviors="clr-namespace:MvvmMauiApp.Behaviors"  
            behaviors:LoadedCommandProperty.Command="{Binding PageLoadedCommand}"
            Title="UserListPage">  
            ...

まとめ

CommunityToolkit.MvvmCommunityToolkit.MauiのBehaviorやConverterを使うことで基本的なMVVMパターンの実装は効率良く進められると思います。
次回の記事ではCommunityToolkit.Mauiの他の機能も使ってみます。

Discussion