In this post, I show you how to create a nice TabBar in MAUI without using any external NuGet package or components. Fully customizable and 100% XAML.
Here is the result of the code:
The source code of this post is on GitHub.
Bindable Layout
.NET MAUI bindable layouts enable any layout class that derives from the Layout class to generate its content by binding to a collection of items, with the option to set the appearance of each item with a DataTemplate.
Bindable layouts are provided by the BindableLayout class, which exposes the following attached properties:
ItemsSource
– specifies the collection ofIEnumerable
items to be displayed by the layout.ItemTemplate
– specifies the DataTemplate to apply to each item in the collection of items displayed by the layout.ItemTemplateSelector
– specifies the DataTemplateSelector that will be used to choose a DataTemplate for an item at runtime.- In addition, the BindableLayout class exposes the following bindable properties:
EmptyView
– specifies thestring
or view that will be displayed when theItemsSource
property isnull
, or when the collection specified by theItemsSource
property isnull
or empty. The default value isnull
.EmptyViewTemplate
– specifies the DataTemplate that will be displayed when theItemsSource
property isnull
, or when the collection specified by theItemsSource
property isnull
or empty. The default value isnull
.
Populate a bindable layout with data
A bindable layout is populated with data by setting its ItemsSource
property to any collection that implements IEnumerable
, and attaching it to a Layout-derived class:
<Grid BindableLayout.ItemsSource="{Binding Items}" />
When the BindableLayout.ItemsSource
attached property is set on a layout, but the BindableLayout.ItemTemplate
attached property isn’t set, every item in the IEnumerable
collection will be displayed by a Label that’s created by the BindableLayout class.
Radiobutton
The .NET Multi-platform App UI (.NET MAUI) RadioButton is a type of button that allows users to select one option from a set. Each option is represented by one radio button, and you can only select one radio button in a group. By default, each RadioButton displays text:
However, on some platforms a RadioButton can display a View, and on all platforms the appearance of each RadioButton can be redefined with a ControlTemplate:
RadioButton defines the following properties:
Content
, of typeobject
, which defines thestring
or View to be displayed by the RadioButton.IsChecked
, of typebool
, which defines whether the RadioButton is checked. This property uses aTwoWay
binding, and has a default value offalse
.GroupName
, of typestring
, which defines the name that specifies which RadioButton controls are mutually exclusive. This property has a default value ofnull
.Value
, of typeobject
, which defines an optional unique value associated with the RadioButton.BorderColor
, of type Color, which defines the border stroke color.BorderWidth
, of typedouble
, which defines the width of the RadioButton border.CharacterSpacing
, of typedouble
, which defines the spacing between characters of any displayed text.CornerRadius
, of typeint
, which defines the corner radius of the RadioButton.FontAttributes
, of typeFontAttributes
, which determines text style.FontAutoScalingEnabled
, of typebool
, which defines whether an app’s UI reflects text scaling preferences set in the operating system. The default value of this property istrue
.FontFamily
, of typestring
, which defines the font family.FontSize
, of typedouble
, which defines the font size.TextColor
, of type Color, which defines the color of any displayed text.TextTransform
, of typeTextTransform
, which defines the casing of any displayed text.
These properties are backed by BindableProperty objects, which means that they can be targets of data bindings, and styled.
RadioButton also defines a CheckedChanged
event that’s raised when the IsChecked
property changes, either through user or programmatic manipulation. The CheckedChangedEventArgs
object that accompanies the CheckedChanged
event has a single property named Value
, of type bool
. When the event is raised, the value of the CheckedChangedEventArgs.Value
property is set to the new value of the IsChecked
property.
RadioButton grouping can be managed by the RadioButtonGroup
class, which defines the following attached properties:
GroupName
, of typestring
, which defines the group name for RadioButton objects in anILayout
.SelectedValue
, of typeobject
, which represents the value of the checked RadioButton object within anILayout
group. This attached property uses aTwoWay
binding by default.
For more information about the GroupName
attached property, see Group RadioButtons. For more information about the SelectedValue
attached property, see Respond to RadioButton state changes.
The UI implementation
First, I created my container with a set of data to be repeated for each tab.
<HorizontalStackLayout>
<BindableLayout.ItemTemplate>
<!-- Radiobutton here -->
</BindableLayout.ItemTemplate>
<BindableLayout.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>Hot Dishes</x:String>
<x:String>Cold Dishes</x:String>
<x:String>Soups</x:String>
<x:String>Appetizers</x:String>
<x:String>Desserts</x:String>
</x:Array>
</BindableLayout.ItemsSource>
</HorizontalStackLayout>
Then, I add the RadioButton
<BindableLayout.ItemTemplate>
<DataTemplate>
<RadioButton Content="{Binding .}" />
</DataTemplate>
</BindableLayout.ItemTemplate>
To make this a grouped list of radios, I added a name to the parent layout.
<HorizontalStackLayout
RadioButtonGroup.GroupName="MenuCategories">
Add a style
Then, I styled the RadioButton
using a ControlTemplate
.
<RadioButton Content="{Binding .}">
<RadioButton.ControlTemplate>
<ControlTemplate>
<Grid RowDefinitions="30,4">
<Label Text="{TemplateBinding Content}" />
<BoxView Grid.Row="1" Color="Transparent"/>
</Grid>
</ControlTemplate>
</RadioButton.ControlTemplate>
</RadioButton>
Because we are inside of a control template, rather than using Binding
I use TemplateBinding
. The Content
could be anything, but I supplied a String
so it seems safe to bind that directly to the label text.
Now, to get a different look for selected vs unselected, I:
- added a
VisualStateManager
(VSM) to the control template layout - gave my label and box names so I could target them from the VSM
- styled the checked and unchecked states
<ControlTemplate>
<Grid RowDefinitions="30,4">
<VisualStateManager.VisualStateGroups>
<VisualStateGroupList>
<VisualStateGroup x:Name="CheckedStates">
<VisualState x:Name="Checked">
<VisualState.Setters>
<Setter
TargetName="TextLabel"
Property="Label.TextColor"
Value="{StaticResource Primary}"/>
<Setter
TargetName="Indicator"
Property="BoxView.Color"
Value="{StaticResource Primary}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Unchecked">
<VisualState.Setters>
<Setter
TargetName="TextLabel"
Property="Label.TextColor"
Value="White"/>
<Setter
TargetName="Indicator"
Property="BoxView.Color"
Value="Transparent"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</VisualStateManager.VisualStateGroups>
<Label Text="{TemplateBinding Content}" x:Name="TextLabel" />
<BoxView x:Name="Indicator" Grid.Row="1" Color="Transparent"/>
</Grid>
</ControlTemplate>
Binding with a ViewModel
Now, I have a nice and easy way to display tabs but I want to bind the UI with my data that are coming from the ViewModel.
First, I add a model for the items called MenuItem
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MauiTabs
{
/// <summary>
/// Class MenuItem.
/// </summary>
public class MenuItem
{
/// <summary>
/// Gets or sets the text.
/// </summary>
/// <value>The text.</value>
public string? Text { get; set; }
/// <summary>
/// Gets or sets the value.
/// </summary>
/// <value>The value.</value>
public string? Value { get; set; }
}
}
Create the ViewModel
Now, using the CommunityToolkit
I’m going to create the ViewModel as in the following code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MauiTabs
{
public partial class MainPageViewModel : ObservableObject
{
[ObservableProperty] private List<MenuItem>? _menuItems;
[ObservableProperty] private string? _selected;
public MainPageViewModel()
{
MenuItems = new List<MenuItem>
{
new MenuItem { Text = "Words", Value = "Words" },
new MenuItem { Text = "Games", Value = "Games" },
new MenuItem { Text = "Statistics", Value = "Statistics" }
};
Selected = MenuItems[0].Value;
}
}
}
In line 13, I define the selected or default value. In this variable, the application will save the choice of the user. It is a simple string
that will contain the Value
of the MenuItem
.
Then, in line 24, I define by default that the default element alias the element I want to highlight when the page will appear is the first one.
Bind the ViewModel
Now, in the page, in my case _MainPage_, I’m going to bind the ViewModel to the page itself like that:
namespace MauiTabs
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
BindingContext = new MainPageViewModel();
}
}
}
Change the XAML
After everything, I have to change the XAML to display the text and the value for the ViewModel.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
x:Class="MauiTabs.MainPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:dt="clr-namespace:MauiTabs">
<ScrollView>
<VerticalStackLayout>
<HorizontalStackLayout BindableLayout.ItemsSource="{Binding MenuItems}"
RadioButtonGroup.GroupName="MenuCategories"
RadioButtonGroup.SelectedValue="{Binding Selected}">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="dt:MenuItem">
<RadioButton
x:Name="radio"
Margin="0,0,15,0"
Content="{Binding Text}"
GroupName="MenuCategories"
Value="{Binding Value}">
<RadioButton.ControlTemplate>
<ControlTemplate>
<Grid RowDefinitions="30,4">
<Label x:Name="TextLabel" Text="{TemplateBinding Content}" />
<BoxView
x:Name="Indicator"
Grid.Row="1"
Color="Transparent" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroupList>
<VisualStateGroup x:Name="CheckedStates">
<VisualState x:Name="Checked">
<VisualState.Setters>
<Setter TargetName="TextLabel" Property="Label.TextColor" Value="{StaticResource Primary}" />
<Setter TargetName="Indicator" Property="BoxView.Color" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Unchecked">
<VisualState.Setters>
<Setter TargetName="TextLabel" Property="Label.TextColor" Value="Black" />
<Setter TargetName="Indicator" Property="BoxView.Color" Value="Transparent" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</RadioButton.ControlTemplate>
</RadioButton>
</DataTemplate>
</BindableLayout.ItemTemplate>
</HorizontalStackLayout>
<HorizontalStackLayout>
<Label Text="Your selection:" />
<Label x:Name="labelSelection" Text="{Binding Selected}" />
</HorizontalStackLayout>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
In line 12, I bind the variable Selected
to the RadioButtonGroup.SelectedValue
. The first time the page is displayed, the first RadioButton
is selected as expected. When a user taps on the others, the Selected
value will change with the defined Value
.
In lines 18 and 20, I bind the Text
to display and the Value for each element. Remember to set the GroupName
because instead it will not work.
Finally, in line 57, the Selected
value will be display.
Wrap up
In conclusion, I hope this code will help you to create a nice TabBar in MAUI without using third-party components and full-customized as you like. Let me know what you think in the comment or in the Forum.