In my app, I've encountered a couple of cases when it's possible to navigate to an already displayed view model:
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?
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?
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.