Recently, we built an ASP.NET MVC 2 web site for a customer. Then they asked us to build another one, almost the same, but with enough differences in both the data model, the design and flow that we branched the code and created a new version of it. The original site was built in VS2008, and the new one was upgraded to VS2010.
A few weeks later, they came back to us again, and asked for three more sites, just like the second one, but with a few differences. The data model held pretty true, but the page flow was different, and they wanted a portal like presentation, where a user could easily switch between any of the four sites. The sites were to be set up as follows:
www.companyname.com (Root of Portal)
www.companyname.com/ProductA
www.companyname.com/ProductB
www.companyname.com/ProductC
www.companyname.com/ProductD
We obviously didn’t want to make four copies of the code since the sites had to feel like one site at the portal level. And we couldn’t very well go putting case statements in every page to trap all the differences. Most of the pages and processes for each product are the same, but a few are different. We needed to come up with an approach that allowed us to share as much as we could, and then only create new functionality where we absolutely had to.
We came up with the idea of the abstract controller. We created an Abstract ProductController that defined abstract methods for all of the Actions needed for the Products. We then added protected methods inside the AbstractController that implemented the Actions, and returned views.
The next step was to create a Product specific controller for each of the Products that implemented all of the methods of the base controller. Most often, the Product specific controller simply called the Abstract Controller’s protected methods, but where the logic differed, it was free to specify its own logic. This cut down development and testing effort tremendously.
Here is shortened code for the abstract controller and a sample Product Controller
1: public abstract class ProductController : BaseController
2: {
3: #region virtual Methods that need to be implemented for each Product Controller
4: public abstract ActionResult Home();
9: public abstract ActionResult Faqs();
13: #endregion
14:
15: #region Base Methods
16:
17: protected ActionResult GetProductHomeView(string productQualifier)
18: {
19: var productId = ConfigurationManager.AppSettings.Get(string.Format("ProductId_{0}", productQualifier));
20: int localProductId;
21:
22: if (!Int32.TryParse(productId, out localProductId))
23: {
24: var homeViewModel = new HomeViewModel
25: {
26: Title = "Welcome To Company WebSite",
27: };
28:
29: //show the big header
30: return View(StringConstants.ActMainHome, homeViewModel);
31: }
32:
33: var productViewModel = new ProductViewModel
34: {
35: Title = "Welcome To Product",
36: ProductId = localProductId,
37: };
38:
39: ViewData[StringConstants.VdProductId] = localProductId;
40: ViewData[StringConstants.VdProductQualifier] = productQualifier;
41: return View(StringConstants.ActProductHome, productViewModel);
42: }
43:
44: protected ActionResult GetFaqsView(string productQualifier)
45: {
46: var productId = ConfigurationManager.AppSettings.Get(string.Format("ProductId_{0}", productQualifier));
47: int localProductId;
48:
49: if (!Int32.TryParse(productId, out localProductId))
50: {
51: var homeViewModel = new HomeViewModel
52: {
53: Title = "Welcome To Company Web Site",
54: };
55:
56: //show the big header
57: return View(StringConstants.ActMainHome, homeViewModel);
58: }
59:
60: var faqViewModel = new FaqViewModel
61: {
62: Title = "FAQ",
63: ProductId = localProductId,
64: };
65: ViewData[StringConstants.VdProductQualifier] = treatmentProductQualifier;
66: ViewData[StringConstants.VdProductProgramId] = localProductProgramId;
67: return View(StringConstants.ActTreatmentFaqs, faqViewModel);
68: }
69: }
70:
71: public class ProductAController : ProductController
72: {
73: public const string ProductQualifier = "ProductA";
74:
75: [HttpGet]
76: public override ActionResult Home()
77: {
78: return GetProductHomeView(ProductQualifier);
79: }
80:
81: [HttpGet]
82: public override ActionResult Faqs()
83: {
84: return GetFaqsView(ProductQualifier);
85: }
86: }
A few notes:
1. The ProductQualifier is a critical concept. When the Name Product Controller is instantiated, this value is passed into the protected calls to help identify the correct resources to load (i.e. this is how the product is identified). But we don’t explicitly trust this string. We make sure (by checking the config file), that we are actually set up to handle this product. If we somehow get past the controller and pass in an invalid product, the user is redirected to the Portal Home Page. This ensures that no process is ever run without picking a valid product.
2. The Views must be placed in the Shared Views Folder, otherwise the MVC View engine cannot find them.
3. Each view that references the controllers, either for a form submit, or a redirect, must know explicitly which controller to submit to. Fortunately, this is already available in the MVC framework. For form submits, you should always submit to the same controller that served up the page. The following line of code works quite nicely.
1: <% using (Html.BeginForm(StringConstants.ActBuyProduct, Url.RequestContext.RouteData.GetRequiredString("Controller"), FormMethod.Post, new { name = StringConstants.ActBuyProduct }))
4. On The Portal View, where we want to explicitly switch to different controllers, it’s pretty straightforward. Note that the Action Method is exactly the same. The controller changes.
1: <%= Html.ActionLink("Product A", StringConstants.ActProductHome, StringConstants.CtrlProductA)%><br />
2: <%= Html.ActionLink("Product B", StringConstants.ActProductHome, StringConstants.CtrlProductB)%><br />
3: <%= Html.ActionLink("Product C", StringConstants.ActProductHome, StringConstants.CtrlProductC)%><br />
4: <%= Html.ActionLink("Product D", StringConstants.ActProductHome, StringConstants.CtrlProductD)%><br />
5. We use named constants for our string names everywhere. We’re pretty religious about it, and that made this conversion and refactoring a lot easier, especially when done with ReSharper. I didn’t define any of the constants in the sample above, but they should be pretty easy to figure out.
This solution works pretty well where the number of Products is well known, and the functionality for each is similar, but not exact. You will have to add code and configuration each time you add a new Product. However, with the requirements of this project, we had to add code each time anyway to handle product process changes, and the savings of this simple, common approach more than compensated for the fact that the solution may not be perfect from a purist’s view. We’re not talking a few hours of saved time here. We’re talking hundreds of hours of dev time saved. All from a simple little pattern.
Tags:
748c7f71-3a46-4cd0-8006-1be4877b4081|0|.0