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



## CallMethodActionを使いたい

Xamarin.Forms 1.3.0でBehaviors、Triggers、Actionsが追加されましたが `CallMethodAction` が含まれていなかったので自作することにしました。
[CallMethodAction](https://msdn.microsoft.com/ja-jp/library/microsoft.expression.interactivity.core.callmethodaction%28v=expression.40%29.aspx)はCommandを経由せずにViewModelのメソッドを呼び出せる便利なヤツです。

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

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

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

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

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


## ソースコード

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


```c#:
	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`)にイベントハンドラを解除します。

```c#:
		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メソッドを取得してイベントハンドラを登録しています。ここでイベントを持っているインスタンスとイベントハンドラのインスタンスが必要になります。

```c#:
		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`メソッドで作成します。これは１度作れば十分なので使いまわします。


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

```c#:
		// イベントハンドラ
		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`の第２引数は`null`にします。

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

```c#:
		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を作って...

```c#:
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:
<?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](https://qiita-image-store.s3.amazonaws.com/0/40121/a49f67e3-7869-696e-2fbe-6831d0a8d584.png "IMG_0480.PNG") ![IMG_0481.PNG](https://qiita-image-store.s3.amazonaws.com/0/40121/e29207f4-bff2-3e19-4453-188abf12549b.png "IMG_0481.PNG")

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

## 参考

[方法 : リフレクションを使用してデリゲートをフックする](https://msdn.microsoft.com/ja-jp/library/ms228976%28v=vs.110%29.aspx)