前提条件
まずは以下の記事内容を踏まえてください。
- カップ麺シリーズ:一から学べるWPF における CommunityToolkit.Mvvm(MVVM ToolKit) 覚え書き(1) 簡単な依存性注入とデータバインディング
- カップ麺シリーズ:一から学べる、WPF における CommunityToolkit.Mvvm(MVVM ToolKit) 覚え書き(2) 依存性注入を利用した簡単なサンプル
- カップ麺シリーズ:一から学べるWPF における CommunityToolkit.Mvvm(MVVM ToolKit) 覚え書き(3) メッセージングの簡単なサンプル
CommunityToolkit.Mvvm V8について
今回、プロジェクト作成は行いません。覚え書き(1)に沿って、最低限コンパイルできる所まで進めてください。
CommunityToolkit.Mvvm V8 は、ざっくり言うと、繰り返しとなる記述を省略できる仕組みが取り入れられています。
[ObservableProperty]
例えば以下のコード
private string _username = string.Empty;
public string Username
{
get => _username;
set => SetProperty(ref _username, value);
}
であれば
[ObservableProperty]
private string _Username = string.Empty;
で済みます。
実際はこの時クラスが partial
になり、裏側で同等のコードが生成あされています。コンパイルエラーが出ることもあるので、partial
クラスになることは最低限覚えておきましょう。
そして、このようなコード
private string _username = string.Empty;
public string Username
{
get => _username;
set
{
SetProperty(ref _username, value);
NameCommand.NotifyCanExecuteChanged();
}
}
では、結局従来型の利用方法になるじゃないかと思ったでしょうが、ご安心を。このようにします。
[ObservableProperty]
private string _Username = string.Empty;
partial void OnUsernameChanged(string value) => NameCommand.NotifyCanExecuteChanged();
当然、OnUsernameChanged
は {}
で括れます。ここでは一行で済むため =>
を利用しただけですからね。
ちなみに OnUsernameChanged
でも引数が (oldValue, newValue)
も用意されていますし、
事前に処理したい場合は OnUsernameChanging
も、引数両パターンが用意されています。
これらの書き方の優れた所は、例えば OnUsernameChanged(sgtring value)
では、実際に値が変更されなければメソッドに飛ばない所です。
[RelayCommand]
Command
もよりシンプルに書けるようになっています。
例えば以下のコード
public partial class MainWindowViewModel : ObservableObject, IMainWindowViewModel
{
[ObservableProperty]
private string _Username = string.Empty;
partial void OnNameChanged(string value) => NameCommand.NotifyCanExecuteChanged();
public RelayCommand NameCommand { get; set; }
private readonly IMessenger _messenger;
public MainWindowViewModel(IMessenger messenger)
{
_messenger = messenger;
NameCommand = new RelayCommand(
() => _messenger.Send(new OpenViewNameWindowMessage()),
() => !string.IsNullOrEmpty(_Username)
);
}
}
は以下のようなコードになります。
public partial class MainWindowViewModel : ObservableObject, IMainWindowViewModel
{
[ObservableProperty]
private string _username = string.Empty;
partial void OnUsernameChanged(string value) => NameCommand.NotifyCanExecuteChanged();
private readonly IMessenger _messenger;
public MainWindowViewModel()
{ throw new NotImplementedException(nameof(MainWindowViewModel)); }
public MainWindowViewModel(IMessenger messenger)
{
_messenger = messenger;
}
[RelayCommand(CanExecute = nameof(CanExecuteNameCommand))]
private void Name()
{
_messenger.Send(new OpenViewNameWindowMessage());
}
private bool CanExecuteNameCommand()
{
return !string.IsNullOrEmpty(Username);
}
}
注意:覚え書きのユーザー名 Name
を使うと、[RelayCommand]
で利用している Name
が衝突します!
この private void Name()
が NameCommand
を生成します。また private bool CanExecuteNameCommand()
が、そのコマンドの実行可否の判定をしています。
ここで唐突なお詫び
申し訳ありません、非同期コマンドについては自信がないため記述できません。
まあ多分ですが
[RelayCommand(CanExecute = nameof(CanExecuteNameCommand))]
private async Task NameAsync()
{
// awaitを含む処理
}
という形になるでしょう、実際ただのメソッドなので非同期処理に慣れている方には説明不要かと思います。
もりゃきのように、非同期処理の async/await
でなんか釈然としない人に、俺からの贈り物「await
は魔法の言葉じゃなくて、Task
を待ってるんだよ」知ってる?それはお粗末様でした。
総決算
では、色々弄ったので V8 化に伴って変更したファイルを全て提示します。
MainWindow.xaml
<Window
x:Class="NameTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:NameTest"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Grid VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBox
Grid.Row="0"
Width="300"
Height="30"
Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}" />
<Button
Grid.Row="1"
Width="60"
Height="30"
Command="{Binding NameCommand}"
Content="名前表示" />
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using NameTest.ViewModels;
namespace NameTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = Ioc.Default.GetService<IMainWindowViewModel>();
var messenger = Ioc.Default.GetService<IMessenger>() ?? throw new NullReferenceException(nameof(IMessenger));
messenger.Register<OpenViewNameWindowMessage>(this, (_, _) =>
{
var viewNameWindow = new Views.ViewName();
viewNameWindow.Show();
});
}
}
public class OpenViewNameWindowMessage;
}
MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
namespace NameTest.ViewModels
{
public interface IMainWindowViewModel
{
public string Username { get; set; }
}
internal partial class MainWindowViewModel : ObservableObject, IMainWindowViewModel
{
private readonly IMessenger _messenger;
[ObservableProperty]
private string _username = string.Empty;
partial void OnUsernameChanged(string value) => NameCommand.NotifyCanExecuteChanged();
public MainWindowViewModel()
{ throw new NotImplementedException(nameof(MainWindowViewModel)); }
public MainWindowViewModel(IMessenger messenger)
{
_messenger = messenger;
}
[RelayCommand(CanExecute = nameof(CanExecuteNameCommand))]
private void Name()
{
_messenger.Send(new OpenViewNameWindowMessage());
}
private bool CanExecuteNameCommand()
{
return !string.IsNullOrEmpty(Username);
}
}
}
ViewName.xaml
<Window
x:Class="NameTest.Views.ViewName"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:NameTest.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="ViewName"
Width="800"
Height="450"
mc:Ignorable="d">
<Grid VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label
Grid.Row="0"
Width="300"
Height="30"
Content="あなたの名前" />
<TextBox
Grid.Row="1"
Width="300"
Height="30"
Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</Window>
ViewName.xaml.cs
using System.Windows;
using CommunityToolkit.Mvvm.DependencyInjection;
using NameTest.ViewModels;
namespace NameTest.Views
{
/// <summary>
/// ViewName.xaml の相互作用ロジック
/// </summary>
public partial class ViewName : Window
{
public ViewName()
{
InitializeComponent();
DataContext = Ioc.Default.GetService<IViewNameViewModel>();
}
}
}
ViewNameViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
namespace NameTest.ViewModels
{
interface IViewNameViewModel;
internal partial class ViewNameViewModel : ObservableObject, IViewNameViewModel
{
[ObservableProperty]
private string _username = string.Empty;
partial void OnUsernameChanged(string value)
=> _mainWindowViewModel.Username = value;
// コンストラクタインジェクションで注入された ViewModel を保持する
private readonly IMainWindowViewModel _mainWindowViewModel;
// コンストラクタインジェクションを行っている
public ViewNameViewModel(IMainWindowViewModel mainWindowViewModel)
{
_mainWindowViewModel = mainWindowViewModel;
Username = _mainWindowViewModel.Username;
}
}
}
終わりに
もちろん、CommunityToolkit.Mvvm V8 を必ず使わなければならない、ということは無いでしょう。
ただ、理屈を理解して使うなら、可読性が向上するので、個人的には徐々に移行しています。もちろん欠点もあります、頻繁なリビルドが必要…とかね。
トレンドに乗ることが必ず正しい訳ではない、という事を踏まえて、以上のコードにメリットを感じたなら部分的にでも取り入れてみてはいかがでしょうか?