かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

MVVM でイベント引数の値を ViewModel のコマンドに渡す方法

こちらを見て、そういえばさらっと書いてるだけだったなぁと思ったので…。

elf-mission.net

イベント引数を ViewModel で使いたい

マウス系イベントや選択系イベントは、イベント引数にしか入ってない値とかもあったりして使いたくなりますよね。 まぁ、イベントハンドラーを普通に書いて、そこからコマンド呼んでも大したことはないんですが、一応ライブラリーによっては、仕組みが用意されていたりします。

ReactiveProperty の場合

UWP と WPF 向けに用意しています。 EventToReactiveCommand と EventToReactiveProperty になります。EventToReactiveCommand がトリガーの引数を変換処理も挟みつつ ReactiveCommand の Execute メソッドの引数に渡します。EventToReactiveProperty がトリガーの引数を変換処理も挟みつつ ReactiveProeprty の Value に設定します。

これは、基本的に EventTrigger の下に置く Action として想定しています。別に他のトリガーでも動きますが。 何もしないと設定した ReactiveCommand と ReactiveProperty にトリガーに渡された引数をそのまま渡します。 ただ、引数がそのままわたるのは、ちょっとなぁ…という場合には引数を ViewModel のレイヤーのオブジェクトに変換するためのコンバーターが用意されています。

例として、マウスを動かすと、その座標を画面に表示するものを作ってみようと思います。

ViewModel の作成

とりあえず、ViewModel 側でマウス座標を表す MousePosition というクラスを作ります。

namespace ReactivePropertySample
{
    public class MousePosition
    {
        public double X { get; set; }
        public double Y { get; set; }
    }
}

そして MainWindow 用の ViewModel を作ります。 こいつは ReactiveCommand<MousePosition> と、コマンドで受け取ったものを文字列に変換して ReadOnlyReactivePropertySlim にしています。

using Reactive.Bindings;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;

namespace ReactivePropertySample
{
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ReactiveCommand<MousePosition> MouseMoveCommand { get; }

        public ReadOnlyReactivePropertySlim<string> Message { get; }

        public MainWindowViewModel()
        {
            MouseMoveCommand = new ReactiveCommand<MousePosition>();
            Message = MouseMoveCommand.Select(x => $"({x.X}, {x.Y})")
                .ToReadOnlyReactivePropertySlim();
        }
    }
}

では、この ViewModel と View をつないでいきます。まず MouseMove イベントの引数の MouseEventArgs から MousePosition への変換処理を書きます。これは ReactiveConverter<変換元型名, 変換先型名> クラスを継承して OnConvert メソッドをオーバーライドして書きます。 こんな感じ。

using Reactive.Bindings.Interactivity;
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Input;

namespace ReactivePropertySample
{
    public class MouseMoveToMousePositionConverter : ReactiveConverter<MouseEventArgs, MousePosition>
    {
        protected override IObservable<MousePosition> OnConvert(IObservable<MouseEventArgs> source) => source
            .Select(x => x.GetPosition((IInputElement)AssociateObject))
            .Select(x => new MousePosition
            {
                X = x.X,
                Y = x.Y,
            });
    }
}

あとは、View で EventTrigger と EventToReactiveCommand と 先ほど作成した MouseMoveToMousePositionConverter を使ってイベントと ReactiveCommand をつなぎます。

<Window
    x:Class="ReactivePropertySample.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:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:interactivity="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.NET46"
    xmlns:local="clr-namespace:ReactivePropertySample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseMove">
            <interactivity:EventToReactiveCommand Command="{Binding MouseMoveCommand}">
                <local:MouseMoveToMousePositionConverter />
            </interactivity:EventToReactiveCommand>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <TextBlock
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Text="{Binding Message.Value}" />
    </Grid>
</Window>

実行してマウスを動かすと座標が画面に出ます。

f:id:okazuki:20190419163135p:plain

Prism の場合

Prism の場合は WPF と Xamarin.Forms 用で似たような機能が提供されています。大体同じ使い方(WPF は Trigger の Action として、Xamarin.Forms は Behavior として提供されています)なので今回は WPF の使い方を試してみます。

Prism のクラスとして InvokeCommandAction があります。 これは Command プロパティに指定したコマンドを呼び出すアクションです。

この時コマンドの引数に渡すものを指定する方法として CommandParameter プロパティに指定する方法と、TriggerParameterPath に指定する方法の 2 通りがあります。CommandParameter は普通に Binding とかを指定できます。TriggerParameterPath は、トリガーのパラメーター(EventTrigger の場合はイベント引数)から、任意のプロパティを受け渡します。

今回は、イベント引数の GetPosition メソッドを呼んだ結果を受け渡したいので…無理じゃん!

どうしよう

逃げ道としては、イベント引数をそのまま受け取る方法ですが、今回は GetPosition を呼びたい。引数は View のクラス…辛い。おとなしくコードビハインドですね。何も問題ない。

でも、こういうケースがアプリ内でたくさんあるなら部品化する価値はある。ということで部品化するとしたら、Prism の InvokeCommandAction を参考にして間に任意の変換処理を挟むための IHogeHogeConverter Converter { get; set; } プロパティでも追加してあげる感じでしょうか…?

いっそのことプルリクエストしてもいいかもしれませんね。

Livet の場合

こっちも特にイベント引数を加工するような仕組みはないように見えるので、皆どうしてるんだろう?機能欲しい?

まとめ

意外と ReactiveProperty のやつが何でもやりたい放題だった。