Workflow Controller in Xamarin.Forms

One of the main issues I am seeing with large projects, is controlling the workflow of pages. The complexity continues to increase as more pages can link to and from it. It also occurred to me that keeping navigation inside the ViewModel, only seems to tie the ViewModel to other pages, rather than keeping it isolated and reusable, as intended.

Note: This is just me playing around again. Nothing production ready is in this post.

Controller

Instead of injecting a NavigationService into each ViewModel, you can inject a controller. The controller has 2 methods, Complete and Cancel. The ViewModel either completed successfully, or you want to cancel.

public interface IController
{
    Task Complete(Result result);
    Task Cancel();
}

The Complete method, accepts a Result parameter, that includes a StateId and Parameter, that you might want to pass.

public class Result
{
    public int StateId { get; set; } = 0; // Optional StateId
    public object Parameter { get; set; } // Optional Data you want sending
}

Mapping

To manage the mapping and implementation of the workflow, I have a simple struct that holds a StateId and Page type.

struct Map
{
    public static Map Create(int stateId, Type page)
    {
        return new Map()
        {
            StateId = stateId,
            Page = page
        };
    }
    public int StateId;
    public Type Page;
}

Here we actually map the workflow, showing which stateId, and source Page Type, maps to which page.

IDictionary<Map, Type> _workflow = new Dictionary<Map, Type>();

void MapWorkflow()
{
    _workflow.Add(Map.Create(0, typeof(MainPage)), typeof(SecondPage));
    _workflow.Add(Map.Create(1, typeof(MainPage)), typeof(ThirdPage));
}

This is just the simple implementation of the Complete and Cancel methods.

public async Task Complete(Result result)
{
    var toPage = _workflow.First(x => x.Key.StateId == result.StateId && x.Key.Page == _navigationService.CurrentPage).Value;
    await _navigationService.Push((Page)Activator.CreateInstance(toPage));
}

public async Task Cancel()
{
    await _navigationService.Pop();
}

NavigationService

I created a very simple navigation service, but there is no reason why you couldn’t use any existing navigation service from MvvmCross, Prism or MvvmLight.

public interface INavigationService
{
    Task Push(Page page);
    Task Pop();
    Type CurrentPage { get; }
}

I injected this into my controller, to actually perform the navigation.

Usage

Then to use the Controller, all you need to do is:

public class MainViewModel
{
    IController _controller => App.Instance.Controller;
    public Command<string> NavigationCommand => new Command<string>((stateId) =>
    {
        _controller.Complete(new Result() { StateId = Convert.ToInt32(stateId) });
    });
}

This way, the ViewModel knows nothing of the outside world and is isolated, can be reused or moved around.

Note: I would normally use DI to inject the controller, but this is just a sample.

Summary

This approach keeps the ViewModels completely isolated, and unknowing of where they have come from, or where they are going. They only return a result upon completion, and the WorkflowController takes care of the rest.

The mapping is a little clumsy at the moment, but this was just a proof of concept, and could be enhanced to include many variations such as defining NoHistory, Modals and MainPage navigation stack switches, as necessary.

Source code at Workflow GitHub Repo.


XAMARIN WEEKLY NEWSLETTER

Subscribe to a hand-picked round up of the best Xamarin development links every week. Published every Friday. Free.

Signup to Weekly Xamarin

* I do not maintain or run this newsletter, but I do recommend it.

Related Posts

Microsoft MVP | Xamarin MVP | Xamarin Forms Developer | Build Flutter

3 Comments

  1. Bogusław Błoński

    boguslawblonski(@filipoff):
    I have same problem but my solution is:

    I have proxy: routing service as #aspdotnet routing, in fact many routing services BookingRoutingService, CustomerProfileRoutingService…

    where each XxxRoutingService.NavigateAsync(fromPage, toPage, objectParam)

    Routing service decides what to do with related pages: refresh, close in (popup) to classic, forgot navigation stack, and finally navigate to page or not when VM.loadData( objectParam) fails.

    Object param is what feed VM or enable loading data for fill up VM.

  2. Ahmd ElMadi

    I think this makes it even more harder than it was . Yes from one point the ViewModel do not care what happens next when they are done with their job, which is nice, but it will be a hell of a work for the workflow to list all the viewmodels and the possibility combinations where to go next. Imagine having more than 2 statIds for some ViewModels and others have only one .
    I liked the way you expressed your thoughts by avoiding events and messagingCenter to use Struct and Dictionary instead. That is good to see and exotic .

    For me to solve this issue and keeping a centralized workflow to control where to go is by having this solution on a modular level (e.g. Modularity principle by Prism)

    Within the module itself the viewmodels can navigate to each other from withen the viewmodel.

    however if we know that the viewmodel would go to a viewmodel in another module then give that job to a workflow controller that would control to which module it should go next.

    Here you will get 100% agnostic modules , and not so complicated to debug and follow.

    A small example to make it more clear .
    Assume you have an app that starts with a sign in page and has sign in button that takes you to the main page and a signup button that takes you to a CreateNewUserPage.

    For me as I know that a signup button will Always take me to a signUp page then I would do the navigation to it within the signInViewModel
    However, since after pressing SignIn you are not sure where to go next but you know that you will be out of this module (e.g. Authentication Module) then the ViewModel would tell the workflow controller that it is done working and the workflow controller would know where to go next.

    1. Adam Pedley

      I agree that the API to declare workflows should be cleaned up and made a lot more Fluent, to ensure it scales properly. But other than this, being on a large project, having the navigation inside the ViewModel has caused a number of issues, including slowing the team down, while they are waiting for other pages to finish. Isolating each ViewModel helps a dev just do their work, without having to wait or modify another ViewModel/View.

      For small apps, I think this solution will be a bit of an overkill though.