In the preceding parts of the series we created the main visual elements of our application, like pages and content views. They are filled with smaller visual elements called views or controls. This all looks good, but the app doesn’t do anything. All the data it displays is hardcoded and never changes. This is not how applications work. What would an application be without data? It would be no good. So, it’s time we took care of it.
Data in an application can come from different sources. It can come from properties defined in the code-behind or from a visual component. If you enter something in an entry, it becomes data that may be consumed by other elements, like for instance be displayed by a label.
How do we handle data in a .NET MAUI application? Well, one way is to use events. For example you enter something in an entry like in the example above, its TextChanged
event fires and the Text
property of the label is set to a new value, which is handled in the code-behind. This approach works, but if there’s a lot of data, there would have to be lots of events, the code would quickly become lengthy. Fortunately, there’s a more efficient approach, data binding.
If we used data binding instead of an event, the Text
property of the label would be set automatically when you entered text in the entry. This is because in data binding properties of two objects are linked so that if one changes, the other changes too.
In this part of the series, we’ll be talking about data binding. It’s a complex topic, but first things first. Let’s start with some basic terminology.
In this part of the series I’ll be demonstrating stuff using simple code in the TestPage
. We’re going to implement data binding in the actual app when we discuss the MVVM pattern.
Table of Contents
Data Binding Terminology
It’ll be easier to talk about basic terminology if we have an example to work on. Actually, let’s implement the example we discussed above, with an entry and a label. Make sure your code in the TestPage.xaml
file looks like this:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<VerticalStackLayout>
<Entry
x:Name="entry"
FontSize="30" />
<Label
BindingContext="{x:Reference Name=entry}"
Text="{Binding Path=Text}"
FontSize="30" />
</VerticalStackLayout>
</ContentPage>
If you run the app (on Windows or Android, it doesn’t matter), you will see just an empty entry. The label is there too, but its Text
property is set to an empty string, so we don’t see anything. This is because it’s linked to the Text
property of the entry, which also is an empty string at this moment. But as you start typing in the entry, the exact same text will start appearing in the label:
So, here we have a working data binding. The Text
properties of the two views are linked. And now the terminology.
We have two objects here, an entry and a label. The entry is the source, the label is the target.
The source is the object on which data binding is set.
The target is the object referenced by the data binding.
The data flows from source to target, so the value of the target property is set to the value of the source property. This is how it works here, but sometimes data can flow in the opposite direction or in both directions, as we’re going to see.
Then we have the binding context, another term. The binding context is set on the target and references the source so that the target object knows where to pull the data from.
The target property must be a bindable property. This means the target object must inherit from BindableObject. Element
, VisualElement
, View
and View
derivatives inherit from BindableObject
. The Text
property of the Label
class is associated with the bindable property TextProperty
.
We also use two markup extensions here. The first one is x:Reference
. As its name suggests, it’s used to reference an object by its name. The second one is Binding
. Its Path
property is set to the property of the source object we want to bind to.
The Name
property of x:Reference
and the Path
property of Binding
are content properties, so they may be omitted if they’re the first properties (or the only ones like here). So, we could rewrite the code like this:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<Label
BindingContext="{x:Reference entry}"
Text="{Binding Text}"
...
We usually set the bindings in XAML, but, naturally, we could do it in C# as well. Let’s see how.
Data Binding in C# Code
Let’s rewrite the code above so that the binding is set in C# code. First, let’s remove the binding from the XAML file and add a name to the label so that we can reference it from the code-behind:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
<VerticalStackLayout>
<Entry
x:Name="entry"
FontSize="30" />
<Label
x:Name="label"
FontSize="30" />
</VerticalStackLayout>
</ContentPage>
And now let’s set the binding in the TestPage.xaml.cs
file:
namespace Slugrace.Views;
public partial class TestPage : ContentPage
{
public TestPage()
{
InitializeComponent();
label.BindingContext = entry;
label.SetBinding(Label.TextProperty, "Text");
}
}
So, we use the BindingContext
property to specify the source object and the SetBinding
method to specify the target and source properties that should be linked. The SetBinding
method is called on the target.
Now look how the source and target properties are specified. The target property is specified as a BindableProperty
object, Label.TextProperty
. The source property is specified as a string.
As I mentioned before, we’re going to set our bindings primarily in XAML, so remove the two lines of code where the binding was set from the code-behind and go back to the XAML file. The example we discussed above with the Text
properties of an entry and a label linked together is an example of view-to-view binding. Let’s have a look at another example of that type.
View-to-view Binding
Each control can have its BindingContext
set to only one source (although there is a way around it, as we’re going to learn), but you can bind as many targets to the source as you wish. To keep things simple, let’s bind two targets to a single source. Modify the TestPage.xaml
file to look like this:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<VerticalStackLayout>
<Slider x:Name="slider"
Maximum="200" />
<Label
x:Name="label"
FontSize="30"
BindingContext="{x:Reference slider}"
Text="{Binding Value}"/>
<BoxView
x:Name="boxView"
Color="Green"
BindingContext="{x:Reference slider}"
WidthRequest="{Binding Value}"
HeightRequest="{Binding Value}" />
</VerticalStackLayout>
</ContentPage>
Here the slider is the source and the label and the box view are the targets. So, both targets have the same binding context and they both bind to the same property of the source, Value
. If you run the app, you will see the slider and the label, but not the box view below:
The Text
property of the label is set to 0 and you can’t see the box view because its WidthRequest
and HeightRequest
properties are set to 0. Why? Because the Text
property of the label and the WidthRequest
and HeightRequest
properties of the box view are all bound to the Value
property of the slider and the default value of the slider is 0. Now try dragging the slider to the right. You should see the label text change and the box view grow:
You can also bind to different properties of the source object. Let’s bind the box view’s WidthRequest
property to the Maximum
property of the slider. As the Maximum
property doesn’t change over time, the width of the box view won’t change either. Here’s the code:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<BoxView
...
WidthRequest="{Binding Maximum}"
HeightRequest="{Binding Value}" />
...
If you now run the app, the label’s Text
property and the box view’s HeightRequest
property will change, but the WidthRequest
property of the latter will remain constant:
So, everything works as expected. But what if we don’t like how the label text is formatted? Well, we can easily fix it.
StringFormat
You can easily format strings by setting the StringFormat
property. This only works if the target property is a string, which is the case with the Text
property of Label
. Let’s display the text as a number with some additional info. If you want to add additional text, you must enclose it in single quotes. The actual data is placed in curly braces and you can format it any way you want. In our case we’ll format it to be a float number with two decimal places:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<Label
...
Text="{Binding Value,
StringFormat='Current slider value: {0:F2}'}" />
<BoxView
...
Now the label looks better:
Here’s another example. Let’s add another label that will display the current date:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<VerticalStackLayout>
...
<BoxView
...
<Label
BindingContext="{x:Static sys:DateTime.Now}"
FontSize="20"
Text="{Binding StringFormat='Today is {0:D}.'}" />
</VerticalStackLayout>
</ContentPage>
In this example we’re not binding to another view, but rather to the static DateTime.Now
property. As you know, we use the x:Static
markup extension to reference a static property. This property is defined in the System
namespace, in a different assembly, so, as we learned in the previous part of the series, we must add the appropriate namespace in the XAML code. Here we’re using the sys
prefix.
Now our TestPage
looks like this:
In the example above we used a binding context. But is it always necessary? Turns out, it isn’t.
The Source Property
Instead of defining a binding context, we can set the Source
property of Binding
. Let’s rewrite one of the bindings in the example above:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<Label
x:Name="label"
FontSize="30"
Text="{Binding Value,
Source={x:Reference slider},
StringFormat='Current slider value: {0:F2}'}" />
<BoxView
...
This is an alternative syntax. But there’s yet another alternative syntax, not so common, but still worth knowing.
Object-Element Syntax
We usually use the Binding
markup extension to set a binding, but using object elements is also possible. Let’s rewrite the binding on the first label again so that it uses this notation:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<Label
x:Name="label"
FontSize="30">
<Label.Text>
<Binding Path="Value"
Source="{x:Reference slider}"
StringFormat="Current slider value: {0:F2}" />
</Label.Text>
</Label>
<BoxView
...
Or even, we can express the x:Reference
markup extension as an object element:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<Label
x:Name="label"
FontSize="30">
<Label.Text>
<Binding Path="Value"
StringFormat="Current slider value: {0:F2}">
<Binding.Source>
<x:Reference Name="slider" />
</Binding.Source>
</Binding>
</Label.Text>
...
This syntax may be useful with complex objects. But in such a simple example as ours, it’s not necessary. Let’s modify the code so that the binding context is set again:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<Label
x:Name="label"
FontSize="30"
BindingContext="{x:Reference slider}"
Text="{Binding Value,
StringFormat='Current slider value: {0:F2}'}"/>
<BoxView
...
The first label and the box view use the same binding context. This means we have to set it twice, on each object individually. Or do we?
Binding Context Inheritance
The BindingContext
property value is inherited by the children of the object it’s set on. So, if the first label and the box view were children of, let’s say, a VerticalStackLayout
, we could set the BindingContext
property on the parent and it would be inherited by the children. Let’s check it out.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
<VerticalStackLayout>
<Slider ... />
<VerticalStackLayout
BindingContext="{x:Reference slider}">
<Label
x:Name="label"
...
<BoxView
x:Name="boxView"
...
</VerticalStackLayout>
<Label
...
The app still works as before.
Now, remember how I told you that each object can only have one BindingContext
and that there is a way around it? Well, let’s have a closer look at this next.
Binding Modes
A single view can have data binding on multiple properties. In our example the box view has two bindings: one set on the WidthRequest
property and another set on the HeightRequest
property. The former is bound to the value of the Maximum
property of the slider and the latter is bound to its Value
property. But still, both Maximum
and Value
are properties of the slider. But what if we wanted to bind the two properties of the box view to properties of two different objects? Well, we would need two binding contexts on the box view, which isn’t possible because a single view can only have one BindingContext
.
Let’s modify our code slightly to illustrate the problem. Here’s the code:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="30" />
</Style>
</ContentPage.Resources>
<VerticalStackLayout>
<Label Text="Width" />
<Slider x:Name="slider1"
Maximum="200" />
<Label Text="Height" />
<Slider x:Name="slider2"
Maximum="200" />
<Label Text="Rotation" />
<Slider x:Name="slider3"
Maximum="360" />
<BoxView x:Name="boxView"
BindingContext="{x:Reference slider1}"
Color="Green"
WidthRequest="{Binding Maximum}"
HeightRequest="{Binding Value}" />
</VerticalStackLayout>
</ContentPage>
Now we have three sliders with accompanying labels to inform us what each slider is meant for. We also have the box view. We now want to use the first slider to change the box view’s width, the second slider to change its height and the third one to rotate it. But we can’t set the BindingContext
property of the box view to all three sliders, we have to pick one. Here slider1
was picked to set the width, but what about the others? Run the app and try dragging the sliders. Only the first one does anything:
So, what can we do? Well, we can’t set more binding contexts, but we could use another feature of data binding, which is modes. Let’s first rewrite the code so that it works as intended and then let’s think about how it works:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<VerticalStackLayout>
<Label Text="Width" />
<Slider x:Name="slider1"
BindingContext="{x:Reference boxView}"
Maximum="200"
Value="{Binding WidthRequest, Mode=OneWayToSource}" />
<Label Text="Height" />
<Slider x:Name="slider2"
BindingContext="{x:Reference boxView}"
Maximum="200"
Value="{Binding HeightRequest, Mode=OneWayToSource}" />
<Label Text="Rotation" />
<Slider x:Name="slider3"
BindingContext="{x:Reference boxView}"
Maximum="360"
Value="{Binding Rotation, Mode=OneWayToSource}" />
<BoxView x:Name="boxView"
Color="Green" />
</VerticalStackLayout>
</ContentPage>
Looks as if everything had been reversed. Now the binding context is no longer set on the box view to reference a slider, but rather the box view is set as the binding context for each slider. The bindings are set on the Value
properties of the sliders. We also set the Mode
property to OneWayToSource
. This is one of a couple possible values (we’re going to discuss them in a while). As the name of this binding mode suggests, the Value
properties of the sliders set the appropriate values of the box view, so WidthRequest
, HeightRequest
and Rotation
respectively. In other words, values are transferred from the target (each particular slider) to the source (the box view). Now run the app and try it out:
So, what binding modes are there? The Mode
property can be set to a member of the BindingMode
enumeration. And the members are:
– Default
– OneWay
– OneWayToSource
– TwoWay
– OneTime
The OneWay
mode is the default mode of most bindable properties and it’s used to transfer values from source to target. The OneWayToSource
mode is used to transfer values in the opposite direction.
In the OneTime
mode values are transferred from source to target, but only if the BindingContext
changes. We’re not going to discuss this mode in more detail.
What about TwoWay
? Well, TwoWay
is used to transfer values in both directions between source and target. Let’s modify our code like this:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="30" />
</Style>
</ContentPage.Resources>
<VerticalStackLayout>
<Label BindingContext="{x:Reference slider}"
Text="{Binding Value, StringFormat='Width: {0:F2}'}" />
<Slider x:Name="slider"
BindingContext="{x:Reference boxView}"
Minimum="50"
Maximum="200"
Value="{Binding WidthRequest, Mode=TwoWay}" />
<BoxView x:Name="boxView"
Color="Green"
HeightRequest="100" />
</VerticalStackLayout>
</ContentPage>
Here we have a label, a slider and a box view. The label’s Text
property is bound to the slider’s Value
property. This is a OneWay
binding. The data flows from source (slider) to target (label).
The slider’s Value
property is bound to the box view’s WidthRequest
property. This is a TwoWay
binding, which we explicitly specified, although we didn’t have to because the default binding mode for the Slider
’s Value
property is TwoWay
. So, we could leave it out:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<Slider x:Name="slider"
...
Value="{Binding WidthRequest}" />
<BoxView ...
Let’s run the app:
As you can see, the value of the Slider’s Value property is 50, and the width of the box view is also 50. Drag the slider in both directions and watch the label and the box view change.
Some properties, like the Slider
’s Value
property, are TwoWay
by default. Some other properties like that are the Date
property of DatePicker
, the Text
property of Entry
and Editor
and some more. But it doesn’t mean we can’t set a different binding mode. We can override the binding mode. Let’s try it out by explicitly setting the binding mode of the slider’s Value
property to OneWay
:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<Slider x:Name="slider"
...
Value="{Binding WidthRequest, Mode=OneWay}" />
<BoxView ...
Run the app again. Now the box view occupies all available width because we didn’t set the WidthRequest
property on it and if you now drag the slider and thus change the Value
property, the WidthRequest
isn’t updated because the flow now is in one direction only:
Let’s change the binding mode back to TwoWay
. As this binding works in both directions, we could set it on the other object as well:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<Slider x:Name="slider"
Minimum="50"
Maximum="200" />
<BoxView x:Name="boxView"
BindingContext="{x:Reference slider}"
Color="Green"
HeightRequest="100"
WidthRequest="{Binding Value, Mode=TwoWay}" />
</VerticalStackLayout>
</ContentPage>
Here, however, we must specify the binding mode explicitly because the default mode for the WidthRequest
property of BoxView
is OneWay
. Now it works as before:
Great. But what if we wanted the data to be used in a different way? Let’s say, we want the box view to be visible only if the slider’s Value
is greater than 150? Or maybe we want the label to represent the Value
of the slider using integer size categories like 1 for very small values, 2 for a bit greater values and so on, moving to the next category every 50 units? To implement scenarios like these, we need value converters.
Value Converters
In the examples above the values that were transferred from source to target, from target to source or in both directions were either of the same type, like the Slider
’s Value
and the BoxView
’s WidthRequest
properties, which are both of type double
, or of different types, but with one type being able to be converted to the other type using an implicit conversion, like the Value
property of Slider
(double
) and the Text
property of Label
(string
). But sometimes an explicit type conversion is necessary.
You can convert any type into a string using the StringFormat
property, as we saw before. But what about other types? Well, for other type conversions we need value converters, also known as binding converters. These are classes that implement the IValueConverter
interface. Let’s see how to use them in practice.
So, suppose we want the label to represent the Value
of the slider using integer size categories like I mentioned before. The Value
property is of type double
and we need categories of type int
. What we need is a value converter that converts doubles to ints. We usually define also a method that does the conversion in the opposite direction. We wouldn’t have to do it if we were sure that the converter is only going to be used in OneWay
bindings, but you never know. You may want to reuse the converter in a different part of the app where a TwoWay
or OneWayToSource
conversion is required, so it’s a good idea to make the converters more universal. This is why we’re always going to define two methods in our conversions, one for the conversion in one direction and the other for the conversion in the opposite direction. These two methods are called Convert
and ConvertBack
respectively.
Anyway, let’s start with our example. We’re going to set the Text
property of the label. If the Value
of the slider is between 50 and 100, the width category should be 1. Category 2 will be for widths between 100 and 150, and category 3 for widths between 150 and 200. We could use just the StringFormat
property for that, but let’s also use a value converter.
We need to convert doubles to ints, so we’ll create a class named DoubleToIntConverter
. To keep things organized, let’s create a new folder in the root of our app named Converters
. In this folder we can now create our first converter, so add the aforementioned class to it and implement the two methods in it:
using System.Globalization;
namespace Slugrace.Converters
{
public class DoubleToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (int)((double)value / 50);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return (double)((int)value * 50);
}
}
}
Now we must instantiate the converter in the TestPage
’s resource dictionary and then reference it by key in the XAML code. We must also remember to add the converter’s namespace with the local
prefix:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Slugrace.Converters"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="30" />
</Style>
<local:DoubleToIntConverter x:Key="doubleToInt" />
</ContentPage.Resources>
<VerticalStackLayout>
<Label BindingContext="{x:Reference slider}"
Text="{Binding Value,
Converter={StaticResource doubleToInt},
StringFormat='Width Category: {0}'}" />
<Slider x:Name="slider"
...
As you can see, here we have both the converter and the StringFormat
property. In such cases, the converter is invoked first and the result is formatted as a string. If we now run the app and drag the slider, the width category will change every 50 units:
The Convert
method takes quite a few parameters. We only used one so far, value
, which is of type object
. This is the object or value from the data-binding source, so, in our case, from the slider’s Value
property. The method must return a value of the type of the data-binding target or a type that can be implicitly converted to the target’s type. Here it returns an int
, which is then converted using the StringFormat
property.
In the example the width category changes every 50 units. But what if we wanted to make the converter more universal and let the user decide what value should be used. We can do it using the parameter
parameter, which is of type object
, too. Let’s modify the Convert
method and then add another label and set the parameter to 50 on one label and 10 on the other. Here’s the Convert
method:
using System.Globalization;
namespace Slugrace.Converters
{
public class DoubleToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double granularity;
if (!double.TryParse(parameter as string, out granularity))
{
granularity = 1;
}
return (int)((double)value / granularity);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
double granularity;
if (!double.TryParse(parameter as string, out granularity))
{
granularity = 1;
}
return (double)((int)value * granularity);
}
}
}
And now let’s set the parameters:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
...
<VerticalStackLayout>
<Label BindingContext="{x:Reference slider}"
Text="{Binding Value,
Converter={StaticResource doubleToInt},
ConverterParameter=50,
StringFormat='Width Category (granularity 50): {0}'}" />
<Label BindingContext="{x:Reference slider}"
Text="{Binding Value,
Converter={StaticResource doubleToInt},
ConverterParameter=10,
StringFormat='Width Category (granularity 10): {0}'}" />
<Slider ...
Now the categories will be different for each label:
Next, let’s create another converter in the Converters
folder. This time we want to make the box view invisible if the Value
property of the slider is greater than 150. To make the box view invisible, we have to set its IsVisible
property to False
, which is a boolean value. So, we need a converter that converts doubles to bools:
using System.Globalization;
namespace Slugrace.Converters
{
class DoubleToBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!double.TryParse(parameter as string, out double limit))
{
limit = 100;
}
return (double)value < limit;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!double.TryParse(parameter as string, out double limit))
{
limit = 100;
}
return (bool)value ? limit : limit + 1;
}
}
}
Let’s use it:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
<ContentPage.Resources>
...
<local:DoubleToIntConverter x:Key="doubleToInt" />
<local:DoubleToBoolConverter x:Key="doubleToBool" />
</ContentPage.Resources>
<VerticalStackLayout>
...
<BoxView x:Name="boxView"
...
WidthRequest="{Binding Value, Mode=TwoWay}"
IsVisible="{Binding Value,
Converter={StaticResource doubleToBool},
ConverterParameter=150}" />
</VerticalStackLayout>
</ContentPage>
If you now run the app and drag the slider, the box view will disappear and reappear depending on the slider’s Value
.
In the examples so far we’ve been binding simple properties to other simple properties, like the Slider
’s Value
property and the Label
’s Text
property. You know that we use the Path
property to set a binding to a particular property. The Path
property is the content property of Binding
and may be omitted when it’s the first property. But we haven’t demonstrated more complex paths yet.
Binding Path
The Path
property may be set to a simple property, a subproperty or to a member of a collection. Let’s check it out:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...
Padding="50"
x:Name="page">
...
<BoxView x:Name="boxView"
...
<Label Text="{Binding Source={x:Reference page},
Path=Content.Children[0],
StringFormat='The first child is of type: {0}.'}" />
<Label Text="{Binding Source={x:Reference page},
Path=Content.Children[1].Text.Length,
StringFormat='The second label contains {0} characters.'}" />
<Label Text="{Binding Source={x:Reference page},
Path=Content.Children.Count,
StringFormat='This page contains {0} children.'}" />
</VerticalStackLayout>
</ContentPage>
First we specified the name of the ContentPage
so that we can reference it. Then we created three labels.
In the first label the Text
property is bound to the first child of Content
. Here Content
is the VerticalStackLayout
. It has 7 children: 5 labels, a slider and a box view. The first child is a label.
In the second label the Text
property is bound to the Length
property of Text
, which is a property of the second child of Content
.
In the third label the Text
property is bound to the Count
subproperty of Children
, which itself is a subproperty of Content
.
Here’s the result:
We can bind to properties of other objects that we specify by name. But we can also use relative bindings.
Relative Bindings
Relative bindings enable us to set the binding source relative to the position of the binding target. We use the RelativeSource
markup extension to create relative bindings. The content property of RelativeSource is Mode
. It can be set to a couple values, one of which is Self
. This value is used if we want to bind one property of an object to another property of the same object. Let’s simplify our TestPage
to look like so:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<VerticalStackLayout>
<BoxView x:Name="boxView"
Color="Green"
WidthRequest="120"
HeightRequest="{Binding Source={RelativeSource Self},
Path=WidthRequest}"
Rotation="{Binding Source={RelativeSource Self},
Path=WidthRequest}" />
</VerticalStackLayout>
</ContentPage>
Here the HeightRequest
and Rotation
properties of the box view are bound to its WidthRequest
property. If you run it, all three properties will have the same value:
Another value Mode
can be set to is FindAncestor
. It’s used to bind to a parent element. To use this mode, the AncestorType
property must be set to a type. If the type derives from Element
, Mode
will be implicitly set to FindAncestor
. Let’s again rewrite the code in the TestPage
:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="30" />
</Style>
</ContentPage.Resources>
<VerticalStackLayout Rotation="5">
<VerticalStackLayout Rotation="-15">
<Label Text="{Binding Source={RelativeSource AncestorType={x:Type VerticalStackLayout}},
Path=Rotation,
StringFormat='inner layout is rotated {0} degrees' }" />
<Label Text="{Binding Source={RelativeSource AncestorType={x:Type VerticalStackLayout},
AncestorLevel=2},
Path=Rotation,
StringFormat='outer layout is rotated {0} degrees' }" />
</VerticalStackLayout>
<Label Text="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}},
Path=Title}" />
</VerticalStackLayout>
</ContentPage>
Here we didn’t even give names to the elements because we’ll be binding to their properties by type, not by name. Let’s see the result first and then discuss it:
The first label’s Text
property is bound to an ancestor of type VerticalStackLayout
. This label has a direct parent of this type and an indirect one, which is its grandparent. By default it binds its Text
property to the direct parent, which is the VerticalStackLayout
with Rotation
set to -15.
If we want to bind to the outer VerticalStackLayout
, we must set AncestorLevel
to 2. This is what the second label does.
The third label is bound to an ancestor of type ContentPage
. There’s only one ancestor of this type.
So far, all the bindings have worked correctly, but what if a binding fails or returns a null value? Let’s have a look at such situations next.
Binding Fallbacks
Sometimes we try to bind to a property that doesn’t exist. How to handle this? Let’s rewrite our code in the TestPage
like so:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="30" />
</Style>
</ContentPage.Resources>
<VerticalStackLayout>
<Label x:Name="label1"
Text="A Label has the Text property." />
<Slider x:Name="slider" />
<Label BindingContext="{x:Reference label1}"
Text="{Binding Text}" />
<Label BindingContext="{x:Reference slider}"
Text="{Binding Text, FallbackValue='Text property not defined'}" />
</VerticalStackLayout>
</ContentPage>
Here we have three labels and a slider. The last label’s Text
property is bound to the slider’s Text
property, which… doesn’t exist. We can use the FallbackValue
property to specify what value should be used instead. If we didn’t specify this property, the last label’s Text
property would be an empty string.
If we now run the app, we’ll see the fallback value in the last label:
This example is pretty artificial, because we know that a Slider
doesn’t have the Text
property and we just don’t bind to it. A more realistic example would be binding to properties of heterogeneous objects in a collection where the given property may not exist on all objects.
There’s just one more topic for now as far as data binding is concerned that I’d like to discuss, multi-bindings.
Multi-bindings
Multi-bindings are the way to go if you want to attach multiple Binding
objects to one target property. Suppose we want to create a page with an entry, a stepper, a slider and a button (plus some labels with info about the main views) and the button should be enabled only if the entry isn’t empty, and the Value
properties of the stepper and the slider are greater than zero. These three conditions must be met simultaneously for the button to be enabled.
A multi-binding is created by instantiating the MultiBinding
class. The class has a Bindings
property that is a collection of the individual bindings. Bindings
is the class’s content property, so we don’t have to use it explicitly in XAML. We also must provide an IMultiValueConverter
instance that evaluates all the Binding
objects inside the Bindings
collection and returns a single value that can be used by the target property.
In our case we have a Bindings
collection of three Binding
objects. The target property is the IsEnabled
property of the button, which is of type boolean. The three source properties are:
– the entry’s Text
property’s Length
property (if it’s zero, the entry is empty),
– the stepper’s Value
property,
– the slider’s Value
property.
These three source properties are all of type double. So, we must create a converter that evaluates multiple double values and returns a boolean value. Let’s create an AllGreaterThanZeroMultiConverter
class in the Converters
folder. Its name is pretty self-explanatory. It’s supposed to return true
only if all source properties are set to a value greater than 0. Here’s the code:
using System.Globalization;
namespace Slugrace.Converters
{
class AllGreaterThanZeroMultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values == null || !targetType.IsAssignableFrom(typeof(bool)))
{
return false;
}
foreach (object value in values)
{
if (value is not (double or int))
{
return false;
}
else if (value is int i)
{
if (i <= 0)
{
return false;
}
}
else if (value is double d)
{
if (d <= 0)
{
return false;
}
}
}
return true;
}
object[] IMultiValueConverter.ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
The class implements the IMultiValueConverter
interface. We’re not going to implement the ConvertBack method, because we don’t need conversions in the opposite direction. Let’s have a closer look at the Convert
method, though.
In the first step we check whether the values
array, which in our case contains the three values from the source properties, is not null
. We also check if a boolean value can be assigned to the target type. If one or both of these conditions are met, the method returns false
. In our case, however, the values
array will contain three values and the target IsEnabled
property can be assigned a boolean value, so the next step can begin.
In the next step we check, in a loop, whether all values that we delivered are of type double or int, and if so, whether they’re less or equal to zero. If any of the values is not a double or int, or if any of the values is less than or equal to 0, the method returns false
. Otherwise, so if all values are positive doubles or ints, the method returns true
.
With the converter in place, let’s replace the code in TestPage.xaml
by the following:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:converters="clr-namespace:Slugrace.Converters"
x:Class="Slugrace.Views.TestPage"
Title="TestPage"
Padding="50">
<ContentPage.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="30" />
</Style>
<converters:AllGreaterThanZeroMultiConverter x:Key="AllGreaterThanZeroConverter" />
</ContentPage.Resources>
<VerticalStackLayout>
<Label Text="At least 1 character" />
<Entry x:Name="entry" WidthRequest="100" Margin="0, 0, 0, 20" />
<Label Text="A positive number" />
<HorizontalStackLayout>
<Stepper x:Name="stepper" Margin="0, 0, 0, 20" />
<Label Text="{Binding Source={x:Reference stepper}, Path=Value}" />
</HorizontalStackLayout>
<Label Text="A value greater than 0" />
<Slider x:Name="slider" Margin="0, 0, 0, 20" />
<Button Text="Proceed">
<Button.IsEnabled>
<MultiBinding Converter="{StaticResource AllGreaterThanZeroConverter}">
<Binding Source="{x:Reference entry}" Path="Text.Length" />
<Binding Source="{x:Reference stepper}" Path="Value" />
<Binding Source="{x:Reference slider}" Path="Value" />
</MultiBinding>
</Button.IsEnabled>
</Button>
</VerticalStackLayout>
</ContentPage>
We instantiated the converter in the resource dictionary and referenced it to set the MultiBinding.Converter
property. If we now run the app, the entry will be empty (which it is by default) and the two other controls will have the default value of the Value
property, which is 0. The button should be disabled:
If you now start playing with the three controls, the button will remain disabled as long as at least one of the source properties is less than or equal to zero:
But if all three values are greater than zero, the button will be enabled:
It will remain enabled until any of the three values is again set to 0. Try it out.
That’s it, as far as data binding is concerned, at least for now. We’ve covered the basics, but where data binding really shines is in an app where the MVVM pattern is implemented. Our Slugrace application is going to be such an app, so let’s start implementing the pattern now.