WPFのMVVMでイベントを処理する方法いろいろ
最近PrismやらReactive Propertyやら勉強中なので忘れないように書いていきます
- コードビハインド
- Prism
- Reactive Property
を使ってイベントを処理するサンプルを作成しました
サンプルコードはこちら github.com
開発環境はVisual Studio Community 2019で対象のフレームワークは.NET Core 3.0です
サンプルの中身
サンプルアプリはこんな感じの画面になっていて
- Textboxに入力したものと同じ内容をTextBlockにコピーする
- ボタンを押したらTextBlockの内容をすべて消去する
という動作になっています
サンプルコードではButtonのClickイベントとTextBoxのPreviewTextInputイベントを処理しています
TextBox クラス (System.Windows.Controls) | Microsoft Docs
Button クラス (System.Windows.Controls) | Microsoft Docs
コードビハインドでのイベント処理
MVVMじゃなくなっちゃいますがコードビハインドを使うことでWinFormっぽくイベントを処理することができます
xamlでViewの部品それぞれに名前とイベントの処理先を書いて、コードビハインドに処理を実装します
- CodeBehind.xaml (UIのレイアウト部分のみ)
<StackPanel Margin="5, 5" > <TextBlock Text="CodeBehind"/> <Button x:Name="button" Content="ボタンです" Click="button_Click"/> <TextBox x:Name="textBox" PreviewTextInput="textBox_PreviewTextInput"/> <TextBlock x:Name="textBlock" Text="TextBoxに入力した文字をここに表示します ボタンを押すと文字をすべて消去します"/> </StackPanel>
- CodeBehind.xaml.cs
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void button_Click(object sender, RoutedEventArgs e) { textBlock.Text = ""; } private void textBox_PreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e) { textBlock.Text += e.Text; } }
Prismでのイベント処理
PrismはMVVMを実現するためのサポートライブラリです
サンプルプログラムにはPrism.Unityパッケージが使われています
Prism.xamlでViewを作成し、PrismViewModelでイベントの処理や入力されたテキストの制御を行います
ソースコードは以下になります
- Prism.xaml
<UserControl x:Class="EventSample.Views.Prism" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:prism="http://prismlibrary.com/" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" prism:ViewModelLocator.AutoWireViewModel="True"> <StackPanel Margin="5, 5"> <TextBlock Text="Prism"/> <Button Content="ボタンです" Command="{Binding ButtonClickCommand}"/> <TextBox Text="{Binding InputText}"> <i:Interaction.Triggers> <i:EventTrigger EventName="PreviewTextInput"> <prism:InvokeCommandAction Command="{Binding PreviewTextInputCommand}"/> </i:EventTrigger> </i:Interaction.Triggers> </TextBox> <TextBlock Text="{Binding OutputText}"/> </StackPanel> </UserControl>
- PrismViewModel.cs
public class PrismViewModel : BindableBase { private string inputText = ""; public string InputText { get { return inputText; } set { SetProperty(ref inputText, value); } } private string outputText = "TextBoxに入力した文字をここに表示します\r\nボタンを押すと文字をすべて消去します"; public string OutputText { get { return outputText; } set { SetProperty(ref outputText, value); } } private DelegateCommand<RoutedEventArgs> buttonClickCommand; public DelegateCommand<RoutedEventArgs> ButtonClickCommand => buttonClickCommand ?? (buttonClickCommand = new DelegateCommand<RoutedEventArgs>(ExecuteButtonClickCommand)); private DelegateCommand<TextCompositionEventArgs> previewInputTextCommand; public DelegateCommand<TextCompositionEventArgs> PreviewTextInputCommand => previewInputTextCommand ?? (previewInputTextCommand = new DelegateCommand<TextCompositionEventArgs>(ExecutePreviewInputText)); public PrismViewModel() { } void ExecuteButtonClickCommand(RoutedEventArgs e) { OutputText = ""; } void ExecutePreviewInputText(TextCompositionEventArgs e) { OutputText += e.Text; } }
ソースコードのポイントは以下のような感じ
- Prism.xaml 5行目
// 5: xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
イベントの処理に使用するためのライブラリをi:のプレフィックスに割り当てています
- Prism.xaml 9行目
// 9: <Button Content="ボタンです" Command="{Binding ButtonClickCommand}"/>
ボタンが押された時に呼び出すViewModel側のButtonClickCommand関数を呼び出すようにしています
- Prism.xaml 10~17行目
// 10: <TextBox Text="{Binding InputText}"> <i:Interaction.Triggers> <i:EventTrigger EventName="PreviewTextInput"> <prism:InvokeCommandAction Command="{Binding PreviewTextInputCommand}"/> </i:EventTrigger> </i:Interaction.Triggers> </TextBox> <TextBlock Text="{Binding OutputText}"/>
ポイント
- 10行目、17行目:TextBox, TextBlockに表示するテキストをViewModelのInputText, OutputTextプロパティにバインドしています
- 12行目:TextBoxで処理したいイベントの設定をしています(今回はPreviewTextInputイベント)
- 13行目:イベントが発生したときに呼び出すViewModelの関数を設定しています(PreviewTextInputCommand関数が呼び出されます)
PrismViewModel.cs 13~25行目
// 13: private string inputText = ""; public string InputText { get { return inputText; } set { SetProperty(ref inputText, value); } } private string outputText = "TextBoxに入力した文字をここに表示します\r\nボタンを押すと文字をすべて消去します"; public string OutputText { get { return outputText; } set { SetProperty(ref outputText, value); } }
TextBox, TextBlockのバインド先を定義しています
※Visual Studioの拡張機能にPrism Template Packをインストールしていてスニペットの設定もできていれば"propp" + Tabで自動入力できます
- PrismViewModel.cs 27~33行目
// 27: private DelegateCommand<RoutedEventArgs> buttonClickCommand; public DelegateCommand<RoutedEventArgs> ButtonClickCommand => buttonClickCommand ?? (buttonClickCommand = new DelegateCommand<RoutedEventArgs>(ExecuteButtonClickCommand)); private DelegateCommand<TextCompositionEventArgs> previewInputTextCommand; public DelegateCommand<TextCompositionEventArgs> PreviewTextInputCommand => previewInputTextCommand ?? (previewInputTextCommand = new DelegateCommand<TextCompositionEventArgs>(ExecutePreviewInputText));
ボタンが押された時とテキストが入力された時のイベントのバインド先を定義しています
※こちらも"cmdg" + Tabで自動入力できます
- PrismViewModel.cs 41~49行目
// 41: void ExecuteButtonClickCommand(RoutedEventArgs e) { OutputText = ""; } void ExecutePreviewInputText(TextCompositionEventArgs e) { OutputText += e.Text; }
- ポイント
- 43行目:ボタンが押された時の処理を書いています。OutputTextはViewのTextBlockにバインドされているのでプロパティの変更と同時に表示が更新されます
- 48行目:TextBoxに文字が入力された時の処理を書いています。入力された文字はe.Textに入っているのでそれをOutputTextに設定しています
Reactive Propertyでのイベント処理
Reactive PropertyはReactive ExtensionsをベースとしたMVVMのサポートライブラリです
Reactive Extensionsの知識があると理解しやすいですが、なくても簡単にイベントを処理することができます
NugetパッケージにReactivePropertyを追加すれば使用可能です
Prismと同様にReactiveProperty.xamlでViewを作成し、ReactivePropertyViewModel.csでイベントの処理や入力されたテキストの制御を行います
ソースコードはこちら
- ReactiveProperty.xaml
<UserControl x:Class="EventSample.Views.ReactiveProperty" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:prism="http://prismlibrary.com/" xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:rp="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.NETCore" prism:ViewModelLocator.AutoWireViewModel="True"> <StackPanel Margin="5, 5"> <TextBlock Text="ReactiveProperty" /> <Button Content="ボタンです" Command="{Binding ButtonClickCommand}"/> <TextBox Text="{Binding InputText.Value}"> <i:Interaction.Triggers> <i:EventTrigger EventName="PreviewTextInput"> <rp:EventToReactiveCommand Command="{Binding PreviewTextInputCommand}"/> </i:EventTrigger> </i:Interaction.Triggers> </TextBox> <TextBlock Text="{Binding OutputText.Value}"/> </StackPanel> </UserControl>
- ReactivePropertyViewModel.cs
public class ReactivePropertyViewModel : BindableBase { public ReactivePropertySlim<string> InputText { get; } = new ReactivePropertySlim<string>(""); public ReactiveProperty<string> OutputText { get; } = new ReactiveProperty<string>("TextBoxに入力した文字をここに表示します\r\nボタンを押すと文字をすべて消去します"); public ReactiveCommand<RoutedEventArgs> ButtonClickCommand { get; } = new ReactiveCommand<RoutedEventArgs>(); public ReactiveCommand<TextCompositionEventArgs> PreviewTextInputCommand { get; } = new ReactiveCommand<TextCompositionEventArgs>(); public ReactivePropertyViewModel() { this.ButtonClickCommand.Subscribe(e => { this.OutputText.Value = ""; }); this.PreviewTextInputCommand.Subscribe(e => { this.OutputText.Value += e.Text; }); } }
以下ポイント
- ReactiveProperty.xaml 5行目
// 5: xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
prismと同じようにイベントの処理に使用するためのライブラリをi:のプレフィックスに割り当てています
ただし、prismと参照するライブラリが違うので注意
参照するライブラリ
- Prism:xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
- ReactiveProperty:xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
ReactiveProperty.xaml 6行目
// 6: xmlns:rp="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.NETCore"
ReactivePropertyをrp:のプレフィックスに割り当てています(イベントの処理をバインドするときに使う)
- ReactiveProperty.xaml 10行目
// 10: <Button Content="ボタンです" Command="{Binding ButtonClickCommand}"/>
ボタンが押された時に呼び出すViewModel側のButtonClickCommand関数を呼び出すようにしています(Prismと同じ)
- Prism.xaml 11~18行目
// 11: <TextBox Text="{Binding InputText.Value}"> <i:Interaction.Triggers> <i:EventTrigger EventName="PreviewTextInput"> <rp:EventToReactiveCommand Command="{Binding PreviewTextInputCommand}"/> </i:EventTrigger> </i:Interaction.Triggers> </TextBox> <TextBlock Text="{Binding OutputText.Value}"/>
ポイント
- 11行目、18行目:表示するテキストをViewModelのInputText, OutputTextプロパティにバインド。Prismと違って.Valueを付ける必要があるので注意
- 13行目:TextBoxで処理したいイベントの設定をしています(Prismと同じ)
- 14行目:イベントが発生したときに呼び出すViewModelの関数を設定しています(PreviewTextInputCommand関数が呼び出されます)
ReactivePropertyViewModel.cs 14~16行目
// 14: public ReactivePropertySlim<string> InputText { get; } = new ReactivePropertySlim<string>(""); public ReactiveProperty<string> OutputText { get; } = new ReactiveProperty<string>("TextBoxに入力した文字をここに表示します\r\nボタンを押すと文字をすべて消去します");
TextBox, TextBlockのバインド先を定義しています
※ReactivePropertyのスニペットの設定もできていれば"rprop" + Tabで自動入力できます
- ReactivePropertyViewModel.cs 18~20行目
// 18: public ReactiveCommand<RoutedEventArgs> ButtonClickCommand { get; } = new ReactiveCommand<RoutedEventArgs>(); public ReactiveCommand<TextCompositionEventArgs> PreviewTextInputCommand { get; } = new ReactiveCommand<TextCompositionEventArgs>();
ボタンが押された時とテキストが入力された時のイベントのバインド先を定義しています
※こちらも"rcommg" + Tabで自動入力できます
- ReactivePropertyViewModel.cs 25~33行目
// 25: this.ButtonClickCommand.Subscribe(e => { this.OutputText.Value = ""; }); this.PreviewTextInputCommand.Subscribe(e => { this.OutputText.Value += e.Text; });
- ポイント
- 定義したコマンドをSubScribeするとイベントが発生したときに、ラムダ式内の処理が呼び出されるようになります
- SubScribeの中で処理をしているだけで27行目、32行目の処理はPrismとほとんど同じです
まとめ
以上、WPFのMVVMでイベントを処理する方法いろいろでした
個人的にはReactive Propertyが一番ViewModelをすっきり書ける気がするので自分はReactivePropertyをよく使っています
おわり