かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Xamarin.FormsでBehaviorのTriggerとActionを作る

Xamarin.Formsのドキュメント上は見つけれなかったけど、ソースコード的にはTriggerとActionがあったりします。

github.com

ただ、この人たちはBindingに対応してないという、ちょっと悲しい感じに仕上がってます。なので、XamarinのBehaviorをベースにBindingに対応したTriggerとActionを作ってみようと思います。

Behaviorの基本クラス

BindingContextを伝搬するBehaviorBase<T>クラスを作ります。

using System;
using Xamarin.Forms;

namespace PrismUnityApp16.Behaviors
{
    public class BehaviorBase<T> : Behavior<T>
        where T : BindableObject
    {
        protected T AssociatedObject { get; private set; }

        protected override void OnAttachedTo(T bindable)
        {
            base.OnAttachedTo(bindable);
            this.AssociatedObject = bindable;
            this.BindingContext = bindable.BindingContext;

            bindable.BindingContextChanged += this.Bindable_BindingContextChanged;
        }

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

        protected override void OnDetachingFrom(T bindable)
        {
            base.OnDetachingFrom(bindable);
            bindable.BindingContextChanged -= this.Bindable_BindingContextChanged;
        }

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

Triggerの基本クラスを作る

次にTriggerの基本クラスを作ります。BehaviorBase<T>を継承してIActionインターフェースを抱え込む感じで作ります。ContentPropertyActionsを指定していい感じにXAMLで書けるようにもしておきましょう。

あと、IActionBindingContextを伝搬させるのも忘れないでやっておきます。

using System.Collections.Generic;
using System.Linq;
using Xamarin.Forms;

namespace PrismUnityApp16.Behaviors
{
    [ContentProperty("Actions")]
    public class TriggerBehaviorBase<T> : BehaviorBase<T>
        where T : BindableObject
    {
        public ICollection<IAction> Actions { get; } = new List<IAction>();

        protected void InvokeActions(object parameter)
        {
            foreach (var action in this.Actions.ToArray())
            {
                action.Execute(parameter);
            }
        }

        protected override void OnAttachedTo(T bindable)
        {
            base.OnAttachedTo(bindable);
            foreach (var action in this.Actions.ToArray())
            {
                action.BindingContext = this.BindingContext;
            }
        }

        protected override void OnDetachingFrom(T bindable)
        {
            base.OnDetachingFrom(bindable);
            foreach (var action in this.Actions.ToArray())
            {
                action.BindingContext = null;
            }
        }

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

            foreach (var action in this.Actions.ToArray())
            {
                action.BindingContext = this.BindingContext;
            }
        }
    }
}

IActoinはこんな感じのシンプルなインターフェースです。

namespace PrismUnityApp16.Behaviors
{
    public interface IAction
    {
        object BindingContext { get; set; }

        void Execute(object parameter);
    }
}

使ってみよう

イベントをもとにActionを実行するEventTriggerBehaviorとCommandを実行するInvokeCommandActionを作ってみようと思います。

EventTriggerBehavior

さくっとリフレクションを使ってイベントを拾ってきて登録します。

using System;
using System.Reflection;
using Xamarin.Forms;

namespace PrismUnityApp16.Behaviors
{
    public class EventTriggerBehavior : TriggerBehaviorBase<View>
    {
        public static readonly BindableProperty EventNameProperty = BindableProperty
            .Create(nameof(EventName), typeof(string), typeof(EventTriggerBehavior));

        private Delegate EventHandler { get; set; }

        private EventInfo EventInfo { get; set; }

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

        protected override void OnAttachedTo(View bindable)
        {
            base.OnAttachedTo(bindable);
            if (string.IsNullOrEmpty(this.EventName))
            {
                return;
            }

            this.EventInfo = this.AssociatedObject.GetType().GetRuntimeEvent(this.EventName);
            if (this.EventInfo == null)
            {
                throw new InvalidOperationException($"{this.EventName} is not found.");
            }

            var methodInfo = typeof(EventTriggerBehavior).GetTypeInfo().GetDeclaredMethod(nameof(OnEvent));
            this.EventHandler = methodInfo.CreateDelegate(this.EventInfo.EventHandlerType, this);
            this.EventInfo.AddEventHandler(bindable, this.EventHandler);
        }

        private void OnEvent(object sender, object args)
        {
            this.InvokeActions(args);
        }

        protected override void OnDetachingFrom(View bindable)
        {
            base.OnDetachingFrom(bindable);
            this.EventInfo.RemoveEventHandler(bindable, this.EventHandler);
        }
    }
}

InvokeCommandAction

BindableObjectから継承してIActionを実装します。Commandを実行する感じに書きましょう。

using System.Windows.Input;
using Xamarin.Forms;

namespace PrismUnityApp16.Behaviors
{
    public class InvokeCommandAction : BindableObject, IAction
    {
        public static readonly BindableProperty CommandProperty = BindableProperty
            .Create(nameof(Command), typeof(ICommand), typeof(EventTriggerBehavior));
        public static readonly BindableProperty CommandParameterProperty = BindableProperty
            .Create(nameof(CommandParameter), typeof(object), typeof(EventTriggerBehavior));
        public static readonly BindableProperty ConverterProperty = BindableProperty
            .Create(nameof(Converter), typeof(IValueConverter), typeof(EventTriggerBehavior));

        public ICommand Command
        {
            get { return (ICommand)this.GetValue(CommandProperty); }
            set { this.SetValue(CommandProperty, value); }
        }
        public object CommandParameter
        {
            get { return this.GetValue(CommandParameterProperty); }
            set { this.SetValue(CommandParameterProperty, value); }
        }
        public IValueConverter Converter
        {
            get { return (IValueConverter)this.GetValue(ConverterProperty); }
            set { this.SetValue(ConverterProperty, value); }
        }


        public void Execute(object parameter)
        {
            var p = this.CommandParameter;

            if (p == null)
            {
                p = this.Converter?.Convert(parameter, typeof(object), null, null);
            }

            if (this.Command?.CanExecute(p) ?? false)
            {
                this.Command.Execute(p);
            }
        }
    }
}

使ってみよう

使い方は簡単。例えばButtonClickedイベントと紐づける場合はこんな感じ。

<?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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             xmlns:controls="clr-namespace:PrismUnityApp16.Controls"
             xmlns:behaviors="clr-namespace:PrismUnityApp16.Behaviors"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="PrismUnityApp16.Views.MainPage"
             Title="MainPage">
  <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
    <Button Text="OK">
      <Button.Behaviors>
        <behaviors:EventTriggerBehavior EventName="Clicked">
          <behaviors:InvokeCommandAction Command="{Binding HelloCommand}" />
        </behaviors:EventTriggerBehavior>
      </Button.Behaviors>
    </Button>
  </StackLayout>
</ContentPage>

ViewModel側はこんな感じです。

using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using System.Diagnostics;

namespace PrismUnityApp16.ViewModels
{
    public class MainPageViewModel : BindableBase, INavigationAware
    {
        public DelegateCommand HelloCommand { get; }

        public MainPageViewModel()
        {
            this.HelloCommand = new DelegateCommand(() => Debug.WriteLine("Clicked"));
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
        }
    }
}

実行してボタンを押すとデバッグウィンドウの出力にClickedと表示されます。

まとめ

デフォで用意しといてくれ。