Real-World Refactoring: POST-ing in ThinController Project
In this second post, we'll refactor the POST-ing of data in the ThinController project.
Earlier this week, we refactored the ThinController project by removing the dependency injection scanner in various places throughout the application and moved it closer to the entry point of the web app.
Today, I want to continue by adding more to the project based on a reader's previous comments.
From my skinniest controller post I wrote, I didn't add the additional CreateAndUpdateProcessResults to the GitHub repo.
This post will be a short article, but it's to keep everything updated with the GitHub repo.
Time to Refactor!
When I created the ThinController project on GitHub, I didn't include the CreateAndUpdateActionResult.
I explain the reasoning behind the ActionResult in the post, but looking back, the name just doesn't match the function because we could be doing more than just creating and updating.
As the age old joke goes, there are two hard things in computer science:
- Cache invalidation
- Naming things (which I absolutely suck at...obviously)
- Off-by-one errors.
<sad trombone> Sorry, couldn't resist.
Recently, I renamed the ActionResult to something a little more vague.
ActionResults/ProcessValidResult.cs
public class ProcessValidResult<T> : ActionResult { protected readonly T Model; public IGeneralFormHandler<T> Handler { get; set; } public Func<T, ActionResult> SuccessResult; public Func<T, ActionResult> FailureResult;
public ProcessValidResult(T model) { Model = model; }
public ProcessValidResult(T model, IGeneralFormHandler<T> handler, Func<T, ActionResult> successResult, Func<T, ActionResult> failureResult) { Model = model; Handler = handler; SuccessResult = successResult; FailureResult = failureResult; }
public override void ExecuteResult(ControllerContext context) { var viewData = context.Controller.ViewData; if (viewData.ModelState.IsValid) { Handler.ProcessForm(context, Model); SuccessResult(Model).ExecuteResult(context); } else { FailureResult(Model).ExecuteResult(context); } } }
The ProcessValidResult is used to POST data and verifying it using the IsValid from the ModelState. If it's successful, I send it on it's way to be processed and on to the SuccessResult. If not, then the FailureResult is executed.
For those looking for the IGeneralHandler, it's located in the Skinniest Controller, Part 2.
As a refresher from that post, I pass four parameters:
- The ViewModel - Add, update, or remove what we want.
- A Handler for the data posted back - Custom class.
- Success ActionResult - A model is passed into it for later.
- Failure ActionResult - Also, a model is passed into it.
We can now add this class to our BaseController in a nice and easy way.
public ActionResult ProcessValidResult<T>(T model, IGeneralFormHandler<T> handler, Func<T, ActionResult> successResult, Func<T, ActionResult> failureResult) { return new ProcessValidResult<T>(model, handler, successResult, failureResult); }
I made this a little more attractive since MVC did the same thing with the ViewResult.
One More ActionResult
Of course, there are some exceptions to the rule.
Maybe we don't want to include an IsValid check. Maybe we just want to execute a process.
Here's the ProcessResult class for the controller.
ActionResults/ProcessResult.cs
public class ProcessResult<T> : ActionResult { protected readonly T Model;
public IGeneralFormHandler<T> Handler { get; set; } public Func<T, ActionResult> SuccessResult; public Func<T, ActionResult> FailureResult;
public ProcessResult(T model) { Model = model; }
public ProcessResult(T model, IGeneralFormHandler<T> handler, Func<T, ActionResult> successResult, Func<T, ActionResult> failureResult) { Model = model; Handler = handler; SuccessResult = successResult; FailureResult = failureResult; }
public override void ExecuteResult(ControllerContext context) { if (Handler.ProcessForm(context, Model)) { SuccessResult(Model).ExecuteResult(context); } else { FailureResult(Model).ExecuteResult(context); } } }
The key here is the ExecuteResult. We don't have an IsValid test.
The Handler "Processes the Form[data]" and returns either a true or false.
Based on the result, it will execute the success ActionResult or the failure ActionResult.
For a sample GeneralHandler, I created a quick CreateFaqFormHandler.
FormHandlers/CreateFaqFormHandler.cs
public class CreateFaqFormHandler : IGeneralFormHandler<FaqViewModel> { public bool ProcessForm(ControllerContext context, FaqViewModel viewModel) { var dbContext = context.GetDbContext<DemoDbContext>(); var result = false; try { // dbContext.FAQs.Add(viewModel.FaqItem); // dbContext.SaveChanges(); } finally { result = true; } /* * If you want, you can have your success page display a message to the user. * context.Controller.TempData["Message"] = viewModel.Faq.Question + " has been successfully saved."; * */
return result; } }
It's something quick, but it gives you an example of moving the processing out of the controller into a separate process.
For our controller, when we postback, it's a simple call to our ProcessValidResult or ProcessResult.
public ActionResult Index(FaqViewModel model) { return ProcessValidResult( model, new CreateFaqFormHandler(), viewModel => Redirect(Url.Content("/")), viewModel => View(viewModel)); }
These FormHandlers make your code a little more friendlier, smaller, and unit testable.
Conclusion
In this post, I took our existing ActionResults and refactored them by renaming and moving them into the BaseController so the details are hidden from the user.
It's a small refactor, but necessary.
Using this same pattern, you can make an entire library of custom data ActionResults.
Did this make sense? Was this confusing? Post your comments below and let's discuss.