LoginSignup
9
9

More than 5 years have passed since last update.

Xamarin.Forms に CallMethodAction が無かったので Behavior で代用してみた

Last updated at Posted at 2015-02-08

CallMethodActionを使いたい

Xamarin.Forms 1.3.0でBehaviors、Triggers、Actionsが追加されましたが CallMethodAction が含まれていなかったので自作することにしました。
CallMethodActionはCommandを経由せずにViewModelのメソッドを呼び出せる便利なヤツです。

やりたいことはこんな感じ。

<!-- こうしたい -->
<Button>
    <Button.Triggers>
        <EventTrigger EventName="Clicked">
            <CallMethodAction
                TargetObject="{Binding}"
                MethodName="Greet" />
        </EventTrigger>
    </Button.Triggers>
</Button>

通常、自作Actionは TriggerAction から派生します。しかし、 TriggerActionBindableObject から派生していないためBindablePropertyを持てません。そこで今回はBehavior<T>派生クラスとして実装することにしました。
ActionをBehaviorで代用するのでTriggerセットできず、発火処理も自作しなければいけなくなります。

最終的にこんな感じになります。

<Button>
    <Button.Behaviors>
        <!-- EventTrigger、CallMethodActionをBehaivorで代用 -->
        <local:EventTriggerCallMethodBehavior
            EventName="Clicked"
            TargetObject="{Binding}"
            MethodName="Greet" />
    </Button.Behaviors>
</Button>

ソースコード

最初にソースコード全体を貼ってから個別に解説していきます。

    public class EventTriggerCallMethodBehavior: Behavior<View>
    {
        #region MethodName バインド可能プロパティ
        public static readonly BindableProperty MethodNameProperty =
            BindableProperty.Create<CallMethodBehaviorBase,string> (p => p.MethodName, default(string));

        public string MethodName {
            get { return (string)GetValue (MethodNameProperty); }
            set { SetValue (MethodNameProperty, value); } 
        }
        #endregion

        #region TargetObject バインド可能プロパティ
        public static readonly BindableProperty TargetObjectProperty =
            BindableProperty.Create<CallMethodBehaviorBase,object> (p => p.TargetObject, default(object));

        public object TargetObject {
            get { return (object)GetValue (TargetObjectProperty); }
            set { SetValue (TargetObjectProperty, value); } 
        }
        #endregion

        #region EventName バインド可能プロパティ
        public static readonly BindableProperty EventNameProperty =
            BindableProperty.Create<EventTriggerCallMethodBehavior,string> (p => p.EventName, default(string));

        public string EventName {
            get { return (string)GetValue (EventNameProperty); }
            set { SetValue (EventNameProperty, value); } 
        }
        #endregion

        protected override void OnAttachedTo (View bindable)
        {
            base.OnAttachedTo (bindable);

            // TargetObject="{Binding}" でアタッチ先のBindingContextを参照するよう伝播させる
            bindable.BindingContextChanged += HandleBindingContextChanged;
            BindingContext = bindable.BindingContext;

            SubscribeEvent (bindable);
        }

        protected override void OnDetachingFrom (View bindable)
        {
            UnsubscribeEvent (bindable);

            bindable.BindingContextChanged -= HandleBindingContextChanged;
            BindingContext = null;

            base.OnDetachingFrom (bindable);
        }

        private void HandleBindingContextChanged (object sender, EventArgs e)
        {
            BindingContext = ((View)sender).BindingContext;
        }

        protected void CallMethod()
        {
            if (TargetObject == null ||
                string.IsNullOrEmpty(MethodName))
                return;

            var methodInfo = TargetObject.GetType().GetMethod (MethodName);
            if (methodInfo == null)
                return;

            methodInfo.Invoke (TargetObject, null);
        }

        // 購読中のイベント
        private EventInfo subscribingEvent;

        // イベントハンドラ
        private void Invoke(object sender, EventArgs e)
        {
            CallMethod ();
        }

        // リフレクションで登録するイベントハンドラのインスタンス
        private Delegate _Handler;
        private Delegate Handler {
            get {
                if (_Handler == null)
                {
                    var handlerMethodInfo = 
                        typeof(EventTriggerCallMethodBehavior)
                            .GetMethod("Invoke", BindingFlags.NonPublic | BindingFlags.Instance);

                    _Handler = Delegate.CreateDelegate (typeof(EventHandler), this, handlerMethodInfo);
                }

                return _Handler;
            }
        }

        private void SubscribeEvent(object associatedObject)
        {
            // リフレクションでEventInfoを取得
            this.subscribingEvent = associatedObject.GetType ().GetEvent (EventName);
            if (subscribingEvent == null)
                return;

            // リフレクションでイベントハンドラを登録
            // (bindable.EventName += Invoke に相当)
            subscribingEvent.GetAddMethod ().Invoke (associatedObject, new[]{ this.Handler } );
        }

        private void UnsubscribeEvent(object associatedObject)
        {
            if (this.subscribingEvent == null)
                return;

            // リフレクションでイベントハンドラを解除
            // (bindable.EventName -= Invoke に相当)
            this.subscribingEvent.GetRemoveMethod ().Invoke (associatedObject, new[]{ this.Handler } );
        }
    }

アタッチ・デタッチ

Behaviorがコントロールに登録されたるタイミング(OnAttachedTo)でイベントハンドラを登録します。また、メモリリークが発生しないように登録解除されるタイミング(OnDetachingFrom)にイベントハンドラを解除します。

        protected override void OnAttachedTo (View bindable)
        {
            base.OnAttachedTo (bindable);

            // TargetObject="{Binding}" でアタッチ先のBindingContextを参照するよう伝播させる
            bindable.BindingContextChanged += HandleBindingContextChanged;
            BindingContext = bindable.BindingContext;

            SubscribeEvent (bindable);
        }

        protected override void OnDetachingFrom (View bindable)
        {
            UnsubscribeEvent (bindable);

            bindable.BindingContextChanged -= HandleBindingContextChanged;
            BindingContext = null;

            base.OnDetachingFrom (bindable);
        }

リフレクションによるイベントハンドラ登録

アタッチされたコントロールの型情報からGetEventメソッドでXAMLから名前で指定したイベントのEventInfoを取得します。このEventInfoは解除する時に使うので保持します。

EventInfoからGetAddMethodメソッドでAddメソッドを取得してイベントハンドラを登録しています。ここでイベントを持っているインスタンスとイベントハンドラのインスタンスが必要になります。

        private void SubscribeEvent(object associatedObject)
        {
            // リフレクションでEventInfoを取得
            this.subscribingEvent = associatedObject.GetType ().GetEvent (EventName);
            if (subscribingEvent == null)
                return;

            // リフレクションでイベントハンドラを登録
            // (bindable.EventName += Invoke に相当)
            subscribingEvent.GetAddMethod ().Invoke (associatedObject, new[]{ this.Handler } );
        }

イベントハンドラ

イベントハンドラの型は(object sender, EventArgs e)を引数にとるEventHandler型です。この型にすることでevent EventHandler<T>で定義された全てのイベントに登録できます。CallMethodを発火させたいだけなのでカスタムされたEventArgsは必要ありません。

GetAddMethodで必要なDelegateインスタンスはDelegate.CreateDelegateメソッドで作成します。これは1度作れば十分なので使いまわします。

(typeof演算子のように、コンパイル時にMethodInfoを取得できる演算子はありませんよね?)

        // イベントハンドラ
        private void Invoke(object sender, EventArgs e)
        {
            CallMethod ();
        }

        // リフレクションで登録するイベントハンドラのインスタンス
        private Delegate _Handler;
        private Delegate Handler {
            get {
                if (_Handler == null)
                {
                    var handlerMethodInfo = 
                        typeof(EventTriggerCallMethodBehavior)
                            .GetMethod("Invoke", BindingFlags.NonPublic | BindingFlags.Instance);

                    _Handler = Delegate.CreateDelegate (typeof(EventHandler), this, handlerMethodInfo);
                }

                return _Handler;
            }
        }

ターゲットメソッド呼び出し

TargetObjectの型情報からGetMethodでMethodNameに指定したメソッドのMethodInfoを取得、MethodInfo.Invokeで起動するだけです。引数無しなのでInvokeの第2引数はnullにします。

本家のCallMethodActionに準じてパラメータを取っていませんが、MethodParameterのようなBindablePropertyを追加してもいいかも知れませんね。

        protected void CallMethod()
        {
            if (TargetObject == null ||
                string.IsNullOrEmpty(MethodName))
                return;

            var methodInfo = TargetObject.GetType().GetMethod (MethodName);
            if (methodInfo == null)
                return;

            methodInfo.Invoke (TargetObject, null);
        }

使ってみる

こんな感じのViewModelを作って...

using System;
using Xamarin.Forms;

namespace CallMethodSample
{
    public class GreetViewModel : BindableObject
    {
        private string _Message;
        public string Message {
            get { return _Message; }
            set
            {
                if (_Message == value)
                    return;
                _Message = value;
                OnPropertyChanged ("Message");
            }
        }

        public void Greet()
        {
            Message = "Hello!";
        }
    }
}

こんな感じのPageを作ってAppクラスのMainPageにセットします。

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:CallMethodSample;assembly=CallMethodSample"
             x:Class="CallMethodSample.GreetPage">
    <ContentPage.BindingContext>
        <local:GreetViewModel />
    </ContentPage.BindingContext>
    <ContentPage.Content>
        <StackLayout Orientation="Vertical">
            <Label  VerticalOptions="EndAndExpand"
                    HorizontalOptions="CenterAndExpand"
                    Text="{Binding Message}" />
            <Button Text="Click me!"
                    HorizontalOptions="CenterAndExpand"
                    VerticalOptions="StartAndExpand">
                <Button.Behaviors>
                    <local:EventTriggerCallMethodBehavior
                        EventName="Clicked"
                        MethodName="Greet"
                        TargetObject="{Binding }" />
                </Button.Behaviors>
            </Button>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

Buttonのクリックイベントに反応してGreetメソッドが呼ばれ、メッセージが変わりました!

IMG_0480.PNG IMG_0481.PNG

リフレクションを使っていても動的コード生成は行っていないので実機iOSでも使えます。(画像は実機のスクリーンショットです)

参考

方法 : リフレクションを使用してデリゲートをフックする

9
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9