5
votes

Let's say we have MvvmCross 6.0.1 native app with one Android Activity containing BottomNavigationView implemented as in this blog post by James Montemagno but without navigating and replacing fragments.

What I would like to do is to bind BottomNavigationView items to MvxCommands (or MvxAsyncCommands) in ViewModel in order to navigate between several ViewModels.

What kind of architecture should I apply to achieve this? Is my approach correct or am I doing something against MVVM pattern and MvvmCross possibilities?

Full working example with several additions can be found here on github.

animated gif showing current progress

At the moment I have (scaffolded with MvxScaffolding).

  • MainContainerActivity and corresponding MainContainerViewModel - here I would like to store commands to navigate between view models
  • MainFragment and corresponding MainViewModel - this is the first fragment/view model
  • SettingsFragment and corresponding SettingsViewModel - I would like to navigate to it from MainViewModel and vice versa
  • FavoritesFragment and corresponding FavoritesViewModel

The main activity is as follows:

using Android.App;
using Android.OS;
using Android.Views;
using PushNotifTest.Core.ViewModels.Main;
using Microsoft.AppCenter;
using Microsoft.AppCenter.Analytics;
using Microsoft.AppCenter.Crashes;
using Microsoft.AppCenter.Push;
using Android.Graphics.Drawables;
using Android.Support.Design.Widget;
using MvvmCross.Binding.BindingContext;
using System;
using System.Windows.Input;

namespace PushNotifTest.Droid.Views.Main
{
    [Activity(
        Theme = "@style/AppTheme",
        WindowSoftInputMode = SoftInput.AdjustResize | SoftInput.StateHidden)]
    public class MainContainerActivity : BaseActivity<MainContainerViewModel>
    {
        protected override int ActivityLayoutId => Resource.Layout.activity_main_container;

        BottomNavigationView bottomNavigation;

        public ICommand GoToSettingsCommand { get; set; }
        public ICommand GoToFavoritesCommand { get; set; }
        public ICommand GoToHomeCommand { get; set; }

        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate();
            AddBottomNavigation();
        }

        private void AddBottomNavigation()
        {
            bottomNavigation = (BottomNavigationView)FindViewById(Resource.Id.bottom_navigation);
            if (bottomNavigation != null)
            {
                bottomNavigation.NavigationItemSelected += BottomNavigation_NavigationItemSelected;
                // trying to bind command to view model property
                var set = this.CreateBindingSet<MainContainerActivity, MainContainerViewModel>();
                set.Bind(this).For(v => v.GoToSettingsCommand).To(vm => vm.NavigateToSettingsCommand);
                set.Bind(this).For(v => v.GoToHomeCommand).To(vm => vm.NavigateToHomeCommand);
                set.Bind(this).For(v => v.GoToFavoritesCommand).To(vm => vm.NavigateToFavoritesCommand);
                set.Apply();
            }
            else
            {
                System.Diagnostics.Debug.WriteLine("Bottom navigation menu is null");
            }
        }

        private void BottomNavigation_NavigationItemSelected(object sender, BottomNavigationView.NavigationItemSelectedEventArgs e)
        {
            try
            {
                System.Diagnostics.Debug.WriteLine($"Bottom navigation menu is selected: {e.Item.ItemId}");

                if (e.Item.ItemId == Resource.Id.menu_settings)
                    if (GoToSettingsCommand != null && GoToSettingsCommand.CanExecute(null))
                        GoToSettingsCommand.Execute(null);
                if (e.Item.ItemId == Resource.Id.menu_list)
                    if (GoToFavoritesCommand != null && GoToFavoritesCommand.CanExecute(null))
                        GoToFavoritesCommand.Execute(null);
                if (e.Item.ItemId == Resource.Id.menu_home)
                    if (GoToHomeCommand != null && GoToHomeCommand.CanExecute(null))
                        GoToHomeCommand.Execute(null);
            }
            catch (Exception exception)
            {
                System.Diagnostics.Debug.WriteLine($"Exception: {exception.Message}");
                Crashes.TrackError(exception);
            }
        }
    }
}

The bottom navigation elements are:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
  <item
      android:id="@+id/menu_home"
      android:enabled="true"
      android:icon="@drawable/ic_history"
      android:title="@string/tab1_title"
      app:showAsAction="ifRoom" />

  <item
      android:id="@+id/menu_list"
      android:enabled="true"
      android:icon="@drawable/ic_list"
      android:title="@string/tab2_title"
      app:showAsAction="ifRoom" />

  <item
      android:id="@+id/menu_settings"
      android:enabled="true"
      android:icon="@drawable/ic_settings"
      android:title="@string/tab3_title"
      app:showAsAction="ifRoom" />
</menu>

And the commands in view model are just:

public IMvxAsyncCommand NavigateToSettingsCommand => new MvxAsyncCommand(async () => await _navigationService.Navigate<SettingsViewModel>());
public IMvxAsyncCommand NavigateToFavoritesCommand => new MvxAsyncCommand(async () => await _navigationService.Navigate<FavoritesViewModel>());
public IMvxAsyncCommand NavigateToHomeCommand => new MvxAsyncCommand(async () => await _navigationService.Navigate<MainViewModel>());
1
Hello, I am not sure what your problem is.Robbit
I'd like to get some comment on my approach - it seems that there's too much code to perform just simple navigation. Seems that it's against some basic rules and it's hard to maintain. If you can share some better approach I would be more than grateful.Dominik Roszkowski

1 Answers

0
votes

Instead of using Fluent Binding, you could create a targeted binding for the BottomNavigationView and handle the navigation in your MainViewModel. Use Swiss binding in your XML.

TargetBinding Class:

public class MvxBottomNavigationItemChangedBinding : MvxAndroidTargetBinding
    {
        readonly BottomNavigationView _bottomNav;
        IMvxCommand _command;

        public override MvxBindingMode DefaultMode => MvxBindingMode.TwoWay;
        public override Type TargetType => typeof(MvxCommand);

        public MvxBottomNavigationItemChangedBinding(BottomNavigationView bottomNav) : base(bottomNav)
        {
            _bottomNav = bottomNav;
            _bottomNav.NavigationItemSelected += OnNavigationItemSelected;
        }

        public override void SetValue(object value)
        {
            _command = (IMvxCommand)value;
        }

        protected override void SetValueImpl(object target, object value)
        {

        }

        void OnNavigationItemSelected(object sender, BottomNavigationView.NavigationItemSelectedEventArgs e)
        {
            if (_command != null)
                _command.Execute(e.Item.TitleCondensedFormatted.ToString());
        }

        protected override void Dispose(bool isDisposing)
        {
            if (isDisposing)
                _bottomNav.NavigationItemSelected -= OnNavigationItemSelected;

            base.Dispose(isDisposing);
        }
    }

Setup.cs :

protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
        {
            MvxAppCompatSetupHelper.FillTargetFactories(registry);
            base.FillTargetFactories(registry);
            registry.RegisterCustomBindingFactory<BottomNavigationView>("BottomNavigationSelectedBindingKey",
                                                                        view => new MvxBottomNavigationItemChangedBinding(view));

        }

BottomNavigationView XML :

Note the target binding key that we added in Setup.cs is used when binding.

<com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/navigation"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/white"
            app:labelVisibilityMode="labeled"
            app:menu="@menu/bottom_nav_menu"
            app:elevation="10dp"
            local:MvxBind="BottomNavigationSelectedBindingKey BottomNavigationItemSelectedCommand"/>

MainViewModel :

public class MainViewModel : BaseViewModel
{
public IMvxCommand<string> BottomNavigationItemSelectedCommand { get; private set; }

List<TabViewModel> _tabs;
public List<TabViewModel> Tabs
{
    get => _tabs;
    set => SetProperty(ref _tabs, value);
}

public MainViewModel(IMvxNavigationService navigationService) : base(navigationService)
{
    //these are for android - start
    BottomNavigationItemSelectedCommand = new MvxCommand<string>(BottomNavigationItemSelected);

    var tabs = new List<TabViewModel>
    {
        Mvx.IoCProvider.IoCConstruct<FirstViewModel>(),
        Mvx.IoCProvider.IoCConstruct<SecondViewModel>(),
        Mvx.IoCProvider.IoCConstruct<ThirdViewModel>()
    };

    Tabs = tabs;
    //end
}

// Android-only, not used on iOS
private void BottomNavigationItemSelected(string tabId)
{
    if (tabId == null)
    {
        return;
    }

    foreach (var item in Tabs)
    {
        if (tabId == item.TabId)
        {
            _navigationService.Navigate(item);
            break;
        }
    }
}
}

TabViewModel :

public class TabViewModel : BaseViewModel
    {
        public string TabName { get; protected set; }
        public string TabId { get; protected set; }

        public TabViewModel(IMvxNavigationService navigationService) : base(navigationService)
        {
           
        }
    }

BottomNavigation Elements:

Add "android:titleCondensed", which will be used as the Id.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
  <item
      android:id="@+id/menu_home"
      android:enabled="true"
      android:icon="@drawable/ic_history"
      android:title="@string/tab1_title"
      android:titleCondensed ="tab_first"
      app:showAsAction="ifRoom" />

  <item
      android:id="@+id/menu_list"
      android:enabled="true"
      android:icon="@drawable/ic_list"
      android:title="@string/tab2_title"
      android:titleCondensed ="tab_second"
      app:showAsAction="ifRoom" />

  <item
      android:id="@+id/menu_settings"
      android:enabled="true"
      android:icon="@drawable/ic_settings"
      android:title="@string/tab3_title"
      android:titleCondensed ="tab_third"
      app:showAsAction="ifRoom" />
</menu>

ViewModel Examples:

public class FirstViewModel : TabViewModel
{
    public FirstViewModel(IMvxNavigationService navigationService) : base(navigationService)
        {
            TabId = "tab_first";
        }
}

public class SecondViewModel : TabViewModel
{
    public SecondViewModel(IMvxNavigationService navigationService) : base(navigationService)
        {
            TabId = "tab_second";
        }
}

Hope this helps someone else who comes into this later on ! :)