💻

WinUI3もMVVMで実装する(MVVM Toolkit)

に公開

はじめに

以前の記事で.NET MAUIをMVVM実装するためにCommunityToolkit.Mvvmを使用した。
CommunityToolkit.MvvmはWinUI3でも使用可能なので、同様にMVVMでの実装をしてみる。

環境

Windows 11
Visual Studio 2022
.NET 8.0
Microsoft.WindowsAppSDK 1.7.250606001
CommunityToolkit.Mvvm 8.4.0

プロジェクト作成

「空のアプリ、パッケージ化(デスクトップのWinUi3)」を選択。

ビルド、実行できることを確認。ボタンクリックするとテキストが変わる。

パッケージ化しない場合

MSIXではなく、.exe形式にする場合。
プロジェクトファイルに以下の設定を追加する。

<Project ...>
  ...
  <PropertyGroup>
    ...
    <WindowsPackageType>None</WindowsPackageType>
    ...
  </PropertyGroup> 
  ...
</Project>

上記を設定せずに「Unpackaged」を選んでビルドするとエラーになる。

https://learn.microsoft.com/ja-jp/windows/apps/winui/winui3/create-your-first-winui3-app

ComunityToolkit.Mvvmをインストール

MVVMを自前で実装すると冗長になるところを自動生成してくれるライブラリ。
NuGetでインストールする。

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

方針

現在は以下の通り、コードビハインドに処理が記述されている。
これをMVVMパターンにするため、ViewModelのプロパティとコマンドをバインドする実装に変更していく。

MainWindow.xaml

MainWindow.xaml
<Button x:Name="myButton" Click="myButton_Click">Click Me</Button>

MainWindow.xaml.cs

MainWindow.xaml.cs
namespace WinUiApp
{
    /// <summary>
    /// An empty window that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
        }

        private void myButton_Click(object sender, RoutedEventArgs e)
        {
            myButton.Content = "Clicked";
        }
    }
}

MainWindowViewModel.csを作成

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

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

  • partial classにする

  • ObservableObjectを継承

  • ObservableProperty
    →ソースジェネレータによってコードが生成され双方向バインディングが可能になる。

  • RelayCommand
    付与するとコマンドが自動生成される。
    コマンド名はプレフィックスonが存在する場合は削除され、末尾にCommandが追加される。

MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace WinUiApp
{
    public partial class MainWindowViewModel : ObservableObject
    {
        [ObservableProperty]
        private string buttonText = "Click Me";

        [RelayCommand]
        private void OnButtonClicked()
        {
            ButtonText = "Clicked!";
        }
    }
}

MainPage.xaml.csを修正

  • コードビハインドの不要なコードを削除する。
  • ViewModelをプロパティとして設定。
MainWindow.xaml.cs
using Microsoft.UI.Xaml;

namespace WinUiApp
{
    public sealed partial class MainWindow : Window
    {
        public MainWindowViewModel ViewModel { get; } = new();

        public MainWindow()
        {
            this.InitializeComponent();
        }
    }
}

MainWindow.xamlを修正

  • ボタンのTextコマンドのバインドを設定。
  • クリックイベントを削除
MainWindow.xaml
<?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WinUiApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUiApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="WinUiApp">

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button Content="{x:Bind ViewModel.ButtonText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                Command="{x:Bind ViewModel.ButtonClickedCommand}" />
    </StackPanel>
</Window>

バインド方法

今回使用した静的バインドx:Bindではなく、動的バインドBindingも使用可能。

🔧 {x:Bind}ではなく{Binding}を使う場合
MainWindow.xaml.cs
using Microsoft.UI.Xaml;

namespace WinUiApp
{
    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
        }
    }
}

WindowにXAML上でバインドすることはできないため、以下の例ではPageDataContextを使用している。

MainWindow.xaml
<?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WinUiApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUiApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="WinUiApp">

    <Page>
        <Page.DataContext>
            <local:MainWindowViewModel />
        </Page.DataContext>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
            <Button Content="{Binding ButtonText}" Command="{Binding ButtonClickedCommand}" />
        </StackPanel>
    </Page>
</Window>

動的バインドは動的にDataContextを変える等の柔軟性に優れ、静的バインドはパフォーマンスや安全性に優れるとされる。

比較

特徴/項目 {Binding}(動的) {x:Bind}(静的)
解決タイミング ランタイム コンパイルタイム
バインドソース DataContext ベース コードビハインドのプロパティが主
型安全性 低い(文字列指定) 高い(コンパイルチェックあり)
パフォーマンス 遅い 速い
動的切り替え対応 あり 基本なし
デフォルトモード OneWay OneTime
双方向バインド対応 あり あり(明示指定が必要)

動作確認

ビルド、実行する。
初期コードと同じようにクリックでメッセージが変わることを確認。

コマンドにパラメータを渡す

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

MainWindowViewModel.cs
[RelayCommand]
private void OnButtonClicked(string name)
{
    ButtonText = $"Clicked by {name}";
}
  • MainWindow.xamlにテキストボックスを追加。
  • 入力された文字列をクリックコマンドのパラメータとして渡す。
MainWindow.xaml
<?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WinUiApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUiApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="WinUiApp">

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <TextBox x:Name="NameInput" Width="200" PlaceholderText="名前を入力" />
        <Button Content="{x:Bind ViewModel.ButtonText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                Command="{x:Bind ViewModel.ButtonClickedCommand}"
                CommandParameter="{x:Bind NameInput.Text, Mode=OneWay}" />
    </StackPanel>
</Window>

実行し動作確認。

まとめ

WinUIとMAUIでは実装方法が少し異なる部分はあるものの、CommunityToolkit.Mvvmを使用することで効率良くMVVMパターンで実装することができた。

Discussion