A quick introduction to Compiled Bindings in .NET applications and why you should use them

A quick introduction to Compiled Bindings in .NET applications and why you should use them

Speed up your bindings and add compile-time validation to your XAML

ยท

9 min read

Introduction

If you have already used modern .NET-based UI frameworks like Xamarin.Forms and .NET MAUI before, you are likely familiar with a common concept called data binding. This concept is used to access data from a data source (such as properties inside a ViewModel) from within the user interface and react to changes that may occur during runtime. Classic data binding in .NET based applications comes with a couple of drawbacks, though.

Worst of all, they are slow and inefficient, because classic bindings are resolved during runtime using reflection, which means that the binding source and target are scanned for matching properties and commands. Secondly, because of this runtime resolution, binding expressions are not evaluated during compile-time. This means that any incorrect bindings will only be noticed during runtime and potentially cause a crash or at the least a somewhat malfunctioning application. Finding these types of bugs can turn out to be a time consuming and finicky task. Lastly, classic bindings require some additional work in order to get useful code-completion suggestions and error messages in the XAML editor window.

Enter compiled bindings. In order to speed up data binding and provide compile-time evaluation of binding expressions (and also allow Intellisense and other tools to provide useful suggestions during design-time), compiled bindings were introduced. According to Microsoft, compiled bindings are up to 20 times (!) faster than classic bindings**. You should definitely use them**, if you can**.**

Disclaimer: This focus of this article is on compiled bindings in Xamarin.Forms and .NET MAUI. However, compiled bindings also exist in Windows Presentation Foundation (WPF), but they work quite differently and won't be covered in this blog article.

For this blog post, I will use a .NET MAUI project (check out the sample repository), but these concepts largely apply also to Xamarin.Forms.

Example ViewModel

For the bindings in this article, I will use the following, simplified ViewModel (you can find the full code in the sample repository), which has a property called Items of type ObservableCollection<BindingItem> , an AddItemCommand to add new items and a RemoveItemCommand to remove either the last item or a specific item, depending on the CommandParameter that is passed in of type BindingItem:

public partial class BindingsViewModel : ObservableObject
{
    [ObservableProperty]
    private ObservableCollection<BindingItem> _items = new();

    private int _counter = 0;

    [RelayCommand]
    private void AddItem()
    {
        Items.Add(new BindingItem
        {
            Name = $"Item {_counter}",
            Count = _counter
        });

        _counter++;
    }

    [RelayCommand]
    private void RemoveItem(BindingItem item = null)
    {
        if (Items.Count == 0)
        {
            return;
        }

        if (item == null)
        {
            Items.Remove(Items.Last());
            return;
        }

        Items.Remove(item);
    }
}

Setting up Compiled Bindings

The central component for compiled bindings is the x:DataType attribute, which exists in both Xamarin.Forms and .NET MAUI. Basically, all we need to do is setting the BindingContext property and the x:DataType attribute in the View and we're good to go (almost at least - there are some common pitfalls that I'll explain further down).

Note: For compiled bindings to be usable, you need make sure that XAML compilation is enabled in your project, which is the default for .NET MAUI. For Xamarin.Forms it needs to be explicitly enabled, either globally or locally. More recent versions of Xamarin.Forms also have XAML compilation enabled by default when using the official XAML templates.

DataType and BindingContext

First, we need to set the x:DataType attribute of our ContentPage or ContentView on the root node in the XAML file to the underlying data type of the BindingContext of our View, which usually is a ViewModel:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
  x:Class="MauiSamples.Views.BindingsPage"
  xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
  xmlns:viewModels="clr-namespace:MauiSamples.ViewModels"
  x:DataType="viewModels:BindingsViewModel">
  <!-- ... -->
</ContentPage>

Here, I pass the name of the ViewModel including its namespace, so that it's fully qualified, because only fully-qualified names can be used in XAML compilation.

Once this is done, we still need to set the BindingContext for the View in the code-behind (the View's .xaml.cs file):

public partial class BindingsPage : ContentPage
{
    public BindingsPage(BindingsViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}

Done. This is all we need in order to set up compiled bindings. Pretty simple ๐Ÿ˜Š.

Technically, this could also be done entirely in XAML, but I'll stick to the more common way of setting the BindingContext in the code-behind, which is also what I recommend to you, because in real world applications, you would typically use dependency injection to pass a ViewModel instance into the View instead of instantiating it yourself in the View's XAML or code-behind.

Note: The x:DataType can be defined on any level in the XAML hierarchy and then only applies downward from that level in the XAML tree. However, it is recommended to set the x:DataType on the root level and then redefine it on deeper nested elements, if necessary (more on this further down).

Using Bindings

Now, with the x:DataType and the BindingContext set, we can go ahead and bind to the properties and commands of our ViewModel as usual:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
  x:Class="MauiSamples.Views.BindingsPage"
  xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
  xmlns:models="clr-namespace:MauiSamples.Models"
  xmlns:viewModels="clr-namespace:MauiSamples.ViewModels"
  x:DataType="viewModels:BindingsViewModel">

  <Grid
    RowDefinitions="*, auto"
    RowSpacing="8">

    <ListView
      Grid.Row="0"
      ItemsSource="{Binding Items}">
      <!-- ... -->
    </ListView>

    <HorizontalStackLayout
      Grid.Row="1">
      <Button
        Command="{Binding AddItemCommand}"
        Text="Add Item" />
      <Button
        Command="{Binding RemoveItemCommand}"
        Text="Remove Last" />
    </HorizontalStackLayout>

  </Grid>

</ContentPage>

Design-time Hints

Apart from the massive boost in binding performance, one of the main differences between compiled bindings and classic bindings is that Visual Studio (as well as plugins and extensions or any other modern IDE) now recognizes the properties and commands and can provide suggestions and errors for the bindings to us during design-time, which means that we can already see in the text editor when something isn't set correctly even before compiling and running the app:

Here, we can see that AddItem is inaccessible, because it's a private method and doesn't exist as a command. The correct name of the command to bind to is AddItemCommand (since we are using MVVM Source Generators here). If we would try to run the app like this, we would receive compiler errors, which wouldn't happen with classic bindings.

Note: I am using the JetBrains ReSharper extension for Visual Studio (no affiliation), which provides the Inlay Hints for the data context in XAML (notice the (BindingsViewModel).Path=). This is additionally useful, because I can directly see what my current BindingContext is.

Nested Data Contexts

There are various common scenarios, where the data context in the View hierarchy changes. On the root level of our View, we have a ViewModel set as the data context (meaning it's set as the BindingContext), but we may have other types that we are using in our ViewModel that are represented in our View hierarchy based on a DataTemplate.

The most common case is when we have a binding to a List or Collection of items of a specific type in our ViewModel, such as the ObservableCollection<BindingItem> in the BindingsViewModel. Using classic bindings, our View only knows at runtime, through reflection, what type the items in the List or Collection have.

With compiled bindings, we can already tell the View at compile-time, before the actual BindingContext is set (which only occurs during runtime), what type the BindingContext will have. Now, this BindingContext usually applies to the entire ContentPage or ContentView that we're dealing with.

This is problematic, because the data context of a DataTemplate usually is inferred from the type of the items that are contained inside of a List or Collection.

In the following situation, the DataTemplate will assume that the data context has not changed and will fail to bind to the Name property of the BindingItem element, because the x:DataType currently applies to the entire View:

 <ContentPage
  x:Class="MauiSamples.Views.BindingsPage"
  xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
  xmlns:models="clr-namespace:MauiSamples.Models"
  xmlns:viewModels="clr-namespace:MauiSamples.ViewModels"
  x:DataType="viewModels:BindingsViewModel">

  <Grid
    RowDefinitions="*, auto"
    RowSpacing="8">

    <ListView
      Grid.Row="0"
      ItemsSource="{Binding Items}">
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <Grid
              Padding="8"
              ColumnDefinitions="*,*">
              <Label
                Grid.Column="0"
                HorizontalOptions="Start"
                Text="{Binding Name}"
                VerticalTextAlignment="Center" />
            </Grid>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  <!-- ... -->
  </Grid>
</ContentPage>

When we try to build and run this, we'll receive the following compiler error:

Error XFC0045 Binding: Property "Name" not found on "MauiSamples.ViewModels.BindingsViewModel".

This is because the Name property doesn't exist in the BindingsViewModel, but belongs to the BindingItem model class instead.

In order to resolve this, we can redefine the x:DataType anywhere in the View hierarchy. For this, all we need to do is specify the BindingItem class as the x:DataType on the DataTemplate and then the bindings will work as expected:

<ContentPage
  x:Class="MauiSamples.Views.BindingsPage"
  xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
  xmlns:models="clr-namespace:MauiSamples.Models"
  xmlns:viewModels="clr-namespace:MauiSamples.ViewModels"
  x:DataType="viewModels:BindingsViewModel">

  <Grid
    RowDefinitions="*, auto"
    RowSpacing="8">

    <ListView
      Grid.Row="0"
      ItemsSource="{Binding Items}">
      <ListView.ItemTemplate>
        <!-- redefining the DataType here -->
        <DataTemplate x:DataType="models:BindingItem">
          <ViewCell>
            <Grid
              Padding="8"
              ColumnDefinitions="*,*">
              <Label
                Grid.Column="0"
                HorizontalOptions="Start"
                Text="{Binding Name}"
                VerticalTextAlignment="Center" />
            </Grid>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  <!-- ... -->
  </Grid>
</ContentPage>

When we run the app now, we'll see that our bindings work correctly and we can add and remove items using the provided buttons:

Neat, we can now use compiled bindings and redefine the x:DataType on any level of the View hierarchy based on the structure of our ViewModel ๐Ÿ˜ƒ. Which leads us to parent bindings, which are also affected by the x:DataType.

Relative Bindings

Instead of just removing the last item, it would be great if we could also remove any individual item in the ObservableCollection. For this, we need to add a button next to the label in the DataTemplate, bind to the RemoveItemCommand and pass the item itself as the CommandParameter:

But wait, the RemoveItemCommand cannot be found ๐Ÿ˜ฑ and the design-time inlay hint already shows us why: The data context is currently set to BindingItem, because we specified that as the data type for our DataTemplate earlier:

<DataTemplate x:DataType="models:BindingItem">

Note: This issue applies to classic bindings and compiled bindings alike. So does the solution for the issue. The difference is that we can now already identify the problem during design-time or compile-time instead of noticing the problem during runtime only.

This is where relative bindings come in. When trying to bind to a property or command of a parent data context, such as our BindingsViewModel, we need to ensure that the binding is set to the correct Binding Source, specified via the Source attribute of a binding expression. In our case, we need to specify a RelativeSource with an AncestorType which is set our BindingsViewModel:

<Button
  Grid.Column="1"
  Text="Remove"
  BackgroundColor="Red"
  Command="{Binding RemoveItemCommand, Source={RelativeSource AncestorType={x:Type viewModels:BindingsViewModel}}}"
  CommandParameter="{Binding .}" />

By doing this, we're telling the binding mechanism to look higher up in the View hierarchy for a data context that matches the type of the specified RelativeSource.

Now, the error is gone and we can build and run the app successfully and add and remove items as much as we like, even individual ones:

Awesome, our bindings work and we can now benefit from the performance boost as well as the compile-time (and also design-time) evaluation of binding expressions that compiled bindings provide ๐Ÿ†.

Conclusions and next steps

Compiled bindings are awesome and highly convenient. Especially the performance boost and the compile-time (as well as design-time) evaluation of binding expressions are gold.

You cannot only speed up your application using compiled bindings, but you can also reduce your debugging efforts and focus on developing quality features instead of hunting for bugs that turn out to be binding errors, which often are difficult to identify without IDE support.

Have you used compiled bindings before? Did you run into any issues with them? Let me know and I'll be happy to write another blog post to explore bindings even further.

If you enjoyed this blog post, then follow me on LinkedIn, subscribe to this blog and star the GitHub repository for this post so you don't miss out on any future posts.

ย