Search code examples
c#xamarin.androidtabsmvvmcrossandroid-appcompat

How to set Title of Tab with MVVMCross 6.2?


I'm trying to figure out how to properly set the localized string as Tab Title (or dynamically change the Tab Title) with MVVMCross 6.2+ in Xamarin Android. How should I set the title of the tab in the simple example app? Thanks in advance for your help.

Here is the simple example app:

MvvmCrossTabs.Core

HomeViewModel.cs

using System.Collections.Generic;
using System.Threading.Tasks;
using MvvmCross.Commands;
using MvvmCross.Logging;
using MvvmCross.Navigation;
using MvvmCross.ViewModels;

namespace MvvmCrossTabs.Core.ViewModels
{
    public class HomeViewModel : MvxNavigationViewModel
    {
        public IMvxAsyncCommand ShowInitialViewModelsCommand { get; private set; }

        public HomeViewModel(IMvxLogProvider logProvider, IMvxNavigationService navigationService) : base(logProvider, navigationService)
        {
            ShowInitialViewModelsCommand = new MvxAsyncCommand(ShowInitialViewModels);
        }

        private async Task ShowInitialViewModels()
        {
            await Task.WhenAll(new List<Task>
            {
                NavigationService.Navigate<Tab1ViewModel>(),
                NavigationService.Navigate<Tab2ViewModel>(),
                NavigationService.Navigate<Tab3ViewModel>()
            });
        }
    }
}

Tab1ViewModel.cs (Tab2ViewModel.cs, Tab3ViewModel.cs)

using MvvmCross.Logging;
using MvvmCross.Navigation;
using MvvmCross.ViewModels;

namespace MvvmCrossTabs.Core.ViewModels
{
    public class Tab1ViewModel : MvxNavigationViewModel
    {
        public Tab1ViewModel(IMvxLogProvider logProvider, IMvxNavigationService navigationService) : base(logProvider, navigationService)
        {

        }
    }
}

App.cs

using MvvmCross.IoC;
using MvvmCross.ViewModels;
using MvvmCrossTabs.Core.ViewModels;

namespace MvvmCrossTabs.Core
{
    public class App : MvxApplication
    {
        public override void Initialize()
        {
            CreatableTypes()
                .EndingWith("Service")
                .AsInterfaces()
                .RegisterAsLazySingleton();

            RegisterAppStart<HomeViewModel>();
        }
    }
}

MvvmCrossTabs.Android

MainApplication.cs

using System;
using Android.App;
using Android.Runtime;
using MvvmCross.Droid.Support.V7.AppCompat;
using MvvmCrossTabs.Core;

namespace MvvmCrossTabs.Android
{
    [Application]
    public class MainApplication : MvxAppCompatApplication<MvxAppCompatSetup<App>, App>
    {
        public MainApplication() : base() { }

        public MainApplication(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { }
    }
}

home.axml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/maincontent"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_scrollFlags="scroll|enterAlways" />

        <android.support.design.widget.TabLayout
            android:id="@+id/tabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_scrollFlags="enterAlways"
            app:tabGravity="fill"
            app:tabMaxWidth="0dp" />

    </android.support.design.widget.AppBarLayout>

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</android.support.design.widget.CoordinatorLayout>

tab1.axml (tab2.axml, tab3.axml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:local="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main_frame"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

</LinearLayout>

styles.xml

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
</resources>

HomeView.cs

using Android.App;
using Android.Content.PM;
using Android.OS;
using Android.Support.V7.Widget;
using MvvmCross.Droid.Support.V7.AppCompat;
using MvvmCross.Platforms.Android.Presenters.Attributes;
using MvvmCrossTabs.Core.ViewModels;

namespace MvvmCrossTabs.Android
{
    [Activity(Label = "@string/app_name", LaunchMode = LaunchMode.SingleTask, Theme = "@style/AppTheme", MainLauncher = true)]
    [MvxActivityPresentation]
    public class HomeView : MvxAppCompatActivity<HomeViewModel>
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            SetContentView(Resource.Layout.home);

            // Replaces Action Bar with new Toolbar.
            var toolbar = FindViewById<Toolbar>(Resource.Id.toolbar);
            SetSupportActionBar(toolbar);

            ViewModel.ShowInitialViewModelsCommand.Execute();
        }
    }
}

Tab1View.cs (Tab2View.cs, Tab3View.cs)

using Android.OS;
using Android.Runtime;
using Android.Views;
using MvvmCross.Droid.Support.V4;
using MvvmCross.Platforms.Android.Binding.BindingContext;
using MvvmCross.Platforms.Android.Presenters.Attributes;
using MvvmCrossTabs.Core.ViewModels;

namespace MvvmCrossTabs.Android.Views
{
    [MvxTabLayoutPresentation(TabLayoutResourceId = Resource.Id.tabs, ViewPagerResourceId = Resource.Id.viewpager, Title = "Tab 1", ActivityHostViewModelType = typeof(HomeViewModel))]
    [Register(nameof(Tab1View))]
    public class Tab1View : MvxFragment<Tab1ViewModel>
    {
        public override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);

            // Create your fragment here
        }

        public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
        {
            base.OnCreateView(inflater, container, savedInstanceState);

            return this.BindingInflate(Resource.Layout.tab1, null);
        }
    }
}

Solution

  • When your tabs are created using the ShowInitialViewModelsCommand, the built-in presenter creates a list of MvxViewPagerFragmentInfo objects, passing in the Title value from the attributes. You can see that happening in the ShowViewPagerFragment method in the MvvmCross source code.

    The list of MvxViewPagerFragmentInfo objects is then passed into the MvxCachingFragmentStatePagerAdapter that is created for the ViewPager. You can see that happening here

    MvxCachingFragmentStatePagerAdapter inherits from the MvxFragmentPagerAdapter class. Inside the MvxFragmentPagerAdapter class is finally where the Title you provided is actually used. You can see it being used in the GetPageTitleFormatted method here

    So in order to change the Title at runtime, you could do the following:

    Subclass the default presenter and override the ShowViewPagerFragment method (it is marked virtual) and provide the correct localized title string instead of the one defined in the attribute

    Here is an example of how to accomplish this:

    1.) Create a custom presenter and override ShowViewPagerFragment

    public class LocalizedTabPresenter : MvxAppCompatViewPresenter
    {
        public LocalizedTabPresenter(IEnumerable<Assembly> androidViewAssemblies) : base(androidViewAssemblies)
        {
        }
    
        protected override Task<bool> ShowViewPagerFragment(Type view, MvxViewPagerFragmentPresentationAttribute attribute, MvxViewModelRequest request)
        {
            if (attribute.ViewModelType == typeof(Tab1ViewModel)) { 
                attribute.Title = "My Localized Title for Tab 1"
            }
    
            return base.ShowViewPagerFragment(view, attribute, request);
        }
    }
    

    2.) In your Setup.cs class, let MvvmCross know to use the custom presenter instead

    protected override IMvxAndroidViewPresenter CreateViewPresenter()
    {
        return new LocalizedTabPresenter(AndroidViewAssemblies);
    }
    

    Note:

    • This will only work if you need to set the title only once when the app launches and is setting up the tabs for the first time.

    If you are in a situation where the Title can change multiple times while the app is running, you need to subclass MvxCachingFragmentStatePagerAdapter and override the GetPageTitleFormatted method and provide a more custom implementation that fits your use case.

    Hope that helps.