Xamarin.Forms MVVM - Custom Tabbed Page

Introduction

The Xamarin.Forms TabbedPage consists of a list of tabs and a larger detailed area, with each tab loading content into the detail area. But If we want to add some extra controls top of tabbed page or bottom of the tabbed page we can't; in that case, we need to create our own custom tabs. So, this article will explain to you how we can create our own custom tabbed page.

Requirements

  • This article's source code is prepared by using Visual Studio with MAC machine. And it is better to install the latest Visual Studio updates from here.
  • This application is targeted for Android, iOS. And tested on Android device & iOS simulator.
  • This project is Xamarin.Forms PCL project.

Description

In this article, we going to create three custom tabs such as C# Corner, Xamarin, Microsoft. 

Let's start creating Custom TabbedView.

Step 1

First, follow the below steps to create the new Xamarin.Forms project.

  • Open Visual Studio for Mac.
  • Click on the File menu, and select New Solution.
  • In the left pane of the dialog, let's select the type of templates to display. Multiplatform > App > Xamarin.Forms > Blank Forms App and click on Next.
  • Next Enter your app name (Ex: CustomTabbedPage). At the bottom select target platforms to Android & iOS and shared code to Portable Class Library and click on Next button.

  • Then choose project location with the help of Bbowse button and click on create.

Now, the project structure will be created like below. 

  • CustomTabbedPage
    It is for Shared Code

  • CustomTabbedPage.Droid
    It is for Android.

  • CustomTabbedPage.iOS
    It is for iOS 
Step 2

PCL

Create a class with RibbonView name it should inherit from ContentView and place it inside ControlsToolkit -->Custom folder and add code.

RibbonView.cs

  1. using System;  
  2. using System.Collections;  
  3. using System.Collections.Generic;  
  4. using System.Linq;  
  5. using System.Windows.Input;  
  6. using Xamarin.Forms;  
  7. namespace CustomRabbedPageControlsToolkit.Custom {  
  8.     public class RibbonView: ContentView {  
  9. #region ItemsSource property  
  10.         public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IList), typeof(RibbonView), null, propertyChanged: OnItemsSourceModified);  
  11.         public IList ItemsSource {  
  12.             get {  
  13.                 return (IList) GetValue(ItemsSourceProperty);  
  14.             }  
  15.             set {  
  16.                 SetValue(ItemsSourceProperty, value);  
  17.             }  
  18.         }  
  19.         private static void OnItemsSourceModified(object sender, object oldValue, object newValue) {  
  20.             RibbonView rv = (RibbonView) sender;  
  21.             rv._itemsContainerLayout.Children.Clear();  
  22.             IEnumerator iter = ((IList)newValue).GetEnumerator(); 
  23.  
  24.             int index = 0;  
  25.             while (iter.MoveNext()) {  
  26.                 String label = (String)iter.Current;  
  27.                 StackLayout layout = new StackLayout() {  
  28.                     Orientation = StackOrientation.Vertical,  
  29.                         VerticalOptions = LayoutOptions.FillAndExpand,  
  30.                         HorizontalOptions = LayoutOptions.FillAndExpand,  
  31.                         Padding = new Thickness(10, 10, 10, 0),  
  32.                 };  
  33.                 Label titleLabel = new Label() {  
  34.                         Text = label,  
  35.                         TextColor = rv.TextColor,  
  36.                         FontSize = rv.FontSize,  
  37.                         Style = rv.Style,  
  38.                         VerticalTextAlignment = TextAlignment.Center,  
  39.                         HorizontalTextAlignment = TextAlignment.Center,  
  40.                         VerticalOptions = LayoutOptions.FillAndExpand,  
  41.                         HorizontalOptions = LayoutOptions.FillAndExpand  
  42.                 };  
  43.                 BoxView box = new BoxView() {  
  44.                         BackgroundColor = rv.BarColor,  
  45.                         VerticalOptions = LayoutOptions.EndAndExpand,  
  46.                         HeightRequest = 2,  
  47.                         HorizontalOptions = LayoutOptions.FillAndExpand  
  48.                 };  
  49.                 layout.Children.Add(titleLabel);  
  50.                 layout.Children.Add(box);  
  51.                 layout.GestureRecognizers.Add(new TapGestureRecognizer() {  
  52.                     Command = new Command((obj) => {  
  53.                         rv.OnSelectedItem(label);  
  54.                     })  
  55.                 });  
  56.                 ++index;  
  57.                 rv._itemsContainerLayout.Children.Add(layout);  
  58.             }  
  59.             rv.OnSelectedItem(((List<String>)rv.ItemsSource).ElementAt(rv.SelectedItemIndex));  
  60.         }
  61. #endregion 
  62.  
  63. #region Textcolor property  
  64.         public static readonly BindableProperty TextColorProperty = BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(RibbonView), Color.Black);  
  65.         public Color TextColor {  
  66.             get {  
  67.                 return (Color)GetValue(TextColorProperty);  
  68.             }  
  69.             set {  
  70.                 SetValue(TextColorProperty, value);  
  71.             }  
  72.         }
  73. #endregion  
  74.         public static new readonly BindableProperty StyleProperty = BindableProperty.Create(nameof(Style), typeof(Style), typeof(RibbonView), null);  
  75.         public new Style Style {  
  76.             get {  
  77.                 return (Style)GetValue(StyleProperty);  
  78.             }  
  79.             set {  
  80.                 SetValue(StyleProperty, value);  
  81.             }  
  82.         }
  83. #region Barcolor property  
  84.         public static readonly BindableProperty BarColorProperty = BindableProperty.Create(nameof(BarColor), typeof(Color), typeof(RibbonView), Color.Black);  
  85.         public Color BarColor {  
  86.             get {  
  87.                 return (Color) GetValue(BarColorProperty);  
  88.             }  
  89.             set {  
  90.                 SetValue(BarColorProperty, value);  
  91.             }  
  92.         }
  93. #endregion  
  94. #region FontSize property  
  95.         public static readonly BindableProperty FontSizeProperty = BindableProperty.Create(nameof(FontSize), typeof(int), typeof(RibbonView), 13);  
  96.         public int FontSize {  
  97.             get {  
  98.                 return (int) GetValue(FontSizeProperty);  
  99.             }  
  100.             set {  
  101.                 SetValue(FontSizeProperty, value);  
  102.             }  
  103.         }
  104. #endregion  
  105. #region SelectedItemIndex property  
  106.         public static readonly BindableProperty SelectedItemIndexProperty = BindableProperty.Create(nameof(SelectedItemIndex), typeof(int), typeof(RibbonView), 0);  
  107.         public int SelectedItemIndex {  
  108.             get {  
  109.                 return (int) GetValue(SelectedItemIndexProperty);  
  110.             }  
  111.             set {  
  112.                 SetValue(SelectedItemIndexProperty, value);  
  113.             }  
  114.         }
  115. #endregion  
  116. #region ItemSelected property  
  117.         public static readonly BindableProperty ItemSelectedProperty = Create(nameof(ItemSelected), typeof(ICommand), typeof(RibbonView), null);  
  118.         public ICommand ItemSelected {  
  119.             get {  
  120.                 return (ICommand) GetValue(ItemSelectedProperty);  
  121.             }  
  122.             set {  
  123.                 SetValue(ItemSelectedProperty, value);  
  124.             }  
  125.         }
  126. #endregion  
  127.         StackLayout _mainLayout;  
  128.         StackLayout _itemsContainerLayout;  
  129.         public RibbonView() {  
  130.             _mainLayout = new StackLayout() {  
  131.                     HorizontalOptions = LayoutOptions.FillAndExpand,  
  132.                     Padding = new Thickness(0),  
  133.                     Spacing = 0  
  134.             };  
  135.             _itemsContainerLayout = new StackLayout() {  
  136.                     Orientation = StackOrientation.Horizontal,  
  137.                     HorizontalOptions = LayoutOptions.FillAndExpand,  
  138.                     VerticalOptions = LayoutOptions.FillAndExpand,  
  139.                     Padding = new Thickness(5, 5, 5, 0),  
  140.                     Spacing = 0  
  141.             };  
  142.             _mainLayout.Children.Add(_itemsContainerLayout);  
  143.             // _mainLayout.Children.Add(new BoxView() { HeightRequest = 1, BackgroundColor = Color.Silver, HorizontalOptions = LayoutOptions.FillAndExpand });   
  144.             Content = _mainLayout;  
  145.         }  
  146.         private void OnSelectedItem(String labelTitle) {  
  147.             int i = 0;  
  148.             IEnumerator iter = ItemsSource.GetEnumerator();  
  149.             if (this.SelectedItemIndex > -1) {  
  150.                 StackLayout itemStack = (StackLayout) _itemsContainerLayout.Children.ToArray()[this.SelectedItemIndex];  
  151.                 BoxView bv = (BoxView)itemStack.Children.ToArray()[1];  
  152.                 bv.BackgroundColor = this.BackgroundColor;  
  153.             }  
  154.             while (iter.MoveNext()) {  
  155.                 StackLayout itemStack = (StackLayout) _itemsContainerLayout.Children.ToArray()[i];  
  156.                 BoxView bv = (BoxView) itemStack.Children.ToArray()[1];  
  157.                 if (((string) iter.Current).CompareTo(labelTitle) == 0) {  
  158.                     bv.BackgroundColor = this.BarColor;  
  159.                     this.SelectedItemIndex = i;  
  160.                 } else {  
  161.                     bv.BackgroundColor = this.BackgroundColor;  
  162.                 }  
  163.                 i += 1;  
  164.             }  
  165.             if (ItemSelected != null) {  
  166.                 ItemSelected.Execute(this.SelectedItemIndex);  
  167.             }  
  168.         }  
  169.     }  
  170. }  

Step 3

Create your own Xaml page named HomePage.xaml, and make sure to refer to "RibbonView" class in Xaml by declaring a namespace for its location and using the namespace prefix on the control element. The following code example shows how the "RibbonView" ContentView can be consumed by a XAML page:

HomePage.xaml

  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
  3. xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
  4. x:Class="CustomTabbedPage.Views.HomePage" 
  5. Title="Home" 
  6. BackgroundColor="#533F95" 
  7. xmlns:toolkitcustom="clr-namespace:CustomTabbedPage.ControlsToolkit.Custom">  
  8.     <ContentPage.Resources>  
  9.         <ResourceDictionary>  
  10.             <Style x:Key="DisplayDataValueStyle" TargetType="Label">  
  11.                 <Setter Property="FontFamily" Value="Times New Roman" /><Setter Property="TextColor" Value="#1B3B5F" /><Setter Property="FontSize" Value="15" />  
  12.             </Style>  
  13.         </ResourceDictionary>  
  14.     </ContentPage.Resources>  
  15.     <ContentPage.Content>  
  16.         <Grid HorizontalOptions="FillAndExpand" BackgroundColor="Transparent" VerticalOptions="FillAndExpand" RowSpacing="0" Padding="0,0,0,0">  
  17.             <Grid.RowDefinitions>  
  18.                 <RowDefinition Height="Auto" />  
  19.                 <RowDefinition Height="Auto" />  
  20.                 <RowDefinition Height="*" /> </Grid.RowDefinitions>  
  21.             <Grid Grid.Row="0" Padding="20,20,20,10">  
  22.                 <Button BackgroundColor="White" TextColor="#533F95" FontSize="15" FontAttributes="Bold" BorderRadius="0" Text="Custom TabbedPage" HorizontalOptions="FillAndExpand" /> </Grid>  
  23.             <toolkitcustom:RibbonView x:Name="ribbonViews" Padding="20,0,20,0" Row="1" HorizontalOptions="FillAndExpand" BackgroundColor="Transparent" BarColor="White" TextColor="White" Style="{StaticResource DisplayDataValueStyle}" ItemsSource="{Binding RibbonOptions}" ItemSelected="{Binding OptionSelectionChangedCommand}" />  
  24.             <WebView x:Name="webview" Source="{Binding LoadURI}" BackgroundColor="White" Margin="5" Grid.Row="2" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" />  
  25.             <ActivityIndicator IsRunning="{Binding IsBusy}" IsVisible="{Binding IsBusy}" Grid.Row="2" HeightRequest="50" WidthRequest="50" Color="Maroon" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" /> </Grid>  
  26.     </ContentPage.Content>  
  27. </ContentPage>   

Note
The "xmlns:toolkitcustom" namespace prefix can be named anything. However, the clr-namespace and assembly values must match the details of the ContentView class. Once the namespace is declared the prefix is used to reference the custom View/control/layout

HomePage.xaml.cs

  1. using CustomTabbedPage.ViewModels;  
  2. using Xamarin.Forms;  
  3. namespace CustomTabbedPage.Views {  
  4.     public partial class HomePage: ContentPage {  
  5.         public HomePage() {  
  6.             InitializeComponent();  
  7.             var homePageViewModel = new HomePageViewModel();  
  8.             BindingContext = homePageViewModel;  
  9.             webView.Navigated += (sender, e) => {  
  10.                 if (e.Url != null) {  
  11.                     homePageViewModel.IsBusy = false;  
  12.                 }  
  13.             };  
  14.         }  
  15.     }  
  16. }  

Here we gave ViewModel object to BindingContext to the view. And BindingContext will help to get or set an object that contains the properties that will be targeted by the bound properties that belong to this BindingContext.

HomePageViewModel.cs

In this, we need to create the following properties and commands.

Properties

  • RibbonOptions(for tabs list).
  • LoadURI(for dynamic WebView URL based on tab selection).
  • IsBusy(for ActivityIndicator).

 Commands

  • OptionSelectionChangedCommand(for tab selection)

  1. using System;  
  2. using System.Collections;  
  3. using System.Collections.Generic;  
  4. using System.ComponentModel;  
  5. using System.Runtime.CompilerServices;  
  6. using System.Windows.Input;  
  7. using Xamarin.Forms;  
  8. namespace CustomTabbedPage.ViewModels {  
  9.     public class HomePageViewModel: INotifyPropertyChanged {  
  10.         private IList _ribbonOptions;  
  11.         public IList RibbonOptions {  
  12.             get {  
  13.                 return _ribbonOptions;  
  14.             }  
  15.             set {  
  16.                 SetProperty(ref _ribbonOptions, value);  
  17.             }  
  18.         }  
  19.         private string _loadURI;  
  20.         public string LoadURI {  
  21.             get {  
  22.                 return _loadURI;  
  23.             }  
  24.             set {  
  25.                 SetProperty(ref _loadURI, value);  
  26.             }  
  27.         }  
  28.         private bool _isBusy;  
  29.         public bool IsBusy {  
  30.             get {  
  31.                 return _isBusy;  
  32.             }  
  33.             set {  
  34.                 if (_isBusy == value) return;  
  35.                 _isBusy = value;  
  36.                 OnPropertyChanged("IsBusy");  
  37.             }  
  38.         }  
  39.         private ICommand _optionSelectionChangedCommand;  
  40.         public ICommand OptionSelectionChangedCommand {  
  41.             get {  
  42.                 return _optionSelectionChangedCommand;  
  43.             }  
  44.             set {  
  45.                 SetProperty(ref _optionSelectionChangedCommand, value);  
  46.             }  
  47.         }  
  48.               protected bool SetProperty<T>(ref T backingStore, T value, [CallerMemberName] string propertyName = "", Action onChanged = null) {  
  49.             if (EqualityComparer<T>.Default.Equals(backingStore, value)) 
  50.                  return false;  
  51.             backingStore = value;  
  52.             onChanged?.Invoke();  
  53.             OnPropertyChanged(propertyName);  
  54.             return true;  
  55.         }  
  56.         public event PropertyChangedEventHandler PropertyChanged;  
  57.         protected void OnPropertyChanged([CallerMemberName] string propertyName = "") {  
  58.             var changed = PropertyChanged;  
  59.             if (changed == null
  60.                  return;  
  61.             changed.Invoke(thisnew PropertyChangedEventArgs(propertyName));  
  62.         }  
  63.         public HomePageViewModel() {  
  64.             List<String> lst = new List<String>() {  
  65.                 "C# Corner",  
  66.                 "Xamarin",  
  67.                 "Microsoft"  
  68.             };  
  69.             this.RibbonOptions = lst;  
  70.             LoadURI = "https://www.c-sharpcorner.com/resources/aboutus.aspx";  
  71.             IsBusy = true;  
  72.             this.OptionSelectionChangedCommand = new Command((obj) => {  
  73.                 var selectedItemRibbonIndex = obj.ToString();  
  74.                 IsBusy = true;  
  75.                 if (selectedItemRibbonIndex == "0") {  
  76.                     LoadURI = "https://www.c-sharpcorner.com/resources/aboutus.aspx";  
  77.                 } else if (selectedItemRibbonIndex == "1") {  
  78.                     LoadURI = "https://docs.microsoft.com/en-us/xamarin/xamarin-forms/";  
  79.                 } else {  
  80.                     LoadURI = "https://www.microsoft.com/en-in?SilentAuth=1&wa=wsignin1.0";  
  81.                 }  
  82.             });  
  83.         }  
  84.     }  
  85. }  

In HomeViewModel constructor we are assigning items to listTabs; it will return the number of tabs, so, however many tabs we want for displaying those items, we need to add to listTabs.

Note
In OptionSelectionChangedcommand we will get the SelectedTab index and based on that index we need to change the content of the detail area for each tab. In this article, we are changing the Web Source. For example, if we want to display the listview with a different source for each tab in that case just change the ItemsSource based on selected item index. And one more example is if want to display different UI for each tab just make a design with StackLayout and based on tab selection index, with the help of IsVisible Binding for layout we can display different UI for each tab.

Output:

Android

Xamarin

iOS

Xamarin

Please, download the sample from here.


Similar Articles