Home / News / Custom Controls in Xamarin.Forms

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.

The 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:

The controls folder

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.

Creating the FormEntry conrol

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 Labels 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. The TitleProperty
  • nameof(Title): The name of the property that can be data-bound on our control instance. We use the nameof()syntax and reference
  • typeof(string): The return type of our new property. We provide a System.Type value by using the typeof()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:

Extracting a XAML section into a 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:

Implementing bindable properties

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.

1 comment

Jun 02, 2022 • Posted by Sayyad Hasan

Excellent, very well explained

Leave a comment