In my previous post, I talked about using BindableLayout
extensions with other Layout
types, including the FlexLayout
. The BindableLayout
is extremely useful when working with a collection of items; however, these layouts are missing touch events and commands.
In order to add this missing functionality, we can create a custom Behavior
. Behaviors
are great when you want to add functionality to an existing Xamarin.Forms
control, without subclassing. So if I wanted to do a validation on an Entry
or add touch events to a FlexLayout
, a Behavior
would be a great option.
In our example, we want to add a TapGestureRecognizer
to every item that we bound to the FlexLayout
. When you create a new Behavior
, you should implement two methods: OnAttachedTo
and OnDetachingFrom
.
Create a new class that inherits from
Behavior<T>
:public class FlexLayoutItemTappedBehavior : Behavior<FlexLayout>
Override
OnAttachedTo
andOnDetachingFrom
:protected override void OnAttachedTo(FlexLayout bindable) { } protected override void OnDetachingFrom(FlexLayout bindable) { }
Create a
BindableProperty
for theICommand
that we want to have executed each time an item is tapped. This will allow us to bind a command in our XAML.public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(FlexLayoutItemTappedBehavior), defaultBindingMode: BindingMode.OneWay); public ICommand Command { get => (ICommand)this.GetValue(CommandProperty); set => this.SetValue(CommandProperty, value); }
Add a method to execute each time the
TapGestureRecognizer
hits theTapped
event:private async void OnItemTapped(object sender, EventArgs e) { if (sender is VisualElement visualElement) { var animations = new List<AnimationBase>(); var scaleIn = new ScaleToAnimation { Target = visualElement, ScaleTo = .95, Duration = 50 }; animations.Add(scaleIn); var scaleOut = new ScaleToAnimation { Target = visualElement, ScaleTo = 1, Duration = 50 }; animations.Add(scaleOut); var storyBoard = new StoryBoard(animations); await storyBoard.Begin(); } if (sender is BindableObject bindable && this.Command != null && this.Command.CanExecute(null)) { this.Command.Execute(bindable.BindingContext); } }
In the above code, I am doing two things. First, I am executing an animation using a library called
Xamanimation
by Javier Suárez Ruiz. This is a great library which allows you to express animations in XAML. Since this behavior doesn’t have a XAML front end, we are writing the animation in code instead. Either way, the library is great, and you should check it out if you haven’t. In our case, the animation simply scales the element as if it were “pushed”, and then slides it back in to place. The animation is also the reason that I am using anasync void
here, which is not something you would usually use, except in certain cases (event handlers being one of them).Secondly, I am checking to see if the
BindableProperty
Command
that we created in step three is set and able to execute, then execute it. Additionally, we are passing theBindingContext
of the item as a parameter to the command.Now we need to attach a
TapGestureRecognizer
to each item. The best way to do this is to hook into theFlexLayout.ChildAdded
event. So, let’s create an event handler to create and add theTapGestureReciognizer
.private void OnFlexLayoutChildAdded(object sender, ElementEventArgs args) { if (args.Element is View view) { var tappedGestureRecognizer = new TapGestureRecognizer(); tappedGestureRecognizer.Tapped += TappedGestureRecognizer_Tapped; view.GestureRecognizers.Add(tappedGestureRecognizer); } }
Then, hook up the event to the handler in the
OnAttachedTo
method that we overrode in step one.bindable.ChildAdded += this.OnFlexLayoutChildAdded;
That’s almost it, but we have to detach our event handlers when our
Behavior
is detached. In theOnDetachingFrom
method, we need to unregister theOnFlexLayoutChildAdded
handler, as well as theOnItemTapped
handler. The first is easy and looks like this:bindable.ChildAdded -= this.OnFlexLayoutChildAdded;
The second is a little more difficult, but still not too bad. We can simply loop through the children in the
FlexLayout
, check forTappedGestureRecognizers
, and unregister them.foreach (var child in bindable.Children) { if (child is View childView && childView.GestureRecognizers.Any()) { var tappedGestureRecognizers = childView.GestureRecognizers.Where(x => x is TapGestureRecognizer).Cast<TapGestureRecognizer>(); foreach (var tapGestureRecognizer in tappedGestureRecognizers) { tapGestureRecognizer.Tapped -= this.OnItemTapped; childView.GestureRecognizers.Remove(tapGestureRecognizer); } } }
Keep in mind, however, that if you have
TappedGestureRecignizers
on these elements for some other reason, that the above code would remove them.We also need to make sure that the
BindingContext
of the parent element (theFlexLayout
) is properly passed on to ourBehavior
. To do this, we set theBindingContext
in theOnAttachedTo
method, and register an event handler for when the context changes (this probably doesnt happen often, but a good practice nonetheless).- Create a new event handler:
private void OnFlexLayoutBindingChanged(object sender, EventArgs e) { if (sender is FlexLayout flexLayout) { this.BindingContext = flexLayout.BindingContext; } }
- Register the event handler and set the inital context:
if (bindable.BindingContext != null) { this.BindingContext = bindable.BindingContext; } bindable.BindingContextChanged += this.OnFlexLayoutBindingChanged;
Lastly, in order to use this
Behavior
from XAML, you can reference it like this:<FlexLayout.Behaviors> <behaviors:FlexLayoutItemTappedBehavior Command="{Binding NavigateToDetailCommand}" /> </FlexLayout.Behaviors>
Ok, so that should be it. Here is a gist with the full implementation if you’d like to see it.
Some possible improvements I could see adding in the future:
- Make this work for any
Layout
. I don’t think you’d have to change anything except for the type defined in the class. - Use a bindable animation, so you could customize it for each view, using Xamanimation.
- Add a
BindableProperty
forCommandParameter
.
Let me know what you think!