じゃがいも畑

開発ネタの記録

WPFのMVVMでイベントを処理する方法いろいろ

最近PrismやらReactive Propertyやら勉強中なので忘れないように書いていきます

  • コードビハインド
  • Prism
  • Reactive Property

を使ってイベントを処理するサンプルを作成しました

サンプルコードはこちら github.com

開発環境はVisual Studio Community 2019で対象のフレームワークは.NET Core 3.0です

サンプルの中身

サンプルアプリはこんな感じの画面になっていて

f:id:whitedog0215:20200315134648p:plain

  • 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に入力した文字をここに表示します&#10;ボタンを押すと文字をすべて消去します"/>
    </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を実現するためのサポートライブラリです

prismlibrary.com

サンプルプログラムにはPrism.Unityパッケージが使われています f:id:whitedog0215:20200315143244p:plain

Prism.xamlでViewを作成し、PrismViewModelでイベントの処理や入力されたテキストの制御を行います

ソースコードは以下になります

<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の知識があると理解しやすいですが、なくても簡単にイベントを処理することができます

github.com

NugetパッケージにReactivePropertyを追加すれば使用可能です

f:id:whitedog0215:20200315171227p:plain

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と参照するライブラリが違うので注意

// 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をよく使っています

おわり