Real-World Refactoring: Replacing ASP.NET MVC Conditional Dispatcher With Command

When posting back from a grid to an MVC POST action, you could encounter a number of conditions that grows into an if..else from hell. Here's how to take it from a Conditional Dispatcher into a Command pattern.

June 1st, 2015 • MVC •
0 (0 votes)
Refactor code screen

When executing an ASP.NET MVC form postback on a grid, you may have a number of "commands" performing a specific function based on a button press.

The postback code can become pretty messy. If you add an if..then..else in the controller, it will become an absolute nightmare to maintain. The controller code needs to be clean and readable.

How can you make a large number of functions without writing an unwieldy if..then..else statement?

By performing a refactoring on the conditional dispatcher with commands.

This technique is from the book Refactoring To Patterns under the heading Replace Conditional Dispatcher With Command. I mentioned this book previously in an earlier post called Top 10 Books Every .NET Developer Should Own and would definitely recommend it...again.

What are we working with?

Let's look at some code and look at the previous post from our WebGrid series - Batch Processing.

Below is an example of what our web grid currently looks like.

WebGrid Screenshot

The grid has a toolbar on the top of the grid. It has many buttons, but we'll focus on two of them for now: The Delete button and a new function called Send Email (just for fun).

When someone clicks the button, whether it's a btnDelete or btnSendEmail, the value of the button will postback to the method.

Let's look at a basic POST controller.

[HttpPost]
public ActionResult Index(FormCollection forms)
{
// "select" variable will contain "select=2,3,4,5,10"
string[] checks = forms["select"].Split(',');

    foreach (string checkedId in checks)     {         if (checkedId.Contains("false")) continue;                  var id = checkedId;         User user = _repository.First(e => e.Id.ToString() == id);         if (!String.IsNullOrEmpty(forms["btnDelete"]))         {             _repository.Delete(user);         }         if (!String.IsNullOrEmpty(forms["btnSendEmail"]))         {             EMailTemplateService.SendTemplatedEMail(user);         }         if (!String.IsNullOrEmpty(forms["btnApprove"]))         {             //          }         if (!String.IsNullOrEmpty(forms["Stuff1"]))         {             // blah         }
        if (!String.IsNullOrEmpty(forms["Stuff2"]))         {             // blah         }     }     return Redirect(Url.Content("~/")); }

Eeek!

It looks like a lot, so let's go through this step-by-step to explain what we're doing here.

Your WebGrid AND toolbar should be entirely contained in your view's form tag (or Html.BeginForm...however you look at it). That way, when we post back, we receive the button press and the checkboxes of the records selected.

Each checkbox in the row will have the attribute name of "select" (name="select") and can be passed back with the list of Ids that require an action when you check them.

When posted, all your checked records should return in a comma-delimited list. These are your selected records with Ids.

To determine the button you pressed, check to see if each button has a value in it. Use the String.IsNullOrEmpty(<buttonName>) to determine the button.

This is your Conditional Dispatcher.

We need to refactor this into a Command pattern.

Time to Refactor!

What we want to do is take all these actions and move them out of the controller. If we add even more functionality to the WebGrid, we'll have a long method...and that's a code smell. Even having this long method in a controller is even worse.

Let's refactor this into more granular pieces and work on some new ActionResults.

The first one we'll work on is the CommandResult. This is considered like a base class of our commands. We just need a couple of methods to help out with the descendant classes.

public class CommandResult : ActionResult
{
    private readonly CommandList<IFormCommand> _commands = new CommandList<IFormCommand>();
    public CommandList<IFormCommand> Commands
    {
        get { return _commands; }
    }
    public string BatchName { getset; }
    public FormCollection FormCollection { getset; }
    public Func<IFormCommandActionResult> SuccessResult;
    public CommandResult(ControllerContext context, Func<IFormCommandActionResult> successResult) 
        : this(forms, successResult, String.Empty) { }
    
    public CommandResult(ControllerContext context, Func<IFormCommandActionResult> successResult, 
        string triggerName)
    {
        BatchName = triggerName;
        FormCollection = new FormCollection(context.HttpContext.Request.Form);
        SuccessResult = successResult;
    }
    public override void ExecuteResult(ControllerContext context)
    {
        var command = GetKnownCommand();
        if (command != null)
            command.Execute(String.Empty);
        SuccessResult(command).ExecuteResult(context);
    }
    protected IFormCommand GetKnownCommand()
    {
        return Commands.FirstOrDefault(
            e => FormCollection.AllKeys.Any(f => f.Equals(e.CommandName)));
    }
}

public class CommandList<TFormCommand> : List<TFormCommand> { }
public interface IFormCommand {     string CommandName { getset; }     string Result { getset; }     bool Success { getset; }     ControllerContext Context { getset; }     bool Execute(string input); }
public abstract class FormCommand : IFormCommand {     public string CommandName { getset; }     public string Result { getset; }     public bool Success { getset; }     public ControllerContext Context { getset; }     public virtual bool Execute(string input) { return true; }     public virtual bool Execute(string input, string input2) { return true; }     protected FormCommand() : this(null) { }     protected FormCommand(ControllerContext context)     {         Context = context;         Success = true;     } }

At the top, the CommandList is defined as a List of IFormCommand. It's a class for managing the commands for this particular form. In our case, the WebGrid form.

Each one of our commands (DeleteUser and SendEmail) for the WebGrid will contain a command name based on the button pressed. The IFormCommand interface defines what our commands will consist of in our class.

Building the BatchCommandResult

Now, we focus on the next class for our commands: The BatchCommandResult.

The BatchCommandResult handles all the "commands" passed from the WebGrid where we want to perform an action on a batch load of records. We have two kinds of commands: Actionable Commands and Non-Actionable Commands.

Actionable Commands are commands where actions occur based on the selection of records.

Non-Actionable Commands are commands where no records are processed.

For example, if you want to batch-delete records from the grid, these are Actionable Commands. If you wanted to set the number of rows for paging when you selected a different number, this would be a Non-Actionable Command.

The FormCommand class is at the end. We need the class abstracted because of the context passed in through the constructor. We need to confirm we received the context so we can use the context to process the command.

NOTE: Based on past posts, you may have noticed that I look at the ControllerContext as king. In the MVC world, it is considered the HttpContext in my eyes. Once you pass this into your methods, the world is your oyster.

ActionResults\BatchCommandResult.cs

public class BatchCommandResult : CommandResult
{
    public BatchCommandResult(ControllerContext context, Func<IFormCommandActionResult> successResult)
        : base(context, successResult)
    {
        BatchName = "select";
    }
    public override void ExecuteResult(ControllerContext context)
    {
        ExecuteCommands();
        var command = ExecuteNonCommands(); // i.e. paging, etc.
        SuccessResult(command).ExecuteResult(context);
    }
    private IFormCommand ExecuteNonCommands()
    {
        foreach (var command in this.FormCollection)
        {
            var formCommand = Commands.FirstOrDefault(e => e.CommandName == command.ToString());
            if (formCommand == nullcontinue;
            
            var value = FormCollection[formCommand.CommandName];
            if (String.IsNullOrEmpty(value)) continue;
            
            var idList = value.Split(',');
            foreach (var valueList in idList)
            {
                formCommand.Execute(valueList);
            }
            return formCommand;
        }
        return null;
    }
    private void ExecuteCommands()
    {
        if (!IsSelected) return;
        var idList = SelectionItem.Split(',');
        var command = GetKnownCommand();
        if (command != null)
        {
            foreach (var valueList in idList)
            {
                command.Execute(valueList);
            }
        }
    }
    protected bool IsSelected
    {
        get
        {
            var result = false;
            if (FormCollection != null && FormCollection.Count > 0)
            {
                result = !String.IsNullOrEmpty(FormCollection[BatchName]);
            }
            return result;
        }
    }
    protected string SelectionItem
    {
        get
        {
            return FormCollection[BatchName];
        }
    }
}

Ok, let's break this method down.

First, the "BatchName" is the name that you are giving the name of the control that selects the records. In this case, we named all our checkboxes "select" in the WebGrid. You can name this whatever you want and create an overloaded constructor for passing in a different BatchName.

If you notice the first line in our controller's POST method, we'll remove it because we are taking care of it in the BatchCommandResult.

string[] checks = forms["select"].Split(',');

Next, we have our ExecuteResult. Here we start executing the commands based on whether we selected the records or not.

The ExecuteCommands() method will check to see if there are any BatchName checkboxes selected based on the IsSelected property. If we selected any records, we split the comma-delimited data and execute the command based on the button name assigned to it by the WebGrid.

If we picked some checkboxes and have a "select" coming back with a name-value pair of "select=2,5,7,12,13" on the postback, we have an Actionable list of id values to process.

Now, the next line.

Let me ask you a question.

What happens when someone wants to view the WebGrid with 100 records instead of 10? No one selects any records, but we need to process this action. This is why we have Non-Actionable Commands.

ExecuteNonCommands() method looks through the list and tries to find the command based on what was POST-ed back. For example, the "command"/Name for our SetPagingRowCommand is "rows." When we find this command in the FormCollection, we know that there is an action when the postback occurred on our WebGrid.

We continue the process by splitting the value from the postback (i.e. "rows=100") and we process the value in the SetPageRowCommand by passing in the value to the command through the Execute method.

You'll notice after we are finished processing the Non-Actionable Command, we return the command back to the controller. We did this on purpose. We want to make you aware of the selected command that we acted on so you can make better decisions based on that command's results later if necessary.

Based on the SetPageRowCommand, we can take that and use a Redirect(Url.UserUrl(command.Result)) where Result would be the paging number so we can pass it into our Url ("users.cshtml?pagesize=100").

NOTE: If you need a refresher on UrlHelpers, visit my past posts on UrlHelpers called ASP.NET MVC Url Helpers: Catalog Your Site and 6 ASP.NET MVC UrlHelper Quick Tips to Maximize Your Link Management

Making the Commands

Finally, we get to the fun part: making all our commands.

These are the actions inside of each if..endif conditional from your controller. They are small, but they are unit-testable.

First, our DeleteUserCommand.

public class DeleteUserCommand : FormCommand
{
    public DeleteUserCommand(ControllerContext context): base(context)
    {
        CommandName = "btnDelete";
    }
    public override bool Execute(string input)
    {
        if (String.IsNullOrEmpty(input)) return false;
        var unitOfWork = Context.GetUnitOfWork<AbstractUnitOfWork>();
        var userId = input;
        unitOfWork.UserRepository.DeleteUser(userId);
        try
        {
            unitOfWork.UserRepository.SaveChanges();
            Success = true;
        }
        catch
        {
            Success = false;
        }
        return Success;
    }
}

The CommandName should match the name of the pressed button or the control making the action (i.e. the <select> paging control called "rows") so when it's encountered in the FormCollection, it will know which command to use based on that name's signature.

The meat of the command will be the Execute() method. In this case, the input passed in through the method will be the value of each selected id in the Grid.

The Success property is optional, but it would be a good practice to return something just in case you need a status on whether the command processed or not.

That's it!

Here are the other two commands.

FormCommands\SendEmailCommand.cs

public class SendEmailCommand : FormCommand
{
    public SendEmailCommand(ControllerContext context): base(context)
    {
        CommandName = "btnSendEmail";
    }
    public override bool Execute(string input)
    {
        if (String.IsNullOrEmpty(input)) return false;
        var userId = input;
        EMailTemplateService.SendTemplatedEMail(userId);
        return true;
    }
}

FormCommands\SetPageRowsCommand.cs

public class SetPageRowsCommand : FormCommand
{
    public SetPageRowsCommand(ControllerContext context)
    {
        CommandName = "rows";
        Context = context;
    }
    public override bool Execute(string input)
    {
        Result = input;
        return true;
    }
}

Bring it together!

We need to combine all these commands together using the BatchCommandResult. We do that by making a specific ActionResult for this particular grid.

ActionResults\UserCommandResult.cs

public class UserCommandResultBatchCommandResult
{
    public UserCommandResult(ControllerContext context, Func<IFormCommandActionResult> successResult) 
        : base(context, successResult)
    {
        Commands.Clear();
        Commands.Add(new DeleteUserCommand(context));
        Commands.Add(new SendEmailCommand(context));
        Commands.Add(new SetPageRowsCommand(context));
    }
}

Simple enough.

Now, to put the icing on the cake by adding the UserCommandResult to our postback method in our controller.

[HttpPost]
public ActionResult Index(FormCollection forms)
{
    return new UserCommandResult(
        ControllerContext,
        command => Redirect(Url.Content("~/"))
    );
}

Most of the time when you execute a certain command, you want to return back to the page that issued the command to see if the changes worked.

If you want to redirect based on a command's result (like the SetPageRowsCommand), create an overloaded Url extension method to accept the SetPageRowsCommand and create the Url based on the command's results.

Conclusion

This code refactor has made life a lot easier when adding new "commands" to my WebGrid. All I do is go into my UserCommandResult class and add a new command to my Commands list based on my new action.

When you have a long list of if..else in your code, I would recommend using the Conditional Dispatcher with Command refactoring. It makes it extremely easier to maintain.

Did this help you understand refactoring better? Would you like to see more of these? Let me know in the comments below.

Was this informative? Share it!

Looking to become a better developer?

Sign up to receive ReSharper Design Pattern Smart Templates, ASP.NET MVC Guidelines Checklist, and Newsletter Updates!

Picture of Jonathan Danylko

Jonathan Danylko is a freelance web architect and avid programmer who has been programming for over 20 years. He has developed various systems in numerous industries including e-commerce, biotechnology, real estate, health, insurance, and utility companies.

When asked what he likes to do in his spare time, he replies, "Programming."

comments powered by Disqus