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

ささいなことですが。

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

ViewModelをシンプルに書きたい! - ⑤Delegate→Method接続仕組み実装編 -

前回はこんな感じでViewModelのDelegateと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"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <l:MainWindowVM/>
    </Window.DataContext>

    <!--↓↓↓メソッド接続部↓↓↓-->
    <c:Connection.Methods>
        <c:MethodCollection>
            <c:Method Name ="ShowText" Invoker="NotifyText"/>
            <c:Method Name ="Show" Invoker="Ask">
                <c:Method.Target>
                    <c:MessageBox Caption="Title" Button="{x:Static MessageBoxButton.YesNo}"/>
                </c:Method.Target>
            </c:Method>
        </c:MethodCollection>
    </c:Connection.Methods>
    <!--↑↑↑メソッド接続部↑↑↑-->

    <StackPanel>
        <Button Content="会話" Command="{c:Command Communication }"/>
        <TextBlock Text="{Binding Reply.Value}"/>
    </StackPanel>
</Window>

で、今回は

<c:Connection.Methods>
<c:MethodCollection>
<c:Method Name ="Show" Invoker="Ask">

の実装内容の紹介です。
実装にあたって、この記事を参考にさせていただきました。
MVVM:Messenger + Behaviorを理解するために自作してみた(1):Gushwell's C# Dev Notes

まずは、Connection.Methodsです。これは添付プロパティーで実装しました。上のXamlではWindowクラスに添付でConnection.Methodsを設定しています。
Behaviorにするか迷ったのですけどね。僅差の判断でこの方針にしました。

using System.Windows;
using System.Linq;

namespace VVMConnection
{
    public class Connection
    {
        public static MethodCollection GetMethods(DependencyObject obj)=> (MethodCollection)obj.GetValue(MethodsProperty);

        public static void SetMethods(DependencyObject obj, MethodCollection value)=> obj.SetValue(MethodsProperty, value);

        public static readonly DependencyProperty MethodsProperty =
                DependencyProperty.RegisterAttached(
                    "Methods",
                    typeof(MethodCollection),
                    typeof(Connection),
                    new PropertyMetadata(null, Changed));

        static void Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var element = sender as FrameworkElement;
            if (element == null)
            {
                return;
            }
            var col = sender.GetValue(MethodsProperty) as MethodCollection;
            if (col == null)
            {
                return;
            }
            //接続を実行する
            col.ToList().ForEach(x => x.Connect(element));
        }
    }
}

Methodsが設定されたタイミングはDependencyPropertyに設定したコールバックで取得できます。このとき添付先のFrameworkElementも取得できるの接続が実行できます。
で、実際の接続コードはMethodクラスに実装しています。

using System;
using System.Reflection;
using System.Windows;
using System.Linq;

namespace VVMConnection
{
    public class Method
    {
        public string Name { get; set; }
        public string Invoker { get; set; }
        public object Target { get; set; }

        public void Connect(FrameworkElement element)
        {
            if (element == null || Invoker == null || Name == null)
            {
                return;
            }
            var connect = MakeConnectAction(element);
            if (connect == null)
            {
                return;
            }

            //最初に接続を試みる
            connect();
            //それからDataContextが切り替わったタイミングでも再接続
            element.DataContextChanged += (_, __) => connect();
        }

        Action MakeConnectAction(FrameworkElement target)
        {
            return () =>
            {
                if (target.DataContext == null)
                {
                    return;
                }
                var invokerProp = target.DataContext.GetType().GetProperty(Invoker);
                if (invokerProp == null)
                {
                    return;
                }
                var method = GetMethod(target, invokerProp);
                if (method == null)
                {
                    return;
                }
                invokerProp.SetValue(target.DataContext, method);
            };
        }

        Delegate GetMethod(FrameworkElement element, PropertyInfo invokerProp)
        {
            object target = null;
            if (Target != null)
            {
                target = Target;
            }
            else if (element != null)
            {
                target = element;
            }
            else
            {
                return null;
            }

            //Delegateの型から引数を取得する方法
            //DelegateはInvokeというメソッドを持っている
            var invokeInfo = invokerProp.PropertyType.GetMethod("Invoke");
            if (invokeInfo == null)
            {
                return null;
            }

            //オーバーロードを考慮に入れてメソッド取得
            var targetMethodInfo = target.GetType().GetMethod(Name, invokeInfo.GetParameters().Select(e=>e.ParameterType).ToArray());
            if (targetMethodInfo == null)
            {
                return null;
            }

            //接続先のプロパティーの型に合わせてDelegate作成
            return targetMethodInfo.CreateDelegate(invokerProp.PropertyType, target);
        }
    }
}

他のでもそうですが、リフレクションで力技ですね。ポイントはConnectが呼び出されたタイミングでDataContextがあるわけではないということと、DataContext自体途中で取り換えられる可能性があるということです。なのでDataContextChangedのイベントを受けて接続処理を実行するようにしてます。後のちょっとしたリフレクション的なポイントはコメントで書いてみました。

連載で使っているMarkup拡張のコードはこちらにおいてます。

github.com