XAML Attached Properties Tricks in Xamarin.Forms

I’m sure you know of behaviours in Xamarin.Forms but have you heard of Attached Properties?

Have you ever wondered how you define properties for a Grid on a Label, eg <Label Grid.Row=”5″, and the Grid just seems to know about it. This is attaching a piece of data onto the Label so that the Grid can reference the data.

Let’s take a look at how it works in the Grids case. In our Xaml we’d normally have something like the code above, so somehow we tell the Grid that we want to be on Row number  5. In order for this to work the Grid needs to have a static ‘attached’ bindable property. Let’s take a look at what the Grid actually has.

public static readonly BindableProperty RowProperty = BindableProperty.CreateAttached ("Row", typeof(int), ...);

So yep the Grid has the attached property for Row, awesome, now how does the Grid get that data?

In order for the Grid to be able to access that data it needs a static method which will extract it as follows.

public static int GetRow (BindableObject bindable)
{
    return (int)bindable.GetValue (Grid.RowProperty);
}

We can also see that the Grid calls this method to get the data from the child element.

int row2 = Grid.GetRow (child);

So that’s some pretty cool stuff.

Now for the Trick

Considering this is a bindable property we can actually attach anything to this static property, our own object or even a command. We can also get notifications when the property changes, during that notification we can obtain references to both the Element and the bindable property. This means we can link up custom behaviours without even using behaviours.

Say for instance we wanted to attached a TapGesture for a View to a Command? Normally this is fairly verbose to add in Xaml but setup correctly it can become just a property on the View.

local:TappedGestureAttached.Command="{Binding OpenNewPage}"

So what’s TappedGestureAttached? Let’s take a look.

Below we have a Static property called CommandProperty, that property name is Command, has a return type of ICommand and a declaring type of View. You can also see it’s linked to  the OnItemTappedChanged command, which means when the property changes that event gets called.

public class TappedGestureAttached
{
    public static readonly BindableProperty CommandProperty =
        BindableProperty.CreateAttached (
            propertyName: "Command",
            returnType: typeof(ICommand),
            declaringType: typeof(View),
            defaultValue: null,
            defaultBindingMode: BindingMode.OneWay,
            validateValue: null,
            propertyChanged: OnItemTappedChanged);

Below we have the OnItemTappedChanged command, as I mentioned before we have access to both the View and the Command hence we can wire up the TapGestureRecognizer.

public static void OnItemTappedChanged(BindableObject bindable, object oldValue, object newValue)
{
    var control = bindable as View;

    if (control != null) {
        control.GestureRecognizers.Clear ();
        control.GestureRecognizers.Add (
            new TapGestureRecognizer() {
                Command = new Command((o) => {

                    var command = GetItemTapped (control);

                    if (command != null && command.CanExecute (null))
                        command.Execute (null);
                })
            }
        );
    }
}

There we have it wiring up a TapGestureRecognizer in less than a line.

Here’s the full code.

public class TappedGestureAttached
{
    public static readonly BindableProperty CommandProperty =
        BindableProperty.CreateAttached (
            propertyName: "Command",
            returnType: typeof(ICommand),
            declaringType: typeof(View),
            defaultValue: null,
            defaultBindingMode: BindingMode.OneWay,
            validateValue: null,
            propertyChanged: OnItemTappedChanged);


    public static ICommand GetItemTapped(BindableObject bindable)
    {
        return (ICommand)bindable.GetValue (CommandProperty);
    }

    public static void SetItemTapped(BindableObject bindable, ICommand value)
    {
        bindable.SetValue (CommandProperty, value);
    }

    public static void OnItemTappedChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var control = bindable as View;

        if (control != null) {
            control.GestureRecognizers.Clear ();
            control.GestureRecognizers.Add (
                new TapGestureRecognizer() {
                    Command = new Command((o) => {

                        var command = GetItemTapped (control);

                        if (command != null && command.CanExecute (null))
                            command.Execute (null);
                    })
                }
            );
        }
    }
}

We can also use this to hook up events that are directly on the control without using the code behind of the Xaml. Below we link up the ListView.ItemTapped event to a Command, which will be inside our ViewModel rather than codebehind.

public class ItemTappedAttached
{
    public static readonly BindableProperty CommandProperty =
        BindableProperty.CreateAttached (
            propertyName: "Command",
            returnType: typeof(ICommand),
            declaringType: typeof(ListView),
            defaultValue: null,
            defaultBindingMode: BindingMode.OneWay,
            validateValue: null,
            propertyChanged: OnItemTappedChanged);


    public static ICommand GetItemTapped(BindableObject bindable)
    {
        return (ICommand)bindable.GetValue (CommandProperty);
    }

    public static void SetItemTapped(BindableObject bindable, ICommand value)
    {
        bindable.SetValue (CommandProperty, value);
    }

    public static void OnItemTappedChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var control = bindable as ListView;
        if (control != null)
            control.ItemTapped += OnItemTapped;
    }

    private static void OnItemTapped(object sender, ItemTappedEventArgs e)
    {
        var control = sender as ListView;
        var command = GetItemTapped (control);

        if (command != null && command.CanExecute (e.Item))
            command.Execute (e.Item);
    }
}

 

Leave a Reply