Skip to content
Home » Basics of .NET MAUI – Part 16 – Data Binding

Basics of .NET MAUI – Part 16 – Data Binding

Spread the love

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.

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:

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:

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:

And now let’s set the binding in the TestPage.xaml.cs file:

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:

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:

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:

Now the label looks better:

Here’s another example. Let’s add another label that will display the current date:

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:

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:

Or even, we can express the x:Reference markup extension as an object element:

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:

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.

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:

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:

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:

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:

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:

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:

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:

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:

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:

And now let’s set the parameters:

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:

Let’s use it:

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:

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:

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:

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:

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:

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:

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.


Spread the love

Leave a Reply