Xamarin.Forms でイベントに Command をバインドするための Behavior を作成する

前回、添付プロパティを追加して Command をバインドする方法を試しました。
[Xamarin.Forms でイベントに Command をバインドする](http://qiita.com/norimakiXLVI/items/36b6e6157f4b3af8283c)

バインドする事はできましたが、この方法はイベント種類毎に添付プロパティを用意する必要があり汎用的ではありません。
そのため、今回は Command をバインドするための Behavior を作成してみたいと思います。

# 作成した Behavior のソース
作成した EventToCommand Behavior のソースは以下のようなものです。

```csharp:EventToCommand.cs
using System;
using System.Reflection;
using System.Windows.Input;
using Xamarin.Forms;

namespace XFEventToCommandBehavior
{
	public class EventToCommand : Behavior<VisualElement>
	{
		public static readonly BindableProperty EventNameProperty =
			BindableProperty.Create(
				"EventName",
				typeof(string),
				typeof(EventToCommand),
				"",
				propertyChanged: OnEventNameChanged);

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

		public static readonly BindableProperty CommandProperty =
			BindableProperty.Create(
				"Command",
				typeof(ICommand),
				typeof(EventToCommand),
				null);

		public ICommand Command
		{
			get { return (ICommand)GetValue(CommandProperty); }
			set { SetValue(CommandProperty, value); }
		}

		public static readonly BindableProperty ConverterProperty =
			BindableProperty.Create(
				"Converter",
				typeof(IValueConverter),
				typeof(EventToCommand),
				null);

		public IValueConverter Converter
		{
			get { return (IValueConverter)GetValue(ConverterProperty); }
			set { SetValue(ConverterProperty, value); }
		}

		private Delegate eventHandler;

		private VisualElement associatedObject;

		private static void OnEventNameChanged(
			BindableObject bindable, object oldValue, object newValue)
		{
			var behavior = bindable as EventToCommand;
			if (behavior.associatedObject == null)
			{
				return;
			}

			var oldEventName = oldValue as string;
			var newEventName = newValue as string;

			behavior.DeregisterEvent(oldEventName);
			behavior.RegisterEvent(newEventName);
		}

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

			associatedObject = bindable;

			if (bindable.BindingContext != null)
			{
				BindingContext = bindable.BindingContext;
			}
			bindable.BindingContextChanged += OnBindingContextChanged;

			RegisterEvent(EventName);
		}

		protected override void OnDetachingFrom(VisualElement bindable)
		{
			DeregisterEvent(EventName);

			bindable.BindingContextChanged -= OnBindingContextChanged;

			associatedObject = null;

			base.OnDetachingFrom(bindable);
		}

		protected override void OnBindingContextChanged()
		{
			base.OnBindingContextChanged();

			BindingContext = associatedObject.BindingContext;
		}

		private void OnBindingContextChanged(object sender, EventArgs e)
		{
			OnBindingContextChanged();
		}

		private void RegisterEvent(string name)
		{
			var eventInfo = associatedObject.GetType().GetRuntimeEvent(name);
			if (eventInfo == null)
			{
				return;
			}

			var methodInfo = typeof(EventToCommand).GetTypeInfo().GetDeclaredMethod("OnEvent");
			eventHandler = methodInfo.CreateDelegate(eventInfo.EventHandlerType, this);
			eventInfo.AddEventHandler(associatedObject, eventHandler);
		}

		private void DeregisterEvent(string name)
		{
			if (eventHandler == null)
			{
				return;
			}

			var eventInfo = associatedObject.GetType().GetRuntimeEvent(name);
			if (eventInfo == null)
			{
				return;
			}

			eventInfo.RemoveEventHandler(associatedObject, eventHandler);
			eventHandler = null;
		}

		private void OnEvent(object sender, object eventArgs)
		{
			if (Command != null)
			{
				object param = eventArgs;

				if (Converter != null)
				{
					param = Converter.Convert(eventArgs, typeof(object), null, null);
				}

				if (Command.CanExecute(param))
				{
					Command.Execute(param);
				}
			}
		}
	}
}
```

# 作成した Behavior の説明
作成した Behavior をいくつかにわけて説明します。

## 添付プロパティ
以下の三種類の添付プロパティを定義しています。

```csharp
public static readonly BindableProperty EventNameProperty =
	BindableProperty.Create(
		"EventName",
		typeof(string),
		typeof(EventToCommand),
		"",
		propertyChanged: OnEventNameChanged);

public static readonly BindableProperty CommandProperty =
	BindableProperty.Create(
		"Command",
		typeof(ICommand),
		typeof(EventToCommand),
		null);

public static readonly BindableProperty ConverterProperty =
	BindableProperty.Create(
		"Converter",
		typeof(IValueConverter),
		typeof(EventToCommand),
		null);
```

それぞれ以下の用途で使用します。

* EventName
    * 対象のイベント名を指定します。
* Command
    * イベント発生時に呼び出すコマンド（ICommand）を指定します。
* Converter
    * イベント引数を変換するためのコンバータ（IValueConverter）を指定します。
    * 省略した場合は、発生したイベントのイベント引数をそのままコマンドに渡します。
    * 指定した場合は、イベント引数をコンバータを使って変換してからコマンドに渡します。

## アタッチおよびイベント名変更
Behavior が VisualElement にアタッチされた場合の処理とイベント名プロパティが変更された場合の処理です。

```csharp
private static void OnEventNameChanged(
	BindableObject bindable, object oldValue, object newValue)
{
	var behavior = bindable as EventToCommand;
	if (behavior.associatedObject == null)
	{
		return;
	}

	var oldEventName = oldValue as string;
	var newEventName = newValue as string;

	behavior.DeregisterEvent(oldEventName);
	behavior.RegisterEvent(newEventName);
}

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

	associatedObject = bindable;

	if (bindable.BindingContext != null)
	{
		BindingContext = bindable.BindingContext;
	}
	bindable.BindingContextChanged += OnBindingContextChanged;

	RegisterEvent(EventName);
}
```

以下のような処理を行います。

* OnAttachedTo（アタッチ）
    * 対象 VisualElement の保存、BindingContext の伝搬、イベント登録等を行います。
    実際のイベント登録処理（RegisterEvent）の詳細は後述します。
* OnEventNameChanged（イベント名変更）
    * イベント名変更イベントハンドラです。
    * 対象 VisualElement が保存されている（OnAttachedTo 実行済み）場合は、イベント登録を解除してから登録し直します。

## イベント登録
アタッチまたはイベント名変更時に呼び出され、イベント名プロパティで指定されたイベントのハンドラを登録します。

```csharp
private void RegisterEvent(string name)
{
	var eventInfo = associatedObject.GetType().GetRuntimeEvent(name);
	if (eventInfo == null)
	{
		return;
	}

	var methodInfo = typeof(EventToCommand).GetTypeInfo().GetDeclaredMethod("OnEvent");
	eventHandler = methodInfo.CreateDelegate(eventInfo.EventHandlerType, this);
	eventInfo.AddEventHandler(associatedObject, eventHandler);
}
```

## イベントハンドラ
指定イベントが発生した時に呼び出されるイベントハンドラです。

```csharp
private void OnEvent(object sender, object eventArgs)
{
	if (Command != null)
	{
		object param = eventArgs;

		if (Converter != null)
		{
			param = Converter.Convert(eventArgs, typeof(object), null, null);
		}

		if (Command.CanExecute(param))
		{
			Command.Execute(param);
		}
	}
}
```

Converter が指定されている場合はイベント引数を変換してからコマンド実行します。

# 作成した Behavior の使い方
作成した Behavior を使用しているページの xaml は以下のようなものです。

```xml:MainPage.xaml
<?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:vm="clr-namespace:XFEventToCommandBehavior.Sample.ViewModels;assembly=XFEventToCommandBehavior.Sample"
             xmlns:vw="clr-namespace:XFEventToCommandBehavior.Sample.Views;assembly=XFEventToCommandBehavior.Sample"
             xmlns:b="clr-namespace:XFEventToCommandBehavior;assembly=XFEventToCommandBehavior"
             x:Class="XFEventToCommandBehavior.Sample.Views.MainPage">

  <ContentPage.BindingContext>
    <vm:MainPageViewModel />
  </ContentPage.BindingContext>

  <ContentPage.Resources>
    <ResourceDictionary>
      <vw:NavigationConverter x:Key="NavConverter" />
    </ResourceDictionary>
  </ContentPage.Resources>

  <ContentPage.Behaviors>
    <b:EventToCommand EventName="Appearing" Command="{Binding Loaded}" />
  </ContentPage.Behaviors>

  <StackLayout>
    <Label Text="{Binding Message.Value}" FontSize="24" HorizontalOptions="Center" />
    <WebView Source="{Binding Url.Value}" VerticalOptions="FillAndExpand" HorizontalOptions="Fill" >
      <WebView.Behaviors>
        <b:EventToCommand EventName="Navigating" Command="{Binding Navigating}" Converter="{StaticResource NavConverter}" />
        <b:EventToCommand EventName="Navigated" Command="{Binding Navigated}" Converter="{StaticResource NavConverter}" />
      </WebView.Behaviors>
    </WebView>
  </StackLayout>

</ContentPage>
```

## 単純なコマンド呼び出し
単純にイベント発生時にコマンドを呼び出している箇所が以下の部分です。

```xml
<ContentPage.Behaviors>
  <b:EventToCommand EventName="Appearing" Command="{Binding Loaded}" />
</ContentPage.Behaviors>
```

対象 VisualElement（この場合は ContentPage）の Behaviors 内に、作成した EventToCommand を定義し、イベント名とコマンドを指定します。
上記の場合は ContentPage の Appearing イベント発生時にバインドしたコマンドが呼び出されます。

## コンバータを介したコマンド呼び出し
コンバータを指定してイベント引数を変換してからコマンドを呼び出している箇所が以下の部分です。

```xml
<WebView Source="{Binding Url.Value}" VerticalOptions="FillAndExpand" HorizontalOptions="Fill" >
  <WebView.Behaviors>
    <b:EventToCommand EventName="Navigating" Command="{Binding Navigating}" Converter="{StaticResource NavConverter}" />
    <b:EventToCommand EventName="Navigated" Command="{Binding Navigated}" Converter="{StaticResource NavConverter}" />
  </WebView.Behaviors>
</WebView>
```

上記の場合は WebView の Navigating および Navigated イベント発生時にコンバータを使ってイベント引数を変換してからバインドしたコマンドが呼び出されます（NavConverter はイベント引数から Url を取り出して文字列として返すコンバータ）。

※
なお、Android で動作させた場合、通常のページ移動では Navigating イベントが発生しませんので注意してください（Windows 系は発生する。iOS は未実施なため不明）。
ただし、移動先ページでリダイレクトされた場合は Android でも Navigating イベントが発生します。

# まとめ
作成した Behavior および Behavior を使用するサンプルプロジェクトを GitHub にあげてあります。
https://github.com/norimakiXLVI/XFEventToCommandBehavior

今回作成したコードの確認は Xamarin.Forms v2.2.0.31 で行いました。

## 注意事項
GitHub にあげた Behavior のプロジェクト（XFEventToCommandBehavior）には Library というクラスがあります。

このクラスは、ライブラリを xaml のみで使用していると、Page 表示時に FileNotFoundException が発生する問題を回避するためのものです（cs 側でも使用する事で問題が発生しなくなります）。

サンプルプロジェクトでは App クラスのコンストラクタで呼び出しています。

```csharp
public App()
{
	MainPage = new Views.MainPage();
	XFEventToCommandBehavior.Library.Init();
}
```

※
サンプルプロジェクトは Behavior をライブラリとして参照しているわけではなく、プロジェクトを参照しているので実際にはこの呼び出しは不要です。ライブラリとして参照する場合の例として記載してあります。