This post was originally published in 2015 and may contain outdated information.
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.