Hugoware

The product of a web developer with a little too much caffeine

Flow Based MVC Controllers

with 2 comments

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?

Do you spend enough time managing flow in your applications that a framework like this could help?

Advertisements

Written by hugoware

December 2, 2009 at 10:05 pm

2 Responses

Subscribe to comments with RSS.

  1. There is a very light weight state machine called StateLess by Nicholas Blumhardt (AutoFac) that I used to roll my own workflow system. It allows to define a state as:

    StateMachine

    so that you can define BonusStateMachine.

    Here is the sample from the site:

    var phoneCall = new StateMachine(State.OffHook);

    phoneCall.Configure(State.OffHook)
    .Permit(Trigger.CallDialed, State.Ringing);

    phoneCall.Configure(State.Ringing)
    .Permit(Trigger.HungUp, State.OffHook)
    .Permit(Trigger.CallConnected, State.Connected);

    phoneCall.Configure(State.Connected)
    .OnEntry(() => StartCallTimer())
    .OnExit(() => StopCallTimer())
    .Permit(Trigger.LeftMessage, State.OffHook)
    .Permit(Trigger.HungUp, State.OffHook)
    .Permit(Trigger.PlacedOnHold, State.OnHold);

    // …

    phoneCall.Fire(Trigger.CallDialled);
    Assert.AreEqual(State.Ringing, phoneCall.State);

    Your states can be the keys from your database, and the triggers can be things like “Complete”, or “Reject”. You directly integrate any ORM with this type of solution.

    It’s simply wonderful. I scrapped Windows Workflow in favor of this, as StateLess easily allowed my to integrate a statemachine with my database schema, and allowed me to my primary key BonusStatusId as a representation of state.

    Had I gone the Windows Workflow route, I would have had to have supported 4 additional projects in my solution. If you haven’t worked with Windows Workflow and yet need a state machine implementation, StateLess is the route to go. Having used it I and benefited for how simple it is to configure, I would never consider Windows Workflow for a state machine.

    Here’s the link:

    http://code.google.com/p/stateless/

    I’d be willing to share source code with you if you like. Just hit my contact page on my blog.

    ActiveEngine Sensei

    December 4, 2009 at 7:17 pm

    • Very interesting — I’d prefer to find a library that I could use. I was getting the feeling that enumerated type attributes wouldn’t be sufficient to manage something like this.

      It sounds like this could apply to an MVC Controller — thanks for the link — I’ll contact you Monday!

      webdev_hb

      December 4, 2009 at 7:37 pm


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: