nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Development Technologies。

Xamarin.Formsでイベントに反応してCommandを実行するBehaviorを書いてみた

数日前にAyaseってIDのオータガーさんと、下のような会話をしました。

今日、休日出勤分の振り替えを同月内に消化しないといけなくてお休みいただいたんで、「イベントに反応してCommandを実行するBehavior」を書いてみました。
ん?会話でTriggerとか言ってんのに何でBehaviorなのかって?
Xamarin.FormsのActionがBindableじゃなかったからです。知らんかった!!

使い方のイメージ

以下にContentPageのAppearingイベントに反応して、ViewModelのAppearingCommandを実行する場合のXAMLの表記を記載します。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XFormsEventToCommand;assembly=XFormsEventToCommand"
             x:Class="XFormsEventToCommand.MainPage">
  <ContentPage.BindingContext>
    <local:MainPageViewModel/>
  </ContentPage.BindingContext>
  <ContentPage.Behaviors>
    <local:EventToCommandBehavior EventName="Appearing" Command="{Binding AppearingCommand}"/>
  </ContentPage.Behaviors>

対象のイベントを持つオブジェクトにBindableとして登録し、EventNameプロパティに監視対象のイベント名を設定し、Commandプロパティに実行するコマンドをバインドします。
なお、EventNameに関してはBindableではありません。
また、他にCommandParameterやConverterも指定できるようにしています。
使い方は、WPFのInvokeCommandActionなんかと同様なので、細かい説明は割愛します。

作り方

細かい説明の前に、Github側にコードを公開していますのでそちらを紹介します。

github.com

解説読むの怠いよって人は、上のリポジトリのXFormsEventToCommandソリューション内を見てみてください。

ちなみに。。。作った後に見つけちゃったんですけど。。。
Xamarin公式にこんな記事があったんですよねw

developer.xamarin.com

ま、まぁ、上の記事だけじゃ動くもできないし?気にしないで進めることにします。
ちなみに上を参考にして修正しました。

さて、今回は以下の二つのクラスを作成しました。

  1. BindableBehavior<T>
  2. BindableBehavior<T>を継承したEventToCommandBehavior

です。
EventToCommandBehaviorにはCommandをバインドするために、BindingProperty(WPFのDependencyPropertyのような、BindできるプロパティのXamarin版)を定義する必要があります。
BindingPropertyを利用したBehaviorは今回の物以外にも利用用途がありますし、BindingPropertyを利用するには少しお作法的なものがあるため、 BindableBehavior<T>を基底クラスとして分離しました。

BindableBehavior<T>

上でも述べましたが、BindingPropertyを利用するBehaviorの為の基底クラスです。
ソース全体は以下のリンク先で見ることができます。

XamarinSamples/BindableBehavior.cs at master · nuitsjp/XamarinSamples · GitHub

クラス宣言は以下の通りになります。

    public class BindableBehavior<T> : Behavior<T> where T : BindableObject
    {

Xamarin標準のBehaviorのGenerics版を継承して作成します。
この時、TをBindableObjectに制限します。
親クラスとなるBehavior自体が制限しているためです。

続いて、プロパティを一つ定義します。

    protected T AssociatedObject { get; private set; }

これは、Behaviorをアタッチしたオブジェクトのインスタンスを格納します。
Behaviorを作るのに、アタッチしているオブジェクトを利用することは多々ありますので、実装の利便性の為ここで保持しておきます。

続いてOnAttachedToをオーバーライドします。

        protected override void OnAttachedTo(T bindableObject)
        {
            base.OnAttachedTo(bindableObject);

            AssociatedObject = bindableObject;

            if (bindableObject.BindingContext != null)
                BindingContext = bindableObject.BindingContext;

            bindableObject.BindingContextChanged += OnBindingContextChanged;
        }

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

以下の3つの処理を実施しています。

  1. AssociatedObjectへアタッチ対象のオブジェクトの保持
  2. アタッチ対象のオブジェクトのBindingContextプロパティの値を、自分自身のBindingContextに設定
  3. アタッチ対象のオブジェクトのBindingContextChanged発生時に、OnBindingContextChangedを呼び出すように設定

特に重要なのが2番目です。
子クラスでBindingPropertyを利用するにあたり、バインド対象のオブジェクトを、アタッチ対象のBindingPropertyから取得してくるために必要な処理です。

3.でラムダ式ではなくメソッドを割り当てているのは、後ほどデタッチ処理の際に親のイベントから参照を削除する為です。

と言う分けで、デタッチ処理です。

        protected override void OnDetachingFrom(T bindableObject)
        {
            bindableObject.BindingContextChanged -= OnBindingContextChanged;
        }

特にいう事はありません。(´・ω・`)

最後にOnBindingContextChangedをオーバーライドします。

        protected override void OnBindingContextChanged()
        {
            base.OnBindingContextChanged();
            BindingContext = AssociatedObject.BindingContext;
        }

これは2つのケースから呼び出されます。

  1. 自分自身のBindingContextが変更された場合
  2. アタッチ先のオブジェクトのBindingContextが変更された場合

何れの場合でも、親のBindingContextの参照を自分自身のBindingContextに再設定しています。
子が変更された場合もこれでよいのかは、実のところちょっと悩んでいますw

BindableBehaviorは以上です。

EventToCommandBehavior

では主役の登場です。

クラスの宣言は以下の通りです。

    public class EventToCommandBehavior : BindableBehavior<VisualElement>

Xamarin公式のサンプルだと、VisualElementではなくてViewになっていますが、Pageにも設定したいですし(話の発端がPageのAppearingイベント云々ですし)、Behaviorsプロパティが定義されているのがVisualElementの為、VisualElementを指定しました。

続いてプロパティを定義します。

        public string EventName { get; set; }

        public static readonly BindableProperty CommandProperty = BindableProperty.Create<EventToCommandBehavior, ICommand>(p => p.Command, null);
        public ICommand Command
        {
            get { return (ICommand)GetValue(CommandProperty); }
            set { SetValue(CommandProperty, value); }
        }

        public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create<EventToCommandBehavior, object>(p => p.CommandParameter, null);
        public object CommandParameter
        {
            get { return GetValue(CommandParameterProperty); }
            set { SetValue(CommandParameterProperty, value); }
        }

        public static readonly BindableProperty ConverterProperty = BindableProperty.Create<EventToCommandBehavior, IValueConverter>(p => p.Converter, null);
        public IValueConverter Converter
        {
            get { return (IValueConverter)GetValue(ConverterProperty); }
            set { SetValue(ConverterProperty, value); }
        }

BindablePropertyの細かい説明はここでは省きますが、EventNameだけはBindablePropertyではなく、通常のプロパティにしています。
これは、実装がEventNameが初期化後に変更された場合に対応していないためです。
そもそもあまりEventNameを動的に設定したいケースがないんじゃないかって判断もありますが。。。BindablePropertyにしちゃってもよかったかもしれません。

それから以下の二つのフィールドを定義します。

        EventInfo eventInfo;
        Delegate eventHandler;

これは、バインド対象のイベントと、そのイベントへ設定するためのデリゲートインスタンスです。
アタッチ・デタッチする際に利用します。

続いてOnAttachedToをオーバーライドします。

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

            if (string.IsNullOrWhiteSpace(EventName))
            {
                return;
            }

            eventInfo = AssociatedObject.GetType().GetRuntimeEvent(EventName);
            if (eventInfo == null)
                throw new ArgumentException($"EventToCommandBehavior: Can't register the '{EventName}' event.");

            MethodInfo methodInfo = typeof(EventToCommandBehavior).GetTypeInfo().GetDeclaredMethod("OnEvent");
            eventHandler = methodInfo.CreateDelegate(eventInfo.EventHandlerType, this);
            eventInfo.AddEventHandler(AssociatedObject, eventHandler);
        }

指定されたイベントをアタッチ先のオブジェクトから取得し、そのイベントに、本クラス内のOnEventという名称のメソッドを呼び出すデリゲートを設定しています。

続いてOnDetachingFromのオーバーライドです。

        protected override void OnDetachingFrom(VisualElement bindable)
        {
            if (eventInfo != null && eventHandler != null)
                eventInfo.RemoveEventHandler(AssociatedObject, eventHandler);

            base.OnDetachingFrom(bindable);
        }

OnAttachedToで設定したデリゲートをイベントから削除しています。
じゃないとメモリがリークします。(タブン

最後に、イベント発生後に実際にCommandを実行するメソッドを定義します。

        void OnEvent(object sender, object eventArgs)
        {
            if (Command == null)
            {
                return;
            }

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

            if (Command.CanExecute(resolvedParameter))
            {
                Command.Execute(resolvedParameter);
            }
        }

この時、コマンドを実行するパラメータを以下の優先順位で決定しています。

  1. CommandParameterプロパティが設定されていればCommandProperty
  2. Converterが設定されていればEventArgをConverterで変換した値
  3. 上記のいずれでもない場合はイベントの引数

こうして決まった引数で、コマンドの実行可否を評価したうえでコマンドを実行します。

駆け足になりましたが以上です。
何かわからないことや突っ込みがあれば、気軽にコメントかTwitterへ連絡ください。  なお、マサカリは手加減して投げてね(ハート

それではまた!