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
から派生します。しかし、 TriggerAction
は BindableObject
から派生していないため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メソッドが呼ばれ、メッセージが変わりました!
リフレクションを使っていても動的コード生成は行っていないので実機iOSでも使えます。(画像は実機のスクリーンショットです)