Extending Prism's NavigationService with your own business logic

Tags: UWP, Prism

In the previous post, Using business logic to define your app’s startup workflow, we defined some custom business logic to redefine the startup flow of our application. In this post, we’ll extend Prism’s navigation logic to encapsulate the Queue, taking away some required backstack clear calls from the developer implementing the pages. This to lower possible human errors leaving back buttons in the startup sequence, and of course showing how to inject your own NavigationService in Prism.

We start with the same setup as the previous post: three pages with fixed navigation between them. And then it’s time to start digging in the navigation of Prism, luckily this code is open source. There are quite a few types in the Prism.Windows.Navigation namespace, but these are the ones we’re interested in:

  • INavigationService: the contract of all navigation in Prism.
  • FrameNavigationService: implementation of INavigationService, using the IFrameFacade abstraction.
  • IFrameFacade and FrameFacadeAdapter: abstraction layer on top of the Frame object.

As Prism is completely open-source, nothing keeps you from forking the repository, extending the above interfaces and classes, and add it to your project as source or as a self-made NuGet package. But for simplicity (or maybe complexity), I’m not going that far and will extend the interface (giving us some minor issues on the way though).

Writing your own NavigationService

The first step is extending the interface. If you can stay away from this and only have to change the actual implementation of FrameNavigationService, you’ll have less issues integrating it in your app. But we’ll come back to that.

// Note: this is an extension of Prism's INavigationService
// https://github.com/PrismLibrary/Prism

using Prism.Windows.Navigation;

namespace StartupSequenceNavigationService.Services
{
    public interface INavigationServiceWithBootSequence : INavigationService
    {
        /// <summary>
        /// Returns if the application is currently executing the boot sequence
        /// </summary>
        bool InBootSequence { get; }

        /// <summary>
        /// Add another page to the end of the boot sequence queue
        /// </summary>
        /// <param name="pageToken"></param>
        /// <param name="parameter"></param>
        void AddToBootSequence(string pageToken, object parameter);

        /// <summary>
        /// Execute the next navigation step in the boot sequence
        /// </summary>
        /// <returns></returns>
        bool ContinueBootSequence();
    }
}

I added a BootSequence property in case you’d like to check whether you’re still in the startup flow. This might be handy if some of your pages are also used during the regular flow of your app and have a back button that shouldn’t show during the startup.

Instead of returning the page token back to the app/page for navigation, it’s kept in the NavigationService implementation to do the navigation. For this to work correctly, the AddToBootSequence method not only passes the page token but also a navigation parameter. Note that you can push pages on the queue within your flow if necessary instead of all at once at the beginning, so this keeps open the possibility of using the correct navigation parameters if necessary.

An interface without an implementation is worth nothing, so here’s the extension of the default FrameNavigationService.

// Note: this is an extension of Prism's FrameNavigationService
// https://github.com/PrismLibrary/Prism

using System;
using System.Collections.Generic;
using Prism.Windows.AppModel;
using Prism.Windows.Navigation;

namespace StartupSequenceNavigationService.Services
{
    public class FrameNavigationServiceWithBootSequence : FrameNavigationService, INavigationServiceWithBootSequence
    {
        private static readonly Queue<SequenceItem> BootSequence = new Queue<SequenceItem>();

        /// <summary>
        /// Initializes a new instance of the <see cref="FrameNavigationServiceWithBootSequence"/> class.
        /// </summary>
        /// <param name="frame">The frame.</param>
        /// <param name="navigationResolver">The navigation resolver.</param>
        /// <param name="sessionStateService">The session state service.</param>
        public FrameNavigationServiceWithBootSequence(IFrameFacade frame, Func<string, Type> navigationResolver, ISessionStateService sessionStateService)
            : base(frame, navigationResolver, sessionStateService)
        {
        }

        /// <summary>
        /// Returns if the application is currently executing the boot sequence
        /// </summary>
        public bool InBootSequence { get; private set; }

        /// <summary>
        /// Add another page to the end of the boot sequence queue
        /// </summary>
        public void AddToBootSequence(string pageToken, object parameter)
        {
            BootSequence.Enqueue(new SequenceItem
            {
                PageToken = pageToken,
                Parameter = parameter
            });
            if (BootSequence.Count > 0)
                InBootSequence = true;
        }

        /// <summary>
        /// Execute the next navigation step in the boot sequence
        /// </summary>
        /// <returns></returns>
        public bool ContinueBootSequence()
        {
            if (InBootSequence)
            {
                SequenceItem sequenceItem = BootSequence.Dequeue();
                if (BootSequence.Count == 0)
                    InBootSequence = false;

                bool navigated = Navigate(sequenceItem.PageToken, sequenceItem.Parameter);
                RemoveLastPage();
                return navigated;
            }

            return false;
        }

        private struct SequenceItem
        {
            public string PageToken { get; set; }
            public object Parameter { get; set; }
        }
    }
}

This is basicly the StartupService from the previous post wrapped in the NavigationService implementation with following changes:

  • The Queue now uses a SequenceItem struct to store both the page token and the navigation parameter.
  • Instead of dequeuing and returning the page, we take care of the navigation and immediately clear the last added page on the stack.

Note: I’ve updated the code to only remove the last page and not all pages. This enables using the “bootsequence” flow in the middle of your application, preserving the older items on the backstack that aren’t part of the flow.

Note: The current active page is not on the backstack (stack containing pages to which you can navigate using the back button). The top page on the stack is the previous page you come from.

Integrating your own service in your app

What changes does this bring to our app? Well, here we come to our minor issue. Prism works with the INavigationService interface, which we extended with to our own needs. This means that we’ll have to cast the instance object to our own interface before being able to use the extra property and methods.

    private async Task FillNavigationQueueAsync()
    {
        // do some async tasks to check the startup logic


        var applicationSettingsService = ServiceLocator.Current.GetInstance<IApplicationSettingsService>();
        var extendedNavigationService = (INavigationServiceWithBootSequence)NavigationService;

        // step 1: check initial setup
        if (!applicationSettingsService.IsConfigured())
        {
            extendedNavigationService.AddToBootSequence(PageTokens.SetupPage, null);
        }

        // step 2: check user logged in
        if (string.IsNullOrEmpty(applicationSettingsService.GetUser()))
        {
            extendedNavigationService.AddToBootSequence(PageTokens.LoginPage, null);
        }

        // step 3: actual main page
        extendedNavigationService.AddToBootSequence(PageTokens.MainPage, null);
    }

It’s not that much of an issue, but it’s not totally clean either. The same ‘fix’ has to be applied to our viewmodels as well. Note that we’re no longer clearing the backstack in the viewmodels.

    private void OnLoginClicked()
    {
        _applicationSettingsService.Login(Username);
        _navigationService.ContinueBootSequence();
    }

This part of the job is done, but of course there’s still telling Prism to use our implementation of the FrameNavigationService. If you dig into the open-source code, you’ll notice that since Prism is using the IFrameFacade, it’s a bit more work than simply adding the type to your IoC container. In the base application class, you’ll find this code:

    /// <summary>
    /// Creates the navigation service. Use this to inject your own INavigationService implementation.
    /// </summary>
    /// <param name="rootFrame">The root frame.</param>
    /// <returns>The initialized navigation service.</returns>
    protected virtual INavigationService OnCreateNavigationService(IFrameFacade rootFrame) => null;

    /// <summary>
    /// Creates the navigation service.
    /// </summary>
    /// <param name="rootFrame">The root frame.</param>
    /// <param name="sessionStateService">The session state service.</param>
    /// <returns>The initialized navigation service.</returns>
    protected virtual INavigationService CreateNavigationService(IFrameFacade rootFrame, ISessionStateService sessionStateService)
    {
        var navigationService = OnCreateNavigationService(rootFrame) ?? new FrameNavigationService(rootFrame, GetPageType, sessionStateService);
        return navigationService;
    }

In Prism for Windows Runtime (aka Windows 8.1), this used to be completely internal, so you were ****ed. But luckily now you can override the OnCreateNavigationService method. Also don’t forget to add your type to the IoC container, so dependency injection happens correctly.

    protected override void ConfigureContainer(ContainerBuilder builder)
    {
        builder.RegisterType<ApplicationSettingsService>().As<IApplicationSettingsService>().SingleInstance();
        builder.RegisterType<FrameNavigationServiceWithBootSequence>().As<INavigationService>().SingleInstance();
        base.ConfigureContainer(builder);
    }

    /// <summary>
    /// Override the creation of the FrameNavigationService
    /// </summary>
    protected override INavigationService OnCreateNavigationService(IFrameFacade rootFrame)
    {
        return new FrameNavigationServiceWithBootSequence(rootFrame, GetPageType, SessionStateService);
    }

That’s basically all you have to do to get your own navigation logic working with Prism. If you aren’t expanding the default INavigationService, you’re in for an even easier ride.

Comments

comments powered by Disqus