Friday, 1 November 2019

Bind from a ControlTemplate to a ViewModel with Xamarin.Forms

The best new feature in Xamarin.Forms 4.3 is relative bindings. Relative bindings provide the ability to set the binding source relative to the position of the binding target, and are created with the RelativeSource markup extension, and set as the Source property of a binding expression. For more informatoin about relative bindings, see Xamarin.Forms Relative Bindings.

Relative bindings support a number of modes, including binding to self, binding to an ancestor, and binding from within a ControlTemplate to the templated parent (the runtime object instance to which the template is applied). They also support binding to a view model from within a ControlTemplate, even when the ControlTemplate binds to the templated parent. This makes it possible to support scenarios such as a ControlTemplate containing a Button that binds to a view model ICommand, while other controls in the ControlTemplate bind to the templated parent. This blog post will look at doing this.

The sample this code comes from can be found on GitHub.

Implementation

To demonstrate this scenario, I have a PeopleViewModel class that defines an ObservableCollection named People, and an ICommand named DeletePersonCommand:

    public class PeopleViewModel
    {
        public ObservableCollection People { get; set; }

        public ICommand DeletePersonCommand { get; private set; }

        public PeopleViewModel()
        {
            DeletePersonCommand = new Command((name) =>
            {
                People.Remove(People.FirstOrDefault(p => p.Name.Equals(name)));
            });

            People = new ObservableCollection
            {
                new Person
                {
                    Name = "John Doe",
                    Description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
                },
                new Person
                {
                    Name = "Jane Doe",
                    Description = "Phasellus eu convallis mi. In tempus augue eu dignissim fermentum..."
                },
                new Person
                {
                    Name = "Xamarin Monkey",
                    Description = "Aliquam sagittis, odio lacinia fermentum dictum, mi erat scelerisque..."
                }
            };
        }
    }

There’s also a ContentPage whose BindingContext is set to a PeopleViewModel instance. The ContentPage contains a StackLayout which uses a bindable layout to bind to the People collection:

<ContentPage ...>
    <ContentPage.BindingContext>
        <local:PeopleViewModel />
    </ContentPage.BindingContext>

    <StackLayout Margin="10,35,10,10"
                 BindableLayout.ItemsSource="{Binding People}"
                 BindableLayout.ItemTemplate="{StaticResource PersonTemplate}" />

</ContentPage>

The ItemTemplate of the bindable layout is set to the PersonTemplate resource:

        <DataTemplate x:Key="PersonTemplate">
            <local:CardView BorderColor="DarkGray"
                            CardName="{Binding Name}"
                            CardDescription="{Binding Description}"
                            ControlTemplate="{StaticResource CardViewControlTemplate}" />
        </DataTemplate>

This DataTemplate specifies that each item in the People collection will be displayed using a CardView object that simply defines CardName, CardDescription, BorderColor, and CardColor bindable properties. The appearance of each CardView object is defined using a ControlTemplate named CardViewControlTemplate:

        <ControlTemplate x:Key="CardViewControlTemplate">
            <Frame BindingContext="{Binding Source={RelativeSource TemplatedParent}}"
                   BackgroundColor="{Binding CardColor}"
                   BorderColor="{Binding BorderColor}"
                   CornerRadius="5"
                   HasShadow="True"
                   Padding="8"
                   HorizontalOptions="Center"
                   VerticalOptions="Center">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="75" />
                        <RowDefinition Height="4" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>
                    <Label Text="{Binding CardName}"
                           FontAttributes="Bold"
                           FontSize="Large"
                           VerticalTextAlignment="Center"
                           HorizontalTextAlignment="Start" />
                    <BoxView Grid.Row="1"
                             BackgroundColor="{Binding BorderColor}"
                             HeightRequest="2"
                             HorizontalOptions="Fill" />
                    <Label Grid.Row="2"
                           Text="{Binding CardDescription}"
                           VerticalTextAlignment="Start"
                           VerticalOptions="Fill"
                           HorizontalOptions="Fill" />
                    <Button Text="Delete"
                            Command="{Binding Source={RelativeSource AncestorType={x:Type local:PeopleViewModel}}, 
                                              Path=DeletePersonCommand}"
                            CommandParameter="{Binding CardName}"
                            HorizontalOptions="End" />
                </Grid>
            </Frame>
        </ControlTemplate>

The root element of the CardViewControlTemplate is a Frame object. whose BindingContext is set to its templated parent (the CardView). Therefore, the Frame object, and all of its children, will resolve their bindings against CardView properties.

However, the Button within the CardViewControlTemplate binds to both its templated parent (the CardView), and to the ICommand in the PeopleViewModel instance. How is this possible? It’s possible because the Button.Command property redefines its binding source to be the binding context of an ancestor whose binding context type is PeopleViewModel. Let’s delve into this a little more.

The RelativeSource markup extension has a Mode property that can be set to one of the values of the RelativeBindingSourceMode enumeration: Self, FindAncestor, FindAncestorBindingContext, and TemplatedParent. The Mode property is the ContentProperty of the RelativeSourceExtension class, and so explicitly setting it using Mode= can be eliminated. In addition, the RelativeSource markup extension has a AncestorType property. Setting the AncestorType property to a type that derives from Element (any Xamarin.Forms control, or ContentView) will set the Mode property to FindAncestor. Similarly, setting the AncestorType property to a type that doesn’t derive from Element will set the Mode property to FindAncestorBindingContext.

Therefore, the relative binding expression Command=”{Binding Source={RelativeSource AncestorType={x:Type local:PeopleViewModel}}, Path=DeletePersonCommand}” sets the Mode property to FindAncestorBindingContext, because the type specified in the AncestorType property doesn’t derive from Element. The Source property is set the BindingContext property of the ancestor whose binding context is of type PeopleViewModel, which in this case is the StackLayout. The Path part of the expression can then resolve the DeletePersonCommand property. However, the Button.CommandParameter property doesn’t alter its binding source, instead inheriting it from its parent in the ControlTemplate. Therefore, this property binds to the CardName property of the CardView. The overall effect of the Button bindings is that when the Button is clicked, the DeletePersonCommand in the PeopleViewModel class is executed, with the value of the CardName property being passed to the ICommand.

Summary

The overall effect of this code is that StackLayout uses a bindable layout to display a collection of CardView objects:

The appearance of each CardView object is defined by a ControlTemplate, whose controls bind to properties on its templated parent (the CardView). However, the Button in the ControlTemplate redefines its binding source to be an ICommand in a view model. When clicked, the Button removes the specified CardView from the bindable layout:

The sample this code comes from can be found on GitHub.

No comments:

Post a Comment