Featured image of post EventToCommand in Xamarin Forms Apps

EventToCommand in Xamarin Forms Apps

This EventToCommand Behavior is a Xamarin Forms 1.x and 2.x Behavior that allows you to bind any Control Event to a Command, and it can be very useful.

This component is a part of Pillar, a lightweight MVVM framework that I made for Xamarin.Forms 1.x and 2.x. Please check it out on NuGet or GitHub.

An EventToCommand behavior can be used to bind any event on a visual element to an ICommand. Typically, this element is used in XAML to connect the attached element to a command located in a ViewModel.

When I started playing with Xamarin Forms, I found myself in a situation where I had to bind the ItemTapped event of the ListView to a command. I managed to do so by using the Behaviors from the Cavalli Corrado’s nuget package.

But since Xamarin Forms officially supports Behavior in version 1.3, I wanted to write my own.

EventToCommand Behavior usage

Here is an example of how I can bind the ItemTapped event of the ListView to a Command which takes as parameter the BindingContext of the tapped item, using my EventToCommand.

The ListView ViewModel has a “SayHelloCommand”:

public class HomeViewModel : ViewModelBase
{
    public ObservableCollection<PersonViewModel> People { get; set; }

    public RelayCommand<PersonViewModel> SayHelloCommand { get; set; }

    public HomeViewModel()
    {
        People = new ObservableCollection<PersonViewModel>
        {
            new PersonViewModel("John"),
            new PersonViewModel("Mike"),
            new PersonViewModel("Jane")
        };

        SayHelloCommand = new RelayCommand<PersonViewModel>(SayHello);
    }

    public void SayHello(PersonViewModel person)
    {
        Debug.WriteLine("Hello {0}!", person.Name);
    }
}

I need a converter to extract the tapped BindingContext from the ItemTappedEventArgs:

public class ItemTappedEventArgsConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var eventArgs = value as ItemTappedEventArgs;
        if (eventArgs == null)
            throw new ArgumentException("Expected TappedEventArgs as value", "value");

        return eventArgs.Item;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

And finally, here is the view:

<?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:b="clr-namespace:HelloEventToCommand.Behaviors;assembly=HelloEventToCommand" xmlns:c="clr-namespace:HelloEventToCommand.Converters;assembly=HelloEventToCommand" x:Class="HelloEventToCommand.Views.HomeView">
  <ContentPage.Resources>
    <ResourceDictionary>
      <c:ItemTappedEventArgsConverter x:Key="ItemTappedConverter" />
    </ResourceDictionary>
  </ContentPage.Resources>

  <ListView ItemsSource="{Binding People}">
    <ListView.Behaviors>
      <b:EventToCommandBehavior EventName="ItemTapped" Command="{Binding SayHelloCommand}" EventArgsConverter="{StaticResource ItemTappedConverter}" />
    </ListView.Behaviors>
    <ListView.ItemTemplate>
      <DataTemplate>
        <TextCell Text="{Binding Name}"/>
      </DataTemplate>
    </ListView.ItemTemplate>
  </ListView>
</ContentPage>

How it’s done

public class EventToCommandBehavior : BindableBehavior<View>
{
    public static readonly BindableProperty EventNameProperty = BindableProperty.Create<EventToCommandBehavior, string>(p => p.EventName, null);
    public static readonly BindableProperty CommandProperty = BindableProperty.Create<EventToCommandBehavior, ICommand>(p => p.Command, null);
    public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create<EventToCommandBehavior, object>(p => p.CommandParameter, null);
    public static readonly BindableProperty EventArgsConverterProperty = BindableProperty.Create<EventToCommandBehavior, IValueConverter>(p => p.EventArgsConverter, null);
    public static readonly BindableProperty EventArgsConverterParameterProperty = BindableProperty.Create<EventToCommandBehavior, object>(p => p.EventArgsConverterParameter, null);

    private Delegate _handler;
    private EventInfo _eventInfo;

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

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

    public object CommandParameter
    {
        get { return GetValue(CommandParameterProperty); }
        set { SetValue(CommandParameterProperty, value); }
    }

    public IValueConverter EventArgsConverter
    {
        get { return (IValueConverter)GetValue(EventArgsConverterProperty); }
        set { SetValue(EventArgsConverterProperty, value); }
    }

    public object EventArgsConverterParameter
    {
        get { return GetValue(EventArgsConverterParameterProperty); }
        set { SetValue(EventArgsConverterParameterProperty, value); }
    }

    protected override void OnAttachedTo(View visualElement)
    {
        base.OnAttachedTo(visualElement);

        var events = AssociatedObject.GetType().GetRuntimeEvents().ToArray();
        if (events.Any())
        {
            _eventInfo = events.FirstOrDefault(e => e.Name == EventName);
            if (_eventInfo == null)
                throw new ArgumentException(String.Format("EventToCommand: Can't find any event named '{0}' on attached type", EventName));

            AddEventHandler(_eventInfo, AssociatedObject, OnFired);
        }
    }

    protected override void OnDetachingFrom(View view)
    {
        if (_handler != null)
            _eventInfo.RemoveEventHandler(AssociatedObject, _handler);

        base.OnDetachingFrom(view);
    }

    private void AddEventHandler(EventInfo eventInfo, object item, Action<object, EventArgs> action)
    {
        var eventParameters = eventInfo.EventHandlerType
            .GetRuntimeMethods().First(m => m.Name == "Invoke")
            .GetParameters()
            .Select(p => Expression.Parameter(p.ParameterType))
            .ToArray();

        var actionInvoke = action.GetType()
            .GetRuntimeMethods().First(m => m.Name == "Invoke");

        _handler = Expression.Lambda(
            eventInfo.EventHandlerType,
            Expression.Call(Expression.Constant(action), actionInvoke, eventParameters[0], eventParameters[1]),
            eventParameters
        )
        .Compile();

        eventInfo.AddEventHandler(item, _handler);
    }

    private void OnFired(object sender, EventArgs eventArgs)
    {
        if (Command == null)
            return;

        var parameter = CommandParameter;

        if (eventArgs != null && eventArgs != EventArgs.Empty)
        {
            parameter = eventArgs;

            if (EventArgsConverter != null)
            {
                parameter = EventArgsConverter.Convert(eventArgs, typeof(object), EventArgsConverterParameter, CultureInfo.CurrentUICulture);
            }
        }

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

Where BindableBehavior is a BindingContext-aware behavior, made by Jonathan Yates:

public class BindableBehavior<T> : Behavior<T> where T : BindableObject
{
    public T AssociatedObject { get; private set; }

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

        AssociatedObject = visualElement;

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

        visualElement.BindingContextChanged += OnBindingContextChanged;
    }

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

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

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

As you can see, the EventToCommandBehavior can use an EventArgsConverter which is an IValueConverter. It is very useful in some cases, for example when you need to pass an argument taken from an EventArgs to the Command.