Search code examples
c#mvvmcrossxamarin.mac

Navigating to already existing view model in MvvmCross


In my app, I've encountered a couple of cases when it's possible to navigate to an already displayed view model:

  1. On macOS, application preferences should be displayed in a separate NSWindow that does not block or overlay other windows as it happens in UWP or on ipadOS. So, a user can open the preferences, then keep them opened (minimized or behind other windows), then use the hotkey/menu/button to open them for the second time. How can I direct Navigate<SettingsViewModel>() to the already opened view in a window instead of creating a new one?

  2. My app has a master-detail layout similar to an IDE: with an outline in a sidebar on the left and documents inside tabs on the right. A user may open some document but then click its name again in the outline instead of switching to it via its opened tab. I consider opening new document tabs via Navigate<DocViewModel, DocPathParam>(docPathParam), but how can I catch an already opened one in this case?

Or should I avoid calling Navigate() methods in both cases and instead detect opened windows and tabs from the view layer of a specific platform?


Solution

  • After examining the internal calls of MvvmCross, I concluded that trying to "inject" into its navigation stack would be too complicated, and I'd rather resolve both problems on my side.

    For the first case, I created a small "View Director" class that I use as a proxy for navigation to the single-instance windows (like the app's settings). While this approach breaks the "navigation from VM" principle of MvvmCross, I think it is fine because windowing behavior is platform-specific anyway.

    When the View Director gets a navigation request, it checks for the custom UniqueWindowPresentation attribute and then asks my custom View Presenter to focus in a previously opened window of the requested view model. If the Presenter can't find a window like that, the regular MvvmCross navigation happens (and eventually creates the window).

    public class MvxMacViewDirector : IMvxMacViewDirector
    {
        readonly IMvxViewsContainer _viewContainer;
        readonly IMvxAltMacViewPresenter _viewPresenter;
        readonly IMvxNavigationService _navigationService;
    
        public MvxMacViewDirector()
        {
            _viewContainer = Mvx.IoCProvider.Resolve<IMvxViewsContainer>();
            _viewPresenter = (IMvxAltMacViewPresenter)Mvx.IoCProvider.Resolve<IMvxViewPresenter>();
            _navigationService = Mvx.IoCProvider.Resolve<IMvxNavigationService>();
        }
    
        public void ShowView<TViewModel>() where TViewModel: MvxViewModel
        {
            Type viewType = _viewContainer.GetViewType(typeof(TViewModel));
    
            if (viewType.GetCustomAttribute<UniqueWindowPresentationAttribute>() != null)
            {
                if (_viewPresenter.ShowPreviouslyOpenedWindow<TViewModel>() == false)
                    _navigationService.Navigate<TViewModel>();
            }
            else
                _navigationService.Navigate<TViewModel>();
        }
    }
    

    The method in the custom View Presenter looks like this:

    public bool ShowPreviouslyOpenedWindow<T>() where T : MvxViewModel
    {
        foreach (var item in Windows)
        {
            if (item.ContentViewController is MvxViewController viewController &&
                viewController.ViewModel.GetType() == typeof(T))
            {
                item.MakeKeyAndOrderFront(null);
                return true;
            }
        }
    
        return false;
    }
    

    The second case really made me think about the appropriate use cases of MvvmCross-based navigation. In the end, I decided that it should not be in charge of displaying nested views/VMs (like tabs inside a "detail" part of a split view) because this behavior is too content-dependent to generalize it in a View Presenter. Instead, I manage switching and creation of tabs directly from their parent pane's view model: PaneViewModel creates tab VMs via the IMvxViewModelLoader, meanwhile PaneView observes its VM's collection of tabs and creates corresponding views via IMvxMacViewCreator, and then assigns VMs to them. This is very similar to what MvvmCross does internally to instantiate view+VM pairs.

    So, I use MvvmCross navigation only for the "root view" cases where the whole content of a window or a "detail" part of a split view needs to be replaced. Or when I need to show a dialog/sheet overlay on top. Everything nested inside those views is coordinated from inside their view models.