Search code examples
xamarinmvvmcrossuwp

How to implement a custom presenter in a Windows UWP (Xamarin, MvvmCross)


I have the following code in my Android app, it basically uses one page (using a NavigationDrawer) and swaps fragments in/out of the central view. This allows the navigation to occur on one page instead of many pages:

Setup.cs:

    protected override IMvxAndroidViewPresenter CreateViewPresenter()
    {
        var customPresenter = new MvxFragmentsPresenter();
        Mvx.RegisterSingleton<IMvxFragmentsPresenter>(customPresenter);
        return customPresenter;
    }

ShellPage.cs

    public class ShellPage : MvxCachingFragmentCompatActivity<ShellPageViewModel>, IMvxFragmentHost
    {
        .
        .
        .

        public bool Show(MvxViewModelRequest request, Bundle bundle)
        {
            if (request.ViewModelType == typeof(MenuContentViewModel))
            {
                ShowFragment(request.ViewModelType.Name, Resource.Id.navigation_frame, bundle);
                return true;
            }
            else
            {
                ShowFragment(request.ViewModelType.Name, Resource.Id.content_frame, bundle, true);
                return true;
            }
        }

        public bool Close(IMvxViewModel viewModel)
        {
            CloseFragment(viewModel.GetType().Name, Resource.Id.content_frame);
            return true;
        }

        .
        .
        .
    }

How can I achieve the same behavior in a Windows UWP app? Or rather, is there ANY example that exists for a Windows MvvmCross app which implements a CustomPresenter? That may at least give me a start as to how to implement it.

Thanks!

UPDATE:

I'm finally starting to figure out how to go about this with a customer presenter:

    public class CustomPresenter : IMvxWindowsViewPresenter
    {
        IMvxWindowsFrame _rootFrame;

        public CustomPresenter(IMvxWindowsFrame rootFrame)
        {
            _rootFrame = rootFrame;
        }

        public void AddPresentationHintHandler<THint>(Func<THint, bool> action) where THint : MvxPresentationHint
        {
            throw new NotImplementedException();
        }

        public void ChangePresentation(MvxPresentationHint hint)
        {
            throw new NotImplementedException();
        }

        public void Show(MvxViewModelRequest request)
        {
            if (request.ViewModelType == typeof(ShellPageViewModel))
            {
                //_rootFrame?.Navigate(typeof(ShellPage), null);    // throws an exception

                ((Frame)_rootFrame.UnderlyingControl).Content = new ShellPage();
            }
        }
    }

When I try to do a navigation to the ShellPage, it fails. So when I set the Content to the ShellPage it works, but the ShellPage's ViewModel is not initialized automatically when I do it that way. I'm guessing ViewModels are initialized in MvvmCross using OnNavigatedTo ???


Solution

  • I ran into the same issue, and built a custom presenter for UWP. It loans a couple of ideas from an Android sample I found somewhere, which uses fragments. The idea is as follows.

    I have a container view which can contain multiple sub-views with their own ViewModels. So I want to be able to present multiple views within the container.

    Note: I'm using MvvmCross 4.0.0-beta3

    Presenter

    using System;
    using Cirrious.CrossCore;
    using Cirrious.CrossCore.Exceptions;
    using Cirrious.MvvmCross.ViewModels;
    using Cirrious.MvvmCross.Views;
    using Cirrious.MvvmCross.WindowsUWP.Views;
    using xxxxx.WinUniversal.Extensions;
    
    namespace xxxxx.WinUniversal.Presenters
    {
        public class MvxWindowsMultiRegionViewPresenter
            : MvxWindowsViewPresenter
        {
            private readonly IMvxWindowsFrame _rootFrame;
    
            public MvxWindowsMultiRegionViewPresenter(IMvxWindowsFrame rootFrame)
                : base(rootFrame)
            {
                _rootFrame = rootFrame;
            }
    
            public override async void Show(MvxViewModelRequest request)
            {
                var host = _rootFrame.Content as IMvxMultiRegionHost;
                var view = CreateView(request);
    
                if (host != null && view.HasRegionAttribute())
                {
                    host.Show(view as MvxWindowsPage);
                }
                else
                {
                    base.Show(request);
                }
            }
    
            private static IMvxWindowsView CreateView(MvxViewModelRequest request)
            {
                var viewFinder = Mvx.Resolve<IMvxViewsContainer>();
    
                var viewType = viewFinder.GetViewType(request.ViewModelType);
                if (viewType == null)
                    throw new MvxException("View Type not found for " + request.ViewModelType);
    
                // Create instance of view
                var viewObject = Activator.CreateInstance(viewType);
                if (viewObject == null)
                    throw new MvxException("View not loaded for " + viewType);
    
                var view = viewObject as IMvxWindowsView;
                if (view == null)
                    throw new MvxException("Loaded View is not a IMvxWindowsView " + viewType);
    
                view.ViewModel = LoadViewModel(request);
    
                return view;
            }
    
            private static IMvxViewModel LoadViewModel(MvxViewModelRequest request)
            {
                // Load the viewModel
                var viewModelLoader = Mvx.Resolve<IMvxViewModelLoader>();
    
                return viewModelLoader.LoadViewModel(request, null);
            }
        }
    }
    

    IMvxMultiRegionHost

    using Cirrious.MvvmCross.ViewModels;
    using Cirrious.MvvmCross.WindowsUWP.Views;
    
    namespace xxxxx.WinUniversal.Presenters
    {
        public interface IMvxMultiRegionHost
        {
            void Show(MvxWindowsPage view);
    
            void CloseViewModel(IMvxViewModel viewModel);
    
            void CloseAll();
        }
    }
    

    RegionAttribute

    using System;
    
    namespace xxxxx.WinUniversal.Presenters
    {
        [AttributeUsage(AttributeTargets.Class)]
        public sealed class RegionAttribute
            : Attribute
        {
            public RegionAttribute(string regionName)
            {
                Name = regionName;
            }
    
            public string Name { get; private set; }
        }
    }
    

    These are the three foundational classes you need. Next you'll need to implement the IMvxMultiRegionHost in a MvxWindowsPage derived class.

    This is the one I'm using:

    HomeView.xaml.cs

    using System;
    using System.Diagnostics;
    using System.Linq;
    using Windows.Foundation;
    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Navigation;
    using Cirrious.MvvmCross.ViewModels;
    using Cirrious.MvvmCross.WindowsUWP.Views;
    using xxxxx.Shared.Controls;
    using xxxxx.WinUniversal.Extensions;
    using xxxxx.WinUniversal.Presenters;
    using xxxxx.Core.ViewModels;
    
    namespace xxxxx.WinUniversal.Views
    {
        public partial class HomeView
            : MvxWindowsPage
            , IMvxMultiRegionHost
        {
            public HomeView()
            {
                InitializeComponent();
            }
    
            // ...
    
            public void Show(MvxWindowsPage view)
            {
                if (!view.HasRegionAttribute())
                    throw new InvalidOperationException(
                        "View was expected to have a RegionAttribute, but none was specified.");
    
                var regionName = view.GetRegionName();
    
                RootSplitView.Content = view;
            }
    
            public void CloseViewModel(IMvxViewModel viewModel)
            {
                throw new NotImplementedException();
            }
    
            public void CloseAll()
            {
                throw new NotImplementedException();
            }
        }
    }
    

    The last piece to make this work is the way the actual xaml in the view is set-up. You'll notice that I'm using a SplitView control, and that I'm replacing the Content property with the new View that's coming in in the ShowView method on the HomeView class.

    HomeView.xaml

    <SplitView x:Name="RootSplitView"
               DisplayMode="CompactInline"
               IsPaneOpen="false"
               CompactPaneLength="48"
               OpenPaneLength="200">
        <SplitView.Pane>
            // Some ListView with menu items.
        </SplitView.Pane>
        <SplitView.Content>
            // Initial content..
        </SplitView.Content>
    </SplitView>
    

    EDIT:

    Extension Methods

    I forgot to post the two extension methods to determine if the view declares a [Region] attribute.

    public static class RegionAttributeExtentionMethods
    {
        public static bool HasRegionAttribute(this IMvxWindowsView view)
        {
            var attributes = view
                .GetType()
                .GetCustomAttributes(typeof(RegionAttribute), true);
    
            return attributes.Any();
        }
    
        public static string GetRegionName(this IMvxWindowsView view)
        {
            var attributes = view
                .GetType()
                .GetCustomAttributes(typeof(RegionAttribute), true);
    
            if (!attributes.Any())
                throw new InvalidOperationException("The IMvxView has no region attribute.");
    
            return ((RegionAttribute)attributes.First()).Name;
        }
    }
    

    Hope this helps.