Custom Controls in Xamarin.Forms
Make your codebase more maintainable by creating your own custom controls in Xamarin.Forms.
Introduction
As Xamarin.Forms developers, one of our main goals is to maximise code sharing and minimise code duplication. However, when building our user interfaces, it can be all too common to duplicate UI code in the rush to ship your app!
To solve our problem of code duplication and our maintenance headaches, we can build custom controls!
In this article, we'll build a FormEntry control that can be re-used throughout our app for custom forms.
The source code for this article can be found at https://github.com/matthewrdev/custom-controls-in-xamarin-forms
Our Survey App
Before we get started, let's take a quick look at our survey app.
Our app is a simple survey app that wants to learn a little bit about our user base.
This is what our initial XAML page looks like:
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MySurveyApp"
x:Class="MySurveyApp.SurveyPage" xmlns:controls="clr-namespace:MySurveyApp.Controls">
<StackLayout Margin="10,20">
<Label Text="Xamarin User Survey"
VerticalOptions="Center"
HorizontalOptions="Center"/>
<Grid HorizontalOptions="FillAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Text="First Name:" FontAttributes="Bold"/>
<Entry Grid.Row="1" Text="{Binding FirstName}"/>
</Grid>
<Grid HorizontalOptions="FillAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Text="Last Name:" FontAttributes="Bold"/>
<Entry Grid.Row="1" Text="{Binding LastName}"/>
</Grid>
<Grid HorizontalOptions="FillAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Text="Company:" FontAttributes="Bold"/>
<Entry Grid.Row="1" Text="{Binding Company}"/>
</Grid>
</StackLayout>
</ContentPage>
While this page works as expected, over the long term it will cause maintenance headaches. As we've copy-pasted the same code block for the First Name
, Last Name
and Company
form fields, we now have three distinct UI definitions to maintain instead of one.
What if we wanted to change the background colour of the fields? We'd need to do it three times!
We can drastically improve this XAML page by refactoring the First Name
, Last Name
and Company
form fields into a single reusable control.
Create A Custom Control
Let's get started by creating a re-usable control for the First Name
, Last Name
and Company
fields. These form fields all share the same layout structure of a Label
and Entry
nested within a Grid
.
Creating The FormEntry Control
Firstly, create a new folder named Controls:
Next, make a new XAML control by right clicking on the Controls folder, selecting Add -> New File... and then choosing Forms -> Forms ContentView XAML and naming it FormEntry.
When creating a custom control, I prefer to use XAML to define the user interface and a code-behind for the control logic. In my experience, defining user interfaces is much easier in XAML (less code to write, easier to data-bind etc) and we can apply the Separation Of Concerns principle to isolate the control logic away from the UI definition, which makes the codebase much easier to understand.
Implementing The Controls XAML
Next, we want to implement the XAML for our control. We can easily do this by copying the implementation of one of the form entries in the SurveyPage.xaml
into the new controls XAML:
FormEntry.xaml
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MySurveyApp.Controls.FormEntry">
<ContentView.Content>
<Grid HorizontalOptions="FillAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Text="First Name:" FontAttributes="Bold"/>
<Entry Grid.Row="1" Text="{Binding FirstName}"/>
</Grid>
</ContentView.Content>
</ContentView>
Voila!
For efficiencies sake, we should also remove the root ContentView
of control as it's a redundant container around the Grid
:
<?xml version="1.0" encoding="utf-8"?>
<Grid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:local="clr-namespace:MySurveyApp"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
HorizontalOptions="FillAndExpand"
x:Class="MySurveyApp.Controls.FormEntry">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label FontAttributes="Bold" Grid.Row="0" Text="First Name:"/>
<Entry Grid.Row="1" Text="{Binding FirstName}"/>
</Grid>
You'll notice that the new control has some hardcoded values, which makes this control unable to be re-used against different property bindings.
For example, the label's text is still set to First Name:.
However, consumers of this control need to be able to set it to . Consumers will also want to data-bind other properties against the Entry
's Text
property, not just the FirstName property. At the moment this isn't possible.
So, how do we get around this?
The answer is to use bindable properties.
Bindable properties are a special type of property, where the controls property's value is tracked by the Xamarin.Forms property system. This lets consumers of our control data-bind to the property and for the control itself to respond to changes on that property.
Creating The Title Bindable Property
Let's create a bindable property to enable the Label
s Text
property to be data-bound.
Firstly, we need to apply an x:Name
onto the Label
:
<Label x:Name="title" FontAttributes="Bold" Grid.Row="0"/>
This will generate a field named title
in our code-behind class which we'll use to update its Text
property in response to data-binding.
Next, we create our bindable property:
public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), typeof(FormEntry), default(string), Xamarin.Forms.BindingMode.OneWay);
public string Title
{
get
{
return (string)GetValue(TitleProperty);
}
set
{
SetValue(TitleProperty, value);
}
}
Let's examine what each section of the above code does:
TitleProperty
- We use the
BindableProperty.Create(...)
factory method to create a bindable property that is shared across all instances of our control. TheTitleProperty
-
nameof(Title)
: The name of the property that can be data-bound on our control instance. We use thenameof()
syntax and reference -
typeof(string)
: The return type of our new property. We provide aSystem.Type
value by using thetypeof()
syntax. -
typeof(FormEntry)
: The declaring type of the property. This is our control type,FormEntry
. -
default(string)
: The default value of our bindable property. -
Xamarin.Forms.BindingMode.OneWay
: The direction that data should flow (from the binding context to the control). When setting up binding modes, we can use the following values: -
OneWay
: Data should only flow from the binding context to the control. Changes in the control cannot change the value in the binding context. This is the default binding mode. -
TwoWay
: Data flows to and from the binding context and control. Changes in the binding context will change the control and change the property on the control will change the binding context. -
OneWayToSource
: Data flows from the control to the binding context. Changes in the binding context cannot change the property value of the control.
Title
The Title
property is a public getter and setter that routes into the TitleProperty
... This is where the magic happens.
When we get or set the Title
property, depending on the binding mode, it can change the data-bound property on the binding context and/or trigger an OnPropertyChanged
event on the control.
Setting Up The Title Field Logic
Next, we need to make changes to the Title
property change the value of the title
labels Text
property. We do this by watching for property change events on TitleProperty
through the OnPropertyChanged
:
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == TitleProperty.PropertyName)
{
title.Text = Title;
}
}
Setting Up The Entry Logic
Our control needs one more thing to be complete; when the user enters text into our form field it should apply that change back onto the controls binding context!
Let's start by exposing the entry to our code-behind with an x:Name
:
<Entry x:Name="entry" Grid.Row="1"/>
Next, we create a bindable property for the entry field and bind it :
public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(FormEntry), default(string), BindingMode.TwoWay);
public string Text
{
get
{
return (string)GetValue(TextProperty);
}
set
{
SetValue(TextProperty, value);
}
}
// ...
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == TextProperty.PropertyName)
{
entry.Text = Text;
}
}
To enable changes on the Text
property to change the binding context, we've used the TwoWay
binding mode.
Next, we need to respond to user input on the entry
field and convert that into a property change on the binding context. We do this by updating Text
within the callback, TextChanged:
public FormEntry()
{
InitializeComponent();
entry.TextChanged += OnTextChanged;
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
Text = e.NewTextValue;
}
Our Final Control
Here is what our final control looks like:
FormEntry.xaml
<?xml version="1.0" encoding="utf-8"?>
<Grid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:local="clr-namespace:MySurveyApp"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
HorizontalOptions="FillAndExpand"
x:Class="MySurveyApp.Controls.FormEntry">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label x:Name="title" FontAttributes="Bold" Grid.Row="0"/>
<Entry x:Name="entry" Grid.Row="1"/>
</Grid>
FormEntry.xaml.cs
using System;
using Xamarin.Forms;
namespace MySurveyApp.Controls
{
public partial class FormEntry : Xamarin.Forms.Grid
{
public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), typeof(FormEntry), default(string), Xamarin.Forms.BindingMode.OneWay);
public string Title
{
get
{
return (string)GetValue(TitleProperty);
}
set
{
SetValue(TitleProperty, value);
}
}
public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(FormEntry), default(string), BindingMode.TwoWay);
public string Text
{
get
{
return (string)GetValue(TextProperty);
}
set
{
SetValue(TextProperty, value);
}
}
public FormEntry()
{
InitializeComponent();
title.Text = Title;
entry.Text = Text;
entry.TextChanged += OnTextChanged;
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
Text = e.NewTextValue;
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == TitleProperty.PropertyName)
{
title.Text = Title;
}
else if (propertyName == TextProperty.PropertyName)
{
entry.Text = Text;
}
}
}
}
Consuming Our FormEntry Control
Now that we've built our FormEntry
control, let's replace each of the fields in the SurveyPage
with it.
First, add a new XML namespace controls
to the root XAML element so we can reference the FormEntry
in XAML:
xmlns:controls="clr-namespace:MySurveyApp.Controls"
Next, we simply replace each form field with a reference to the FormEntry
control and specify the values of the Text
and Title
properties:
<controls:FormEntry Text="{Binding FirstName}" Title="First Name:"/>
<controls:FormEntry Text="{Binding LastName}" Title="Last Name:"/>
<controls:FormEntry Text="{Binding Company}" Title="Company:"/>
Using MFractor To Refactor Controls
Now that we've refactored our First Name
, Last Name
and Company
fields into their own controls, let's talk about how we can speed this entire process up.
Instead of refactoring the XAML by hand, we can use MFractor to extract the XAML section into a new control. Simply right-click on one of the Grid
declarations and choose Extract XAML into new control:
Instead of messing around creating the new file through the wizard, MFractor extracts the XAML section, places it into a new file, generates the code-behind and inserts a reference to the new control in the original XAML.
We can also use MFractor to generate our bindable property definitions. Personally, I have difficulty remembering the syntax for bindable properties so the Implement missing members using bindable properties code action makes it painless to create new bindable properties.
We can simply declare our new properties on the control, let MFractor's XAML analyser detect they are missing and then use the Implement missing members using bindable properties code action:
Summary
By building a custom FormEntry
control, we have significantly improved our survey page! Our page is now much easier to maintain, has less code and we've applied the Separation of Concerns principle to make our codebase easier to understand.
And importantly, we've learnt these key concepts:
- How to create our own reusable XAML controls.
- By using
BindableProperties
, we can create custom logic in our controls that respond to changes in the binding context. - We can control data flow between the control and binding context by using the different binding modes (
OneWay
,TwoWay
,OneWayToSource
).
We've also seen how MFractor can significantly speed up this process with the Extract XAML into new control and Implement missing members using bindable properties code actions.
You can visit www.mfractor.com to try out MFractor today.
To see more in-depth Xamarin tutorials, MFractor updates and more, follow @matthewrdev on Twitter.
Jun 02, 2022 • Posted by Sayyad Hasan
Excellent, very well explained