Flow Based MVC Controllers

I've been working on a project lately that has required that a controller follows a series of logical steps before it reaches the end. I don't want users to access the last step until earlier steps have finished. The process also branches into separate paths depending on their selections.

One of the things that bugged me is I hated having so much logic devoted to checking to see if steps had been done before I continued. For example, checking if an image had been uploaded OR if they selected one from a gallery. Most likely I could simply create a property that checked for the state of things, but I wanted to come up with a reusable way to manage the flow of a controller.

The Flow

So here is an example of what kind of flow would go through this. This controller allowed a customer to provide a custom image or choose one from a gallery. The requirements at the end change depending on what a user selects.

Right now I've only developed some rough code -- but here is a basic idea of how the flow can be controlled automatically.

The Basics

This first example shows the basic idea behind identifying a method as requiring a step to complete. An attribute can be applied to

//for now, the flow process uses extension methods instead of an inherited class
using MvcFlow.FlowExtensionMethods;

//A controller for MVC
public class ShopController : Controller {

    //required override to check steps automatically
    protected override void OnActionExecuting(ActionExecutingContext filterContext) {
        //extension method to check an action in the flow
        this.ProcessStep(filterContext);
    }

    //this step has no requirements so it can be browsed to at any time
    public ActionResult Index() {
        returns this.View();
    }

    //this has no requirements but it does APPLY a step if the validation is successful
    public ActionResult ValidateUser(string username, string password) {
        if (this.PasswordIsCorrect(username, password)) {

            //an extension method to update the steps for the flow
            this.ApplyStep(Steps.UserValidated);
            return this.RedirectToAction("SelectType");
        }
        //if the process doesn't apply the next allowed step then it can't proceed
        else {
            return this.RedirectToAction("Index");
        }
    }

    //this step cannot be executed until Steps.UserValidated has been applied
    //which is done in the successful validation step
    [StepRequires(Steps.UserValidated)]
    public ActionResult SelectType() {
        return this.View();
    }

    //snip...

}

The important thing to notice is that there are only three things required to make this process work.

  • Override the OnActionExecuting method to perform the validation. By overriding this step we can make sure an action is allowed to be executed -- and if not, redirect the user.
  • Add an attribute to describe step requirements. The attribute StepRequires allowed us to define what steps must have been completed before they can execute this action.
  • Apply steps after successful validations. If requirements have been met then the step can be saved and a new set of actions will open up.

Removing And Branching

Here is another example of the code that shows how flow can be managed...

//snip...

//this step requires validation has happened -- but it also removes if 
//they have selected certain future steps. This means if someone browses back
//to the page then they are required to answer the question again
[StepRequires(Step.ValidatedUser)]
[StepRemoves(Step.SelectCustom | Step.SelectGallery)]
public ActionResult ChooseType() {
    return this.View();
}

// BRANCH 1
// Custom --------------------------

//Applies the SelectCustom option -- but also removes the option for
//the other branch (if it was even found)
[StepRequires(Step.ValidatedUser)]
[StepRemoves(Step.SelectGallery)]
public ActionResult SelectCustom() {
    this.ApplyStep(Step.SelectCustom);
    return this.RedirectToAction("UploadImage");
}

//This step requires that both the validation and select custom steps have been applied
[StepRequires(Step.ValidateUser | Step.SelectCustom)]
public ActionResult UploadImage() {
    return this.View();
}

// BRANCH 2
// Gallery --------------------------

//Applies the SelectGallery option -- but also removes the option for
//the other branch (if it was even found)
[StepRequires(Step.ValidatedUser)]
[StepRemoves(Step.SelectCustom)]
public ActionResult SelectGallery() {
    this.ApplyStep(Step.SelectGallery);
    return this.RedirectToAction("ShowGallery");
}

//This step requires that both the validation and select gallery steps have been applied
[StepRequires(Step.ValidateUser | Step.SelectGallery)]
public ActionResult ShowGallery() {
    return this.View();
}

Unfortunately, the attributes are a bit difficult to read but the general idea is to break the flow of the controller into two parts -- each existing separate from each other but within the same controller.

You'll notice in these steps we can use our enumerated type to create more than one requirement for an action (for example Step.ValidatedUser | Step.SelectCustom ). We can also use the RemovesStep attribute to automatically move back the current point of the flow.

Too Confusing?

I wrote a lot of the code to do this process today but I'm looking for some thoughts about this process. Is this too much work? Too confusing? Not really helpful?

December 2, 2009

Flow Based MVC Controllers

Attempts to write logic flow based MVC Controllers