DEV Community

Kevin Le
Kevin Le

Posted on

Learn Xamarin.Forms Control Template by making a Button with built-in Loading Indicator in 4 steps

In Xamarin.Forms, Control Template offers a powerful way to build a template. The template is built by composing smaller parts. But building a template is not the end goal. The end goal is to use a so-built template to make UI controls and use them anywhere without having to duplicate common codes.

I’m going to show how to implement a button for a Xamarin.Forms app that has the following special features: (a) Upon being tapped-on, the button will show a loading indicator (Activity Indicator) indicating the app is doing some work asynchronously. (b) This button can be re-used every where in the application without any code duplication.

As an example, the button, in normal state looks like:

Alt Text

Upon being tapped-on, the button will show a loading indicator and looks like:

Alt Text

Side note: I came up with the prefix AsyncButton to be used in the naming which might or might not be the best choice. But for the rest of this post, we’re stuck with it.

Step 0. Read the official docs (optional)

Read about Xamarin.Forms Control Templates and Relative Binding. If it’s a little vague, hopefully, the docs are more valuable after the following 4 steps.

Step 1. Create Bindable Properties in a View file

Create a new file named AsyncButtonView.cs(just a plain old .cs file, no accompanying .xaml file):

using Xamarin.Forms;
namespace AsyncButtonApp.Components
{
public class AsyncButtonView : ContentView
{
public static readonly BindableProperty ButtonCornerRadiusProperty = BindableProperty.Create(nameof(ButtonCornerRadius), typeof(int), typeof(AsyncButtonView), 0);
public static readonly BindableProperty ButtonHasShadowProperty = BindableProperty.Create(nameof(ButtonHasShadow), typeof(bool), typeof(AsyncButtonView), false);
public static readonly BindableProperty ButtonClippedToBoundsProperty = BindableProperty.Create(nameof(ButtonClippedToBounds), typeof(bool), typeof(AsyncButtonView), false);
public static readonly BindableProperty ButtonBackgroundColorProperty = BindableProperty.Create(nameof(ButtonBackgroundColor), typeof(Color), typeof(AsyncButtonView), Color.White);
public static readonly BindableProperty ButtonTextColorProperty = BindableProperty.Create(nameof(ButtonTextColor), typeof(Color), typeof(AsyncButtonView), Color.Black);
public static readonly BindableProperty ButtonTextProperty = BindableProperty.Create(nameof(ButtonText), typeof(string), typeof(AsyncButtonView), string.Empty);
public static readonly BindableProperty IsFetchingProperty = BindableProperty.Create(nameof(IsFetching), typeof(bool), typeof(AsyncButtonView), false);
public static readonly BindableProperty FetchCommandProperty = BindableProperty.Create(nameof(FetchCommand), typeof(Command), typeof(AsyncButtonView), null);
public int ButtonCornerRadius
{
get => (int)GetValue(ButtonCornerRadiusProperty);
set => SetValue(ButtonCornerRadiusProperty, value);
}
public bool ButtonHasShadow
{
get => (bool)GetValue(ButtonHasShadowProperty);
set => SetValue(ButtonHasShadowProperty, value);
}
public bool ButtonClippedToBounds
{
get => (bool)GetValue(ButtonClippedToBoundsProperty);
set => SetValue(ButtonClippedToBoundsProperty, value);
}
public Color ButtonBackgroundColor
{
get => (Color)GetValue(ButtonBackgroundColorProperty);
set => SetValue(ButtonBackgroundColorProperty, value);
}
public Color ButtonTextColor
{
get => (Color)GetValue(ButtonTextColorProperty);
set => SetValue(ButtonTextColorProperty, value);
}
public string ButtonText
{
get => (string)GetValue(ButtonTextProperty);
set => SetValue(ButtonTextProperty, value);
}
public bool IsFetching
{
get => (bool)GetValue(IsFetchingProperty);
set => SetValue(IsFetchingProperty, value);
}
public Command FetchCommand
{
get => (Command)GetValue(FetchCommandProperty);
set => SetValue(FetchCommandProperty, value);
}
}
}

Point of interest: This class AsyncButtonView subclasses from ContentView and is really simple. It just lists all the bindable properties. Glancing at the variables names such ButtonCornerRadius, ButtonBackgroundColor, ButtonTextColor, ButtonText, etc, we can quickly tell that these properties are used later on to customize the look and feel of a button. But this class does not “blueprint” how to construct the button. That’s for step 2.

Step 2. Build a Control Template for the Button

A Control Template “blueprints” how to build the UI for the control. Create a new file named AsyncButtonControlTemplateResourceDictionary.xaml and put it in the same folder with App.xml(Every Xamarin.Forms app should already have an App.xml file):

<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<ControlTemplate x:Key="AsyncButtonControlTemplate">
<Frame CornerRadius="{Binding Source={RelativeSource TemplatedParent},Path=ButtonCornerRadius}"
HasShadow="{Binding Source={RelativeSource TemplatedParent},Path=ButtonHasShadow}"
IsClippedToBounds="{Binding Source={RelativeSource TemplatedParent},Path=ButtonClippedToBounds}"
HorizontalOptions="Center" Padding="0">
<StackLayout BackgroundColor="{Binding Source={RelativeSource TemplatedParent},Path=ButtonBackgroundColor}" Orientation="Horizontal">
<Button Text="{Binding Source={RelativeSource TemplatedParent},Path=ButtonText}" BackgroundColor="{Binding Source={RelativeSource TemplatedParent},Path=ButtonBackgroundColor}"
TextColor="{Binding Source={RelativeSource TemplatedParent},Path=ButtonTextColor}" HorizontalOptions="CenterAndExpand"
Command="{Binding Source={RelativeSource TemplatedParent},Path=FetchCommand}"/>
<ActivityIndicator Scale="0.9" IsRunning="{Binding Source={RelativeSource TemplatedParent},Path=IsFetching}" IsVisible="{Binding Source={RelativeSource TemplatedParent},Path=IsFetching}"
HorizontalOptions="CenterAndExpand" Color="{Binding Source={RelativeSource TemplatedParent},Path=ButtonTextColor}" />
</StackLayout>
</Frame>
</ControlTemplate>
</ResourceDictionary>

Point of interest: The Control Template above instructs to build the button by composing smaller parts. It instructs to layout a Xamarin.Forms Button next to a Xamarin.Forms ActivityIndicator horizontally (line 9 through 15). Then it instructs to put this layout inside a Xamarin.Forms Frame (line 5 through 16). This Frame class allows for the Button’s rounded corners. Finally, we have a Button ControlTemplate out this construction by wrapping it inside a Xamarin.Forms ControlTemplate (line 4 through 17).

But if you look more closely at this file, this Button ControlTemplate is not connected to AsyncButtonView in step 1 in any way. The connection will be made when this Button ControlTemplate is actually used to make a button. All the bindings however are already specified by now as illustrated by the following line.

BackgroundColor="{Binding Source={RelativeSource TemplatedParent},Path=ButtonBackgroundColor}"
Enter fullscreen mode Exit fullscreen mode

Step 3: Consume the Button ControlTemplate to make a button

Finally we are ready to use the Button ControlTemplate to make an Async Button. As an example, the following file shows how to put together everything that we have created so far:

<?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:components="clr-namespace:AsyncButtonApp.Components"
xmlns:vm="clr-namespace:AsyncButtonApp.ViewModels"
x:Class="AsyncButtonApp.Views.MainPage"
Title="Main Page">
<ContentPage.BindingContext>
<vm:MainPageViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/AsyncButtonControlTemplateResourceDictionary.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<StackLayout BackgroundColor="White">
<components:AsyncButtonView ControlTemplate="{StaticResource AsyncButtonControlTemplate}"
HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" Padding="0"
ButtonCornerRadius="8"
ButtonHasShadow="True" ButtonClippedToBounds="True"
ButtonBackgroundColor="#3F51B5"
ButtonTextColor="White"
ButtonText="Fetch Data"
IsFetching="{Binding IsFetching}" FetchCommand="{Binding FetchCommand}" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
view raw MainPage.xaml hosted with ❤ by GitHub

Point of interest: The file in step 2 AsyncButtonControlTemplateResourceDictionary.xaml is referenced in the ResourceDictionary on line 14. On line 20, the AsyncButtonView and the Button ControlTemplate work together along with the supplied values for the rest of the properties. These 2 lines (line 14 and line 20) “connects” the bindable properties in step 1 with the UI Control Template in step 2. Line 9 references the ViewModel which we will do in step 4.

Step 4: Add a ViewModel

using Xamarin.Forms;
using System.ComponentModel;
using System.Threading.Tasks;
namespace AsyncButtonApp.ViewModels
{
public class MainPageViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Command FetchCommand { get; }
public MainPageViewModel()
{
FetchCommand = new Command(async (obj) => await OnFetchClicked(obj));
}
private async Task OnFetchClicked(object obj)
{
IsFetching = true;
await Task.Delay(1500); //simulates some async work
IsFetching = false;
}
private bool _isFetching;
public bool IsFetching
{
get { return _isFetching; }
set
{
_isFetching = value;
OnPropertyChanged(nameof(IsFetching));
}
}
}
}

Cross posted on medium

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay