読者です 読者をやめる 読者になる 読者になる

ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

ViewModelをシンプルに書きたい! - ②イベント接続編 -

はい。この辺からはオレオレな感じになっていきますよー。
イベントですよね。直でViewModelと接続したい。
そういう時は、EventTriggerつかって・・・。

それもいいですけど、こんな感じで書きたいなー。

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

namespace WpfApp
{
    public class MainWindowVM
    {
        public ReactiveProperty<string> Number { get; } = new ReactiveProperty<string>("0");
        public ReactiveProperty<bool> CanIncrement { get; }

        public MainWindowVM()
        {
            int num = 0;
            CanIncrement = Number.Select(e => int.TryParse(e, out num)).ToReactiveProperty();
        }

        public void Increment()
        {
            Number.Value = (int.Parse(Number.Value) + 1).ToString();
        }
    }
}
<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:c="clr-namespace:VVMConnection;assembly=VVMConnection"
        xmlns:l="clr-namespace:WpfApp"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <l:MainWindowVM/>
    </Window.DataContext>

    <StackPanel>
        <StackPanel Orientation="Horizontal">
            <TextBox Width="100" Text="{Binding Number.Value, UpdateSourceTrigger=PropertyChanged}"/>
            <Button Content="インクリメント" IsEnabled="{Binding CanIncrement.Value}" Click="{c:Event Increment}"/>
        </StackPanel>
    </StackPanel>
</Window>

ポイントはここですね。

Click="{c:Event Increment}"

で、MarkupExtension使ってやってみました。
実装時にはこちらのサイトを参考にさせていただきました。こちらはコマンドとつなげてますね。
sourcechord.hatenablog.com

using System;
using System.Reflection;
using System.Windows;
using System.Windows.Markup;
using System.Linq;
using VVMConnection.Inside;

namespace VVMConnection
{
    public class EventExtension : MarkupExtension
    {
        public string _path;
        public string _bridge;

        public EventExtension(string path)
        {
            _path = path;
        }

        public EventExtension(string path, string bridge)
        {
            _path = path;
            _bridge = bridge;
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            var targetProvider = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            if (targetProvider == null)
            {
                return null;
            }

            var target = targetProvider.TargetObject as FrameworkElement;
            if (target == null)
            {
                return null;
            }

            //イベント情報取得
            //添付プロパティーの場合はMethodInfo
            Type handlerType = null;
            var eventInfo = targetProvider.TargetProperty as EventInfo;
            if (eventInfo != null)
            {
                handlerType = eventInfo.EventHandlerType;
            }
            else
            {
                var method = targetProvider.TargetProperty as MethodInfo;
                handlerType = method.GetParameters()[1].ParameterType;
            }

            //イベント実行メソッド
            Action<object, object> invoke = null;
            if (string.IsNullOrEmpty(_bridge))
            {
                //ブリッジ無しの場合は引数なしで呼び出し
                invoke = (_, __) => target.DataContext.GetType().GetMethod(_path).Invoke(target.DataContext, new object[0]);
            }
            else
            {
                //ブリッジを通して呼び出し
                var items = _bridge.Split('.');
                if (items.Length < 2)
                {
                    return null;
                }
                var bridgeType = TypeFinder.GetType(string.Join(".", items.Take(items.Length - 1)));
                if (bridgeType == null)
                {
                    return null;
                }
                var methodInfo = bridgeType.GetMethod(items[items.Length - 1]);
                var args = methodInfo.GetParameters();
                if (args.Length != 3 && typeof(Delegate).IsAssignableFrom(args[2].ParameterType))
                {
                    return null;
                }
                invoke = (o, e) => methodInfo.Invoke(null,
                    new object[] { o, e, target.DataContext.GetType().GetMethod(_path).CreateDelegate(args[2].ParameterType, target.DataContext) });
            }

            //直接イベントを受けるクラスを作り、それにinvokeを呼び出させる
            var arguments = handlerType.GetMethod("Invoke").GetParameters();
            var conType = typeof(Listener<,>).MakeGenericType(arguments[0].ParameterType, arguments[1].ParameterType);
            return conType.GetMethod("Connect").Invoke(null, new object[] { handlerType, invoke });
        }

        static class Listener<O,E>
        {
            public class Connector
            {
                Action<object, object> _core;
                public Connector(Action<object, object> core) { _core = core; }
                public void Invoke(O o, E e) => _core(o, e);
            }

            public static object Connect(Type handlerType, Action<object, object> core)
            {
                var connector = new Connector(core);
                return Delegate.CreateDelegate(handlerType, connector, connector.GetType().GetMethod("Invoke"));
            }
        }
    }
}

ちょっと前からイベントハンドラに対してもMarkupExtension使えるようになってたみたいです。折角なので一工夫加えてみました。イベントハンドラの引数の情報をViewModelに伝えたいときですね。EventArgsを直接渡すのは若干の罪悪感。なのでブリッジメソッドを指定できるというオレオレ仕様を入れましたw。ブリッジメソッドは実際のイベント情報と呼び出し先のDelegateを受け取るようにします。そしてXamlではそのブリッジ関数をネームスペースからのフルパスで指定するという鬼仕様。
例えば、ファイルドロップなどはこんな感じ。Viewで扱うイベント引数の情報より、まろやかな情報に変換して渡します。直接Viewのコードビハインドでイベント受けるのとの違いは、ブリッジ関数を部品化して色んな箇所で使いまわせるとこですね。

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:WpfApp"
        xmlns:c="clr-namespace:VVMConnection;assembly=VVMConnection"

        AllowDrop="True"
        Drop="{c:Event ExecuteFiles, WpfApp.BridgeDrop.Files }"

        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <l:MainWindowVM/>
    </Window.DataContext>
</Window>

VMはこんな感じ

namespace WpfApp
{
    public class MainWindowVM
    {
        public void ExecuteFiles(string[] files)
        {
            //ファイルに対する処理・・・
        }
    }
}

ブリッジ関数
特に何かを継承するわけではなくstaticな関数を指定するようにしてみました。最後のDelegateはViewModelの要求に合わせて自由に変更できます。

using System;
using System.Windows;

namespace WpfApp
{
    class BridgeDrop
    {
        public static void Files(object sender, DragEventArgs e, Action<string[]> core)
        {
            string[] files = e.Data.GetData(DataFormats.FileDrop) as string[];
            if (files != null)
            {
                core(files);
            }
        }
    }
}

こっちに、コード上げてます。

興味があるかたはダウンロードして動かしてみてください。
他にもいくつか小ネタのコード入れたあるので、次回以降で説明していきます。
github.com