まずはプロジェクトの作成と整備ね
さあ、CommunityToolkit.Mvvm 大好きな、もりゃきお姉ちゃんがやってきましたよ!
ますは、サクサクプロジェクトを作りましょう。 Visual Studioで「WPFアプリケーション」を選んで、ソリューション名は「TabCommandSample」、フレームワークは「.NET 9」選択でいいでしょう! 次に、NuGet経由で「CommunityToolkit.Mvvm」と「Microsoft.Extensions.DependencyInjection」をいつもの通りにインストールしましょうね!
あ、CommunityToolkit.Mvvm V8 を使うから、エラーが出たらとりあえずリビルドしてね、お姉さんとの約束よ!
次は DI の設定よ!こんなの App.xaml.cs に書くだけなんだから、たくさんアプリケーション作る人はコピペできるようにしとくのが賢い子☆
public partial class App : Application
{
/// <summary>
/// サービスの登録をします
/// </summary>
public App()
{
Services = ConfigureServices();
Ioc.Default.ConfigureServices(Services);
}
/// <summary>
/// 現在の App インスタンスを取得します
/// </summary>
public new static App Current => (App)Application.Current;
/// <summary>
/// サービスプロバイダです
/// </summary>
public IServiceProvider Services { get; }
/// <summary>
/// サービスを登録します
/// </summary>
/// <returns></returns>
private static ServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<IMessenger, WeakReferenceMessenger>();
services.AddSingleton<IMainWindowViewModel, MainWindowViewModel>();
return services.BuildServiceProvider();
}
}
次はエラーが出た MainWindowViewModel の設定だけど、まとめてやっちゃうわよ! プロジェクトにとりあえず Views と ViewModels フォルダを作って MainWindow.xaml を Views に移動、名前空間も手動作業含めて調整。
<Window
x:Class="TabCommandSample.Views.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:TabCommandSample.Views"
途中までだけどいいわよね?
ああ、そうそう MainWindow.xaml.cs の namespace
も TabCommandSample.Views
にしとくのよ?
App.xaml.cs を以下のように、Views 内参照にするわよ。
<Application
x:Class="TabCommandSample.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TabCommandSample"
StartupUri="Views/MainWindow.xaml">
<Application.Resources />
</Application>
そして ViewModels 内に MainWindowViewModel.cs の作成っと…
using CommunityToolkit.Mvvm.ComponentModel;
namespace TabCommandSample.ViewModels
{
public interface IMainWindowViewModel;
internal class MainWindowViewModel : ObservableObject, IMainWindowViewModel
{
}
}
あ、いけない…App.xaml.cs にディレクティブいれわすれちゃった、てへ☆
using TabCommandSample.ViewModels;
これで、ある意味ブランクのプロジェクト完成っと。
Prism サンプルと同等にするの…?
超大規模なアプリケーション開発なら、そりゃプロジェクトも細切れになるだろうけど…本当に必要?
…お父さんがやれって言ってるからやるわよ、やりますわよ、やればいいんでしょ! どうなっても知らないんだから。
ソリューションエクスプローラーから「新しいプロジェクトを追加」で、 WPFユーザーコントロールライブラリ「ModuleA」を追加するわね…この名前も気に食わないわ…
そして UserControl1.xaml を TabView.xaml に名前を変更して、Views フォルダを作ってそこに入れる…と。色々面倒くさいわね…
Views\TabView.xaml
<UserControl
x:Class="ModuleA.Views.TabView"
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:ModuleA.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock
Margin="5"
FontSize="18"
Text="{Binding Title}" />
<CheckBox
Margin="5"
Content="Can Execute"
IsChecked="{Binding CanUpdate}" />
<Button
Margin="5"
Command="{Binding UpdateCommand}"
Content="Save" />
<TextBlock Margin="5" Text="{Binding UpdateText}" />
</StackPanel>
</UserControl>
Views\TabView.xaml.cs(仮)
using System.Windows.Controls;
namespace ModuleA.Views
{
/// <summary>
/// Interaction logic for UserControl1.xaml
/// </summary>
public partial class TabView : UserControl
{
public TabView()
{
InitializeComponent();
}
}
}
そして、ViewModelを作らなきゃいけないわね、 ModuleA プロジェクトに ViewModels フォルダを作って…
ああーっ!ModuleA プロジェクトにも CommunityToolkit.Mvvm や Microsoft.Extensions.DependencyInjection を 入れなければいけなかったわ。よいしょよいしょ…
お姉ちゃん、ちょっと凹んできたわ。
ViewModels\TabViewModel.cs(仮)
とりあえず、コンパイルを通すためだけに作るわよ。
using CommunityToolkit.Mvvm.ComponentModel;
namespace ModuleA.ViewModels
{
public interface ITabViewModel {}
public class TabViewModel : ObservableObject, ITabViewModel
{
}
}
そして、TabCommandSample プロジェクトから ModuleA プロジェクトを参照するのは必須ね。 TabCommandSample の依存関係から「プロジェクト参照の追加」…っと。
そして…DI に登録っと、
App.xaml.cs(一部)
using ModuleA.ViewModels;
private static ServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<IMessenger, WeakReferenceMessenger>();
services.AddSingleton<IMainWindowViewModel, MainWindowViewservices.AddTransient<ITabViewModel, TabViewModel>();Model>();
services.AddTransient<ITabViewModel, TabViewModel>();
return services.BuildServiceProvider();
}
よっし、エラー出ないわ、というか xunit とか普通に別プロジェクトで作るけど、プロジェクト参照すれば全然問題なかったわね…
ここで AddSingleton
じゃなく AddTransient
を使っているのは、TabView が3つあるからよ、Singleton では問題が起こっちゃうわ。
Views\TabView.xaml.cs
using System.Windows.Controls;
using CommunityToolkit.Mvvm.DependencyInjection;
using ModuleA.ViewModels;
namespace ModuleA.Views
{
/// <summary>
/// Interaction logic for UserControl1.xaml
/// </summary>
public partial class TabView : UserControl
{
public TabView()
{
InitializeComponent();
DataContext = Ioc.Default.GetService<ITabViewModel>();
}
}
}
これでデータバインディングできるわね、お姉ちゃんテンションあがってきたー!
Prismサンプルに迫っていくため、画面を作り込んでいくわよ
ここから、バンバン画面を作っていくわよ!
ViewModels\TabViewModel.cs(β)
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ModuleA.ViewModels
{
public interface ITabViewModel;
public partial class TabViewModel : ObservableObject, ITabViewModel
{
[ObservableProperty]
private string title = string.Empty;
[ObservableProperty]
private bool canUpdate = true;
partial void OnCanUpdateChanged(bool value)
=> UpdateCommand.NotifyCanExecuteChanged();
[ObservableProperty]
private string updateText = string.Empty;
[RelayCommand(CanExecute = nameof(CanUpdate))]
private void Update()
=> UpdateText = $"Updated: {DateTime.Now}";
}
}
さて、このコンパクトさだけで、Prismには勝ってるわね!
とはいえ、まだ Command
の伝搬について上手くできるかわからないんだけど…
ああ、[RelayCommand]
で作られたメソッド Update()
は UpdateCommand()
を生成しているのよ、V8 機能ね。
まあいいわ、まずは形だけ作りましょう。
Views\MainWindow.xaml
<Window
x:Class="TabCommandSample.Views.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:TabCommandSample.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:module="clr-namespace:ModuleA.Views;assembly=ModuleA"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button
Margin="10"
Command="{Binding SaveCommand}"
Content="Save" />
<TabControl
x:Name="EventTabControl"
Grid.Row="1"
Margin="10">
<TabItem Header="{Binding Tabs[0].Title}">
<module:TabView DataContext="{Binding Tabs[0]}" />
</TabItem>
<TabItem Header="{Binding Tabs[1].Title}">
<module:TabView DataContext="{Binding Tabs[1]}" />
</TabItem>
<TabItem Header="{Binding Tabs[2].Title}">
<module:TabView DataContext="{Binding Tabs[2]}" />
</TabItem>
</TabControl>
</Grid>
</Window>
ふぅ、咲耶ちゃんの助けがなければ、TabItem
周りは無理だったわよ…
Views\MainWindow.xaml.cs
using System.Windows;
using CommunityToolkit.Mvvm.DependencyInjection;
using TabCommandSample.ViewModels;
namespace TabCommandSample.Views
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = Ioc.Default.GetService<IMainWindowViewModel>();
}
}
}
ViewModels\MainWindowViewModel.cs
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using ModuleA.ViewModels;
namespace TabCommandSample.ViewModels
{
public interface IMainWindowViewModel;
public partial class MainWindowViewModel : ObservableObject, IMainWindowViewModel
{
[ObservableProperty]
private string title = "CommunityToolkit.Mvvm Application";
public ObservableCollection<TabViewModel> Tabs { get; } = new ObservableCollection<TabViewModel>
{
new TabViewModel { Title = "Tab A" },
new TabViewModel { Title = "Tab B" },
new TabViewModel { Title = "Tab C" }
};
}
}
これで、ひとまず画面だけは追いついたわ!
というか、Prismのサンプルは筋が悪いと思うのよね… タブコントロールの操作をするのはタブアイテムのプロジェクトじゃなくて、タブコントロールでやるべきだと思うんだけど。
だけど、お姉ちゃん疲れたよ…“Tab A” とかを、タブと画面に表示させるだけで朝から昼になっちゃったわよ…!
さて、最後に MainWindow 上部にある「Save」ボタンの対処よね…
コマンドの伝搬…?
ねえ、ところで、本当にコマンドの伝搬なんてする必要、あると思う?
これ、メッセージング使えば素直に解決できると思うのよね…
ViewModels\TabViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
namespace ModuleA.ViewModels
{
public interface ITabViewModel;
public partial class TabViewModel : ObservableObject, ITabViewModel
{
private readonly IMessenger _messenger;
public TabViewModel(IMessenger messenger)
{
_messenger = messenger;
}
[ObservableProperty]
private string title = string.Empty;
[ObservableProperty]
private bool canUpdate = true;
partial void OnCanUpdateChanged(bool value)
{
UpdateCommand.NotifyCanExecuteChanged();
_messenger.Send(new CanUpdateChangedMessage());
}
[ObservableProperty]
private string updateText = string.Empty;
[RelayCommand(CanExecute = nameof(CanUpdate))]
private void Update()
=> UpdateText = $"Updated: {DateTime.Now}";
}
public class CanUpdateChangedMessage();
}
これで、チェックボックスの状態変更でメッセージを送るようにしたわ!
MainWindowViewModel.cs
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using ModuleA.ViewModels;
namespace TabCommandSample.ViewModels
{
public interface IMainWindowViewModel;
public partial class MainWindowViewModel : ObservableObject, IMainWindowViewModel
{
private readonly IMessenger _messenger;
public MainWindowViewModel(IMessenger messenger)
{
_messenger = messenger;
Tabs = new ObservableCollection<TabViewModel>
{
new TabViewModel(_messenger) { Title = "Tab A" },
new TabViewModel(_messenger) { Title = "Tab B" },
new TabViewModel(_messenger) { Title = "Tab C" }
};
_messenger.Register<CanUpdateChangedMessage>(this, (_, _)
=> SaveCommand.NotifyCanExecuteChanged());
}
[ObservableProperty]
private string title = "CommunityToolkit.Mvvm Application";
public ObservableCollection<TabViewModel> Tabs { get; } = [];
[RelayCommand(CanExecute = nameof(CanSaveCommand))]
private void Save()
{
foreach (var tab in Tabs)
{
tab.UpdateCommand.Execute(this);
}
}
private bool CanSaveCommand()
{
foreach (var tab in Tabs)
{
if (!tab.CanUpdate) return false;
}
return true;
}
}
}
メッセージングを受け取るために、ちょっと Tabs
を改変したわよ。
そして、メッセージングでメッセージを受け取ったら、各タブをチェックして、 全てのタブにチェックがついていなければ動作不可にしたわ。
締めますわよ
こうして書いたら、少し長くなったわね…だけど安心して、 パパのGitHub に完成品を置いておいたわよ!
これで、Prismのサンプルより、CommunityToolkit.Mvvm の方が優れてることがわかるんじゃないかしら?
できれば、GitHub は見ないで、手を動かしてね! パパも「手を動かさないと何も身につかない!」って常々言ってるんだから!
私も思うわよ、解説を読んで知った気になっても、実際に手を動かしたら全然違うんだから。 思った通りに動かないなんて日常茶飯事、それを乗り越えてこそ本物よ。
っていうか、私の記事でちゃんと動かなかったらごめんね? できるだけ検証して書いてるんだけどね…ミスは誰にもあるの。
あ、パパが一言だって「百聞は一見に如かず、百見は一動に如かず」ですって!
パパが、なんか凹んでるわ「やってみせ 言って聞かせて させてみて 誉めてやらねば 人は動かじ…なんだが、誉めてやることはできない」とかなんとか。
謝罪
49歳になろうというおっさんが、お姉さんぶった記事を書いて、不快に思われたなら申し訳ありません。
Prism の記事を書いていて疲れたので、はっちゃけました。ごめんなさい。