The Skinniest ASP.NET MVC Controllers You've Ever Seen, Part 1
After pushing the controllers to have a thin implementation, we take that a little further by showing how to do additional GETs, pass parameters, and perform "thin" POSTs to the controllers.
In the previous post about pushing your ASP.NET MVC controllers, we discussed how to take all of your processing code and move it out to a ViewModelBuilder class making your controllers easier to read.
In this first of two posts, we will talk about how to minimize your controllers even more by using multiple view models and accepting and passing parameters on to our ViewModeBuilders for processing.
In the second post on Wednesday, we'll finish everything up when we discuss how to achieve a one-line POST-ing method for our existing FAQController.
These posts should give you a great foundation for developing quick, testable, and efficient ASP.NET MVC web apps. Let's get started!
Setting up additional GETs
Let's examine the rules of our controllers.
If you have two method names with the same method signature, your code won't compile. There are two ways to differentiate between the GET and POST method:
- Make sure your method name that performs the POST has a different parameter signature than the GET.
- You need to place an [HttpPost] attribute on the method name that performs the POST logic.
I'm getting ahead of myself. We need to focus on our GETs first.
Let's use our FaqController again from the previous post. This is what we have now:
Controllers\FAQController.cs
using System.Web.Mvc; using ThinController.Classes; using ThinController.Models; namespace ThinController.Controllers { public class FaqController : Controller { private DefaultViewModelFactory _factory = new DefaultViewModelFactory(); // GET: FAQ public ActionResult Index() { return View(_factory.GetViewModel<FaqController, FaqViewModel>(this)); } } }
Hmm...you know what we need? Let's create a Create and Update FAQ page.
Check out that Model!
If we want to update or create a Faq page, we need to add a FAQ item to our ViewModel so we can pass it back and forth. Let's modify our FaqViewModel to include a single FAQ item:
Models\FaqViewModel.cs
using System.Collections.Generic; using ThinController.Interfaces; namespace ThinController.Models { public class FaqViewModel: BaseViewModel, IFaqViewModel { public IEnumerable<Faq> FaqList { get; set; } public Faq FaqItem { get; set; } } }
Pretty standard, right? But is this model good enough for the creation or updating of a FAQ?
Yes...Yes, it is.
All we require is a single FAQ record (FaqItem) to update. So for example purposes, I will create two new ViewModels so our DefaultViewFactory in our controller can find the ViewModelBuilders to create our ViewModels.
Models\FaqViewModel.cs
using System.Collections.Generic; using ThinController.Interfaces; namespace ThinController.Models { public class FaqViewModel: BaseViewModel, IFaqViewModel { public IEnumerable<Faq> FaqList { get; set; } public Faq FaqItem { get; set; } } public class CreateFaqViewModel : FaqViewModel { } public class UpdateFaqViewModel : FaqViewModel { } }
If our Create or Update View (the HTML pages) requires additional models in our ViewModel, we can easily add them to each of our ViewModels. This can also work with any other type of object that you need to pass to your Views. <object>ViewModel is simply a POCO (Plain Ole CLR Object).
Now for our ViewModelBuilders. The Create is REALLY simple based on our previous renditions of a ViewModelBuilder.
ViewModelBuilder\CreateFaqViewModelBuilder.cs
using ThinController.Controllers; using ThinController.Interfaces; using ThinController.Models; namespace ThinController.ViewModelBuilder { public class CreateFaqViewModelBuilder : IViewModelBuilder<FaqController, CreateFaqViewModel> { public CreateFaqViewModel Build(FaqController controller, CreateFaqViewModel viewModel) { viewModel.PageTitle = "Create FAQ"; viewModel.MetaDescription = "Makin' FAQies..."; viewModel.MetaKeywords = "Creating a FAQ"; viewModel.FaqItem = new Faq(); return viewModel; } } }
Set the properties and you're off!
Uh Oh! A Snag!
If you've been paying attention, you may have noticed that if we are going to load a FAQ from the database, we need a Faq Id or some identifier to actually load it.
But our DefaultViewModelFactory doesn't take any parameters.
Looks like we need to add another method in our DefaultViewModelFactory that takes parameters (I added the second method below with the TInput as a third parameter).
Classes\DefaultViewModelFactory.cs
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using ThinController.Interfaces; namespace ThinController.Classes { public class DefaultViewModelFactory : IViewModelFactory { public TViewModel GetViewModel<TController, TViewModel>(TController controller) { var viewModels = GetSystemTypes(controller, typeof(TViewModel)); var modelBuilders = GetSystemTypes(controller, typeof(IViewModelBuilder<TController, TViewModel>)); var typeName = typeof(TViewModel).Name; var model = (from t in viewModels where typeof(TViewModel).IsAssignableFrom(t) && t.Name.Equals(typeName) let strategy = (TViewModel)Activator.CreateInstance(t) select strategy).FirstOrDefault(); var modelBuilder = (from t in modelBuilders where typeof(IViewModelBuilder<TController, TViewModel>) .IsAssignableFrom(t) let strategy = (IViewModelBuilder<TController, TViewModel>) Activator.CreateInstance(t) select strategy).FirstOrDefault(); // Redirect and assist developers in adding their own modelbuilder/viewmodel if (modelBuilder == null) throw new Exception( String.Format( "Could not find a ModelBuilder with a {0} Controller/{1} ViewModel pairing. Please create one.", typeof(TController).Name, typeof(TViewModel).Name)); return modelBuilder.Build(controller, model); } public TViewModel GetViewModel<TController, TViewModel, TInput>(TController controller, TInput data) { var viewModels = GetSystemTypes(controller, typeof(TViewModel)); var modelBuilders = GetSystemTypes(controller, typeof(IViewModelBuilder<TController, TViewModel, TInput>)); var typeName = typeof(TViewModel).Name; var model = (from t in viewModels where typeof(TViewModel).IsAssignableFrom(t) && t.Name.Equals(typeName) let strategy = (TViewModel)Activator.CreateInstance(t) select strategy).FirstOrDefault(); var modelBuilder = (from t in modelBuilders where typeof(IViewModelBuilder<TController, TViewModel, TInput>) .IsAssignableFrom(t) let strategy = (IViewModelBuilder<TController, TViewModel, TInput>) Activator.CreateInstance(t) select strategy).FirstOrDefault(); // Redirect and assist developers in adding their own modelbuilder/viewmodel if (modelBuilder == null) throw new Exception( String.Format( "Could not find a ModelBuilder with a {0} Controller/{1} ViewModel/{2} TInput pairing. Please create one.", typeof(TController).Name, typeof(TViewModel).Name, typeof(TInput).Name)); return modelBuilder.Build(controller, model, data); } private IEnumerable<Type> GetSystemTypes<TController>(TController controller, Type type) { // First, get the types of the executing assembly var currentAssemblyTypes = Assembly .GetExecutingAssembly() .GetTypes() .Where(e => type.IsAssignableFrom(e)) .ToList(); // Then the calling assembly currentAssemblyTypes.AddRange( Assembly .GetCallingAssembly() .GetTypes() .Where(e => type.IsAssignableFrom(e)) .ToList() ); return currentAssemblyTypes.Distinct(); } } }
NOTE: I have refactored the GetViewModel to use dependency injection using Ninject. Definitely smaller code. Check it out at UPDATE: Using Dependency Injection on ViewModelBuilder.
In addition to that, we need to add another interface to the IViewModelBuilder. Once that's finished. we can now create concrete classes of IViewModelBuilders with parameters and the DefaultViewModelFactory will be able to find the classes that match with a parameter or without a parameter.
Here's our updated IViewModelBuilder interface:
Interfaces\IViewModelBuilder.cs
namespace ThinController.Interfaces { public interface IViewModelBuilder<TController, TViewModel> { TViewModel Build(TController controller, TViewModel viewModel); } public interface IViewModelBuilder<TController, TViewModel, TInput> { TViewModel Build(TController controller, TViewModel viewModel, TInput input); } }
Now, we're ready to build our UpdateFaqViewModelBuilder. Since we have our ViewModelBuilder syntax with parameters, we can pass in anything we want. We can pass in native types or complex objects. In this case, we'll pass in a string Id.
ViewModelBuilder\UpdateFaqViewModelBuilder.cs
using ThinController.Classes; using ThinController.Controllers; using ThinController.Interfaces; using ThinController.Models; using ThinController.Repository; namespace ThinController.ViewModelBuilder { public class UpdateFaqViewModelBuilder : IViewModelBuilder<FaqController, UpdateFaqViewModel, string> { public UpdateFaqViewModel Build(FaqController controller, UpdateFaqViewModel viewModel, string id) { viewModel.PageTitle = "Create FAQ"; viewModel.MetaDescription = "Makin' FAQies..."; viewModel.MetaKeywords = "Creating a FAQ"; var unitOfWork = controller.ControllerContext.GetUnitOfWork<UnitOfWork>(); viewModel.FaqItem = unitOfWork.FaqRepository.GetById(id); return viewModel; } } }
NOTE: If you are wondering what the Unit of Work in the code above is all about, I refer you to a recent post about accessing your data layer with unique requests.
One last thing!
Ok, one last thing before we break and reconvene on Wednesday.
What does the Controller look like now? I'm glad you asked!
Here is our finished controller for part one (Brace yourself, it's scary!)
Controllers\FAQController.cs
using System.Web.Mvc; using ThinController.Classes; using ThinController.Models; namespace ThinController.Controllers { public class FaqController : Controller { private DefaultViewModelFactory _factory = new DefaultViewModelFactory(); // GET: FAQ public ActionResult Index() { return View(_factory.GetViewModel<FaqController, FaqViewModel>(this)); } public ActionResult Create() { return View(_factory.GetViewModel<FaqController, CreateFaqViewModel>(this)); } public ActionResult Update(string id) { return View(_factory.GetViewModel<FaqController, UpdateFaqViewModel, string>(this, id)); } } }
Now I ask you...have you EVER seen a smaller controller in your life? Isn't this awesome!
Also, notice the _factory.GetViewModel in the Update method.
This is where the additional parameter in our UpdateFaqViewModelBuilder comes in. When it finds that specific signature through reflection, it passes the controller and id to the builder for processing and returns the specified ViewModel which is our UpdateFaqViewModel.
I love this technique and it's really cool when you look at it. It's almost defensive programming. It protects you against yourself. This code WILL NOT compile until you use the appropriate types to retrieve the right values.
Now when you write your controllers, you have two ways to process your requests: one with parameters and one without parameters.
Of course, we are only GET-ting pages and not POST-ing, but that will have to wait until Wednesday.
Code on, people, Code on...and I'll see ya Wednesday with part 2.
Did this make sense? Did I miss something? Post in the comments below.