Learn how to combine content views with bindable properties, so you can control both the appearance and behavior of a custom control.
Within the world of mobile development and development in general, there is always a need to combine two or more controls to create more complex components that we can reuse across different projects. Fortunately, .NET MAUI has features that allow us to achieve this.
In this post, you will learn how to create a reusable component that shows the progress of a task. We will achieve this by combining content views with bindable properties, which will allow us to control both the appearance and behavior of the custom control. Let’s get started!
A ContentView is a control that allows the creation of reusable and custom controls. You can think of this control as a blank canvas on which you can place different controls, images, text, etc., with the purpose of reusing it throughout one or several applications.
Some examples of using a ContentView control could be to define the visual appearance of rows in a CollectionView or a custom view for a TitleView. Although there are several ways to create a ContentView, the simplest is to click on the context menu in the solution explorer, then Right Click | Add | New Item | .NET MAUI ContentPage (XAML) as shown below:
Some things to note in the previous image are that it is also possible to create a ContentView file with just C# code. In my case, I will use the XAML template with the name Downloader.
Once the ContentView file has been created, it will be opened automatically. This file has a structure similar to a ContentPage with the difference that the container type is a ContentView
:
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="CustomControlDemo.Views.Downloader">
...
</ContentView>
Just like a ContentPage, a ContentView has a Content
property, which we can use to add any desired content. In this example, we will use the following controls to create the custom control:
Border
to group the control’s content and to add a shadowLabel
to display a custom message about the download in generalLabel
to indicate the percentage completedRadLinearProgressBar
control to visually indicate the progressThe XAML code would look as follows:
<Grid Margin="10" MaximumHeightRequest="150">
<Border
Background="White"
Stroke="White"
StrokeShape="RoundRectangle 10">
<Grid Margin="15" RowDefinitions="*,*,Auto">
<Label
x:Name="SubTitleControl"
FontAttributes="Bold"
FontSize="12"
Text="Your progress"
TextColor="#BFC9D2" />
<Label
x:Name="TitleControl"
Grid.Row="1"
FontAttributes="Bold"
FontSize="15"
Text="30% To Complete"
TextColor="#0F49F7" />
<telerik:RadLinearProgressBar
x:Name="ProgressControl"
Grid.Row="2"
Margin="0,10,0,0"
AutomationId="progressBar"
Value="60" />
</Grid>
<Border.Shadow>
<Shadow
Brush="Gray"
Opacity="0.8"
Radius="40"
Offset="20,20" />
</Border.Shadow>
</Border>
</Grid>
With this, we have created the content of the custom control. The next step is to use it somewhere within the .NET MAUI project, which we will do next.
Once we have created the custom control, we will use it in our project. To do this, you first need to add the reference to the namespace location of your control. I have created a folder called Controls within the project, so the namespace to use will be as follows:
xmlns:controls="clr-namespace:CustomControlDemo.Controls"
With the reference created, all that is needed is to use the control, as in the following example:
<VerticalStackLayout Spacing="50" VerticalOptions="Center">
<controls:Downloader />
<Button Margin="25" Text="Start Download" />
</VerticalStackLayout>
This will show the control when the application is executed, as in the following image:
The control undoubtedly looks great, but it is not usable at the moment because there is no way to manipulate the values of the custom control. This is where Bindable Properties can help us create an access point for custom control. Let’s see how to use them!
A bindable property allows extending a Common Language Runtime (CLR) property by using a BindableProperty
type instead of a field. In short, this means we will be able to use these properties through data binding and, if needed, we can assign default values, validate property values and add callbacks to execute actions when property values change.
To create a bindable property, we will go to the class where we want to add it. In our example, we will go to the Downloader.xaml.cs
file. In this file, we will create the following BindableProperty:
public static readonly BindableProperty TitleProperty =
BindableProperty.Create
(
"Title",
typeof(string),
typeof(Downloader),
"0% To Complete",
propertyChanged: (bindable, oldValue, newValue) =>
{
if (bindable is Downloader instance)
{
instance.TitleControl.Text = (string)newValue;
}
});
This is what each line in the previous code does:
public static readonly BindableProperty
. This signature must always be used when creating a bindable property and, by convention, it should contain the word “Property” at the end.Create
method, which initializes the instance according to the provided parameters.Downloader
.Text
property of the TitleControl
control, which is one of the Labels displaying completion information.The Create
method accepts more parameters than those shown here. For more information, you can refer to the following documentation page.
The second part of creating a bindable property involves creating a property backed by the BindableProperty
we previously created. We achieve this as follows:
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
In the previous code, you can see that the property uses the GetValue
method to return the value of TitleProperty
in the getter, and the SetValue
method to assign the value to TitleProperty
in the setter.
Since we want the custom control to have multiple properties available (you can add as many as you want to the example), I have created a few others which I show below:
public partial class Downloader : ContentView
{
public static readonly BindableProperty TitleProperty =
BindableProperty.Create("Title", typeof(string), typeof(Downloader), "0% To Complete",
propertyChanged: (bindable, oldValue, newValue) =>
{
if (bindable is Downloader instance)
{
instance.TitleControl.Text = (string)newValue;
}
});
public static readonly BindableProperty SubTitleProperty =
BindableProperty.Create("SubTitle", typeof(string), typeof(Downloader), "Your progress",
propertyChanged: (bindable, oldValue, newValue) =>
{
if (bindable is Downloader instance)
{
instance.SubTitleControl.Text = (string)newValue;
}
});
public static readonly BindableProperty ProgressProperty =
BindableProperty.Create("Progress", typeof(double), typeof(Downloader), 0.0,
propertyChanged: (bindable, oldValue, newValue) =>
{
if (bindable is Downloader instance)
{
instance.ProgressControl.Value = (double)newValue;
}
});
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public string SubTitle
{
get { return (string)GetValue(SubTitleProperty); }
set { SetValue(SubTitleProperty, value); }
}
public string Progress
{
get { return (string)GetValue(ProgressProperty); }
set { SetValue(ProgressProperty, value); }
}
public Downloader()
{
InitializeComponent();
TitleControl.Text = (string)TitleProperty.DefaultValue;
SubTitleControl.Text = (string)SubTitleProperty.DefaultValue;
ProgressControl.Value = (double)ProgressProperty.DefaultValue;
}
}
We now have both the graphical and logical parts of the custom control. Now we can go back to where we used the control and use the newly created bindable properties:
<controls:Downloader
Title="Current download: 0%"
Progress="25"
SubTitle="Saving a copy of the file" />
You can see how this allows us to customize the custom control as much as we want. The true power of bindable properties is that we can bind them to a ViewModel
through bindings. In my example, I have created the following ViewModel:
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
string title = "Current download: 0%";
[ObservableProperty]
double progress;
[ObservableProperty]
string subTitle = "Saving a copy of the file";
partial void OnProgressChanged(double value)
{
Title = $"Current download: {value.ToString("F2")}%";
}
[RelayCommand]
public async Task Download()
{
Progress = 0.0;
for (int i = 0; i <= 100; i++)
{
await Task.Delay(100);
Progress = i;
}
}
}
In the previous code, I created bindable properties to manipulate the properties of the control, including a simulation of downloading a file in the Download
method. With this, we can bind the properties of the ViewModel with the XAML code as follows:
<VerticalStackLayout Spacing="50" VerticalOptions="Center">
<controls:Downloader
Title="{Binding Title}"
Progress="{Binding Progress}"
SubTitle="{Binding SubTitle}" />
<Button
Margin="25"
Command="{Binding DownloadCommand}"
Text="Start Download" />
</VerticalStackLayout>
The result is seeing how the values of the custom control change when pressing the button:
In this post, you have learned how to create ContentViews to build the graphical part of a custom control. Likewise, you have learned what bindable properties are and how to use them to enable properties from outside the control, allowing the use of data binding.
This knowledge will give you great power in creating reusable components not only in one project but across all your projects with .NET MAUI.
Héctor Pérez is a Microsoft MVP with more than 10 years of experience in software development. He is an independent consultant, working with business and government clients to achieve their goals. Additionally, he is an author of books and an instructor at El Camino Dev and Devs School.