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.

  1. Create a new class that inherits from Behavior<T>:

    public class FlexLayoutItemTappedBehavior : Behavior<FlexLayout>
    
  2. Override OnAttachedTo and OnDetachingFrom:

    protected override void OnAttachedTo(FlexLayout bindable) { }
    protected override void OnDetachingFrom(FlexLayout bindable) { }
    
  3. Create a BindableProperty for the ICommand 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);
    }
    
  4. Add a method to execute each time the TapGestureRecognizer hits the Tapped 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 an async 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 the BindingContext of the item as a parameter to the command.

  5. Now we need to attach a TapGestureRecognizer to each item. The best way to do this is to hook into the FlexLayout.ChildAdded event. So, let's create an event handler to create and add the TapGestureReciognizer.

    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;
    
  6. That's almost it, but we have to detach our event handlers when our Behavior is detached. In the OnDetachingFrom method, we need to unregister the OnFlexLayoutChildAdded handler, as well as the OnItemTapped 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 for TappedGestureRecognizers, 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.

  7. We also need to make sure that the BindingContext of the parent element (the FlexLayout) is properly passed on to our Behavior. To do this, we set the BindingContext in the OnAttachedTo 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;
    
  8. 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:

  1. Make this work for any Layout. I don't think you'd have to change anything except for the type defined in the class.
  2. Use a bindable animation, so you could customize it for each view, using Xamanimation.
  3. Add a BindableProperty for CommandParameter.

Let me know what you think!

@jtaubensee