The process of building UI elements requires, at some point, some customizations to give the unique feel and look to the application and to extend the functionality of existing controls. Whether the customization is just overriding the default
When the
Process Inputs in run-time Through the
TextColor
of the Entry
control, or creating a brand new control with new look and behavior, building custom controls can provide the simple solution to achieve that, without the need of custom renderers.Create a Custom ToggleBar Control
The
ToggleBar
control can be used to show some options that the user can choose from, for example a filtering mechanism (similar to a group of radio buttons), or a light-weight tabbed control..etc, see the screenshot below (should look the same on iOS):
The behavior of the control is as follows:
- Every button in the control has a selected state and an unselected state determined by the
IsSelected
bindable property. - The states are visually distinguished through the
SelectedColor
and theUnselectedColor
bindable properties. - The selected items can be obtained through the bindable property
SelectedItems
. - The control supports multi-selection (that’s why it’s
SelectedItems
notSelectedItem
) by setting theIsMultiSelect
bindable property totrue
(defaults tofalse
).
Bindable properties are the foundation of custom controls (For more information about Xamarin.Forms bindable properties, see Xamarin.Forms Bindable Properties).
Every button inside the
ToggleBar
control is a custom control by itself. This article will guide you through creating the ToggleButton
control and the same concepts can be leveraged in the ToggleBar
control (see the complete sample).
The process for creating custom controls is as follows:
- Create a subclass of the view you want to extend or modify.
- Alter the functionality of the subclass by overriding the default values of the base class’s bindable properties and/or create new bindable properties that will interact with user actions.
- Initialize the custom control.
- Process inputs in run-time through the
propertyChanged
delegate of the newly added bindable properties.
Create a Subclass of the View you Want to Extend
Most of custom controls are hosted in a
ContentView
as it is the simplest container and doesn't expose special properties (like Orientation
of the StackLayout
that can interfer with the the control behavior). Create a subclass from ContentView
in the .NET Standard library project, name it ToggleButton
, it holds the StackLayout
that has two children: Label
and BoxView
, the following diagram illustrates the control outline:
When the StackLayout
is tapped, the selection state is mutated. The visual state is defined by the TextColor
property of the Label
and the Color
property of the BoxView
.
Alter the Functionality of the Subclass:
Create the bindable properties:
IsSelected
, SelectedColor
, UnselectedColor
, Text
, FontFamily
and FontSize
. This is the SelectedColor
property along with the BindableProperty
backing field:public static readonly BindableProperty SelectedColorProperty = BindableProperty.Create(nameof(SelectedColor), typeof(Color), typeof(ToggleButton), defaultValue: Color.Default); public Color SelectedColor { get { return (Color)GetValue(SelectedColorProperty); } set { SetValue(SelectedColorProperty, value); } }
A bindable property is a special type of property, where the property's value is tracked by the Xamarin.Forms property system. The purpose of bindable properties is to provide a property system that supports data binding, styles, templates, and values set through parent-child relationships. The process of creating a bindable property is as follows:
- Create a
BindableProperty
instance with one of theBindableProperty.Create
method overloads.- Define property accessors for the
BindableProperty
instance.For more information about Xamarin.Forms bindable properties, see Xamarin.Forms Bindable Properties
Custom bindable properties in custom controls can be categorized into two types:
- Bindable properties that are passed down to the built-in bindable properties of child elements, like
Text
bindable property of theToggleButton
custom control, that is passed down to theText
bindable property of theLabel
control.- Bindable properties that are specific to the custom control itself and not owned exclusively by any of the child elements, like the
IsSelected
bindable property. The more behavioral customization required to the custom control, the more of these bindable properties are needed.
Initializing the custom control
In the constructor of the
ToggleButton
class, initialize the inner controls and create the bindings between the new properties of the custom control and the properties of the inner controls (i.e., the StackLayout, the Label and the BoxView):public ToggleButton() { verticalStack = new StackLayout { Spacing = 0, HorizontalOptions = LayoutOptions.FillAndExpand }; label = new Label { HorizontalTextAlignment = TextAlignment.Center, VerticalTextAlignment = TextAlignment.Center, VerticalOptions = LayoutOptions.CenterAndExpand, Margin = new Thickness(5) }; boxView = new BoxView { VerticalOptions = LayoutOptions.End, HeightRequest = HeightRequest > 0 ? HeightRequest / 10d : 2 }; label.SetBinding(Label.TextColorProperty, new Binding(nameof(UnselectedColor), source: this)); label.SetBinding(Label.TextProperty, new Binding(nameof(Text), source: this)); label.SetBinding(Label.BackgroundColorProperty, new Binding(nameof(BackgroundColor), source: verticalStack)); label.SetBinding(Label.WidthRequestProperty, new Binding(nameof(WidthRequest), source: verticalStack)); label.SetBinding(Label.HeightRequestProperty, new Binding(nameof(HeightRequest), source: verticalStack)); label.SetBinding(Label.FontFamilyProperty, new Binding(nameof(FontFamily), source: this)); label.SetBinding(Label.FontSizeProperty, new Binding(nameof(FontSize), source: this)); boxView.SetBinding(BoxView.BackgroundColorProperty, new Binding(nameof(BackgroundColor), source: verticalStack)); verticalStack.GestureRecognizers.Add(new TapGestureRecognizer() { Command = new Command(() => { IsSelected = !IsSelected; SelectionChanged?.Invoke(this, IsSelected); }) }); verticalStack.Children.Add(label); verticalStack.Children.Add(boxView); Content = verticalStack; }
The constructor initializes the control properties, for example the
TextColor
property of the Label
is bound to the UnselectedColor
property of the custom control beacause the control is rendered in unselected state if IsSelected
is not set, similarly, the BoxView
's Color
property is bound to the value of the BackgroundColor
of the StackLayout
to hide it, it only gets highlited with SelectedColor
value when the control is selected. Setting the WidthRequest
and HeightRequest
for the Label
and HeightRequest
for the BoxView
ensures they scale with the StackLayout
size.
A
TapGestureRecognizer
is added to the StackLayout
’s GestureRecognizers
collection to mutate the selection state of the ToggleButton
when the StackLayout
is tapped. The TapGestureRecognizer
provides two approaches for handling the tap action: by the Tapped
event, or by the Command
property. For more information about the tap gesture recognizer, see Adding a tap gesture recognizer. When the value of IsSelected
propery changes, the propertyChanged
delegate handles the visual state of the control (see next section).
Create
SelectionChanged
event that gets invoked when the StackLayout
is tapped, to notify consumers of the ToggleButton
(e.g. the ToggleBar
control) when selection changes:public event EventHandler<bool> SelectionChanged;
Process Inputs in run-time Through the propertyChanged
Delegate
Add the
propertyChanged
parameter to the BindableProperty.Create
method used to create the IsSelectedProperty
, and handle it to mutate the visual state of the control based on the IsSelected
value:public static readonly BindableProperty IsSelectedProperty = BindableProperty.Create(nameof(IsSelected), typeof(bool), typeof(ToggleButton), false, propertyChanged: (bindable, oldValue, newValue) => { ((ToggleButton)bindable).MutateSelect(); } ); void MutateSelect() { if (IsSelected) { button.TextColor = SelectedColor; underLine.Color = SelectedColor; } else { button.TextColor = UnselectedColor; underLine.Color = BackgroundColor; } }
Consuming the Custom Control
The
ToggleButton
control can be referenced in XAML in the .NET Standard library project by declaring a namespace for its location and using the namespace prefix on the control element. The following code example shows how the ToggleButton
control can be consumed by a XAML page:<ContentPage ...
xmlns:controls="clr-namespace:CustomControlsSample.CustomControls"
...>
...
<controls:ToggleButton x:Name="toggleButton" Text="On" BackgroundColor="Black" UnselectedColor="Gray" SelectedColor="White" SelectionChanged="ToggleButton_SelectionChanged"/>
...
</ContentPage>
The following code example shows how the
ToggleButton
control can be consumed by a C# page:public class MainPage : ContentPage
{
public MainPage ()
{
var toggleButton = new ToggleButton
{
Text = "On",
BackgroundColor = Color.Black,
UnselectedColor = Color.Gray,
SelectedColor = Color.White,
};
toggleButton.SelectionChanged += ToggleButton_SelectionChanged;
Content = toggleButton;
}
}
Attach a handler to the
SelectionChanged
event to handle the selection change in the code-behind file:private async void ToggleButton_SelectionChanged(object sender, EventArgs e)
{
string message = toggleButton.IsSelected ? "ToggleButton is selected" : "ToggleButton is unselected";
await DisplayAlert("ToggleButton", message, "OK");
}
Summary
This article has demonstrated how to create a custom control in Xamarin.Forms, enabling developers to create new controls with new look and behavior, in the .NET Standard library project, without creating custom renderers. With the rich set of the layouts and the Animation API in Xamarin.Forms, you can even create more sophisticated and appealing controls.