Series Index
- Part 1: Reusable controller actions & model inheritance
- Part 2: Custom meta data provider (coming soon)
One of the great new features in ASP.NET MVC 2 is the template system. It is very similar to the template form helper system introduced in MvcContrib for MVC V1. For more information on the MVC template system read Brad Wilsons excellent series.
This template system allows you to create convention based views and forms. The scenario that I have been working with the last couple of days is forms for reports. These forms are all very similar, that is they are all built around a couple of report filters/parameters built using dropdowns, checkboxes and datetime pickers.
My idea was that each report would only consist of a new report model and the most of the views, and controller actions could be reused.
Example:
public class ReportController : Controller { public ViewResult ViewRequestForm() { return View("ViewForm", new RequestReportForm()); } public ViewResult ViewOrderForm() { return View("ViewForm", new OrderReportForm()); } [HttpPost] public ActionResult ViewReport(ReportForm form) { return View(form.GetReportParameters()); } }The idea is to be able to reuse the same root view and the same action for all forms. The root view is only going look something like this:
<% Html.BeginForm("ViewReport", "Report"); %> <%=Html.EditorFor(x => x) %> <input type="submit" value="submit" /> <% Html.EndForm(); %>
The EditorFor helper is going to start the MVC template engine. This template engine is first going to try to find a matching editor template for the report form model, since no specific template exists it is going to fallback to the default template for object, this template loops through all properties defined on the model and applies an editor for each property.
This is how the RequestReportForm and OrderReportForm model looks like:
public class RequestReportForm : ReportForm { [DisplayName("Include canceled requests")] public bool IncludeCanceledRequests { get; set; } [DisplayName("Some parameter")] public string SomeParameter { get; set; } public override string GetReportParameters() { return "RequestFilter=" + IncludeCanceledRequests; } } public class OrderReportForm : ReportForm { [DisplayName("Group by status")] public bool GroupByStatus { get; set; } [DisplayName("Some other paramater")] public string SomeOrderParameter { get; set; } public override string GetReportParameters() { return "something"; } }
With the default templates built into MVC these models will be rendered like this:
RequestReportForm:
OrderReportForm:
If we would like to override these templates all we have to do is create our own partial view and name it string.ascx or bool.ascx and place it in a view folder named EditorTemplates, hopefully more on that in a later part in this blog series.
So sharing the same view for both of these report models seems to be very easy with the template system built into MVC 2 but how do we use the same controller action? We want to use the the controller action ViewReport that takes the base type ReportForm as a parameter. The standard DefaultModelBinder will not know which concrete class to bind to. The solution to this problem should be to create a custom model binder that will figure out which concrete form model to instantiate based on some field coming in the post data from the browser.
Example:public class ReportFormModelBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { var reportFormTypeName = bindingContext.ValueProvider.GetValue("ReportFormTypeName"); var reportFormType = Type.GetType("MvcApplication1.Models.ReportForms." + reportFormTypeName.AttemptedValue); var model = bindingContext.ModelMetadata.Model; bindingContext.ModelMetadata = new ModelMetadata(ModelMetadataProviders.Current, bindingContext.ModelMetadata.ContainerType, () => model, reportFormType, bindingContext.ModelMetadata.PropertyName); return base.BindModel(controllerContext, bindingContext); } }
This custom model binder looks for a form data field "ReportFormTypeName". With this name it is possible to get the .NET Type (based on some namespace convention) and thereby create a new ModelMetadata object, now we can call the base implementation. All we need to do now is to add this ReportFormTypeName field as a hidden field to our form. Lets add it to the base class ReportForm, like this:
public abstract class ReportForm { [HiddenInput(DisplayValue = false)] public string ReportFormTypeName { get { return this.GetType().Name; } } public abstract string GetReportParameters(); }
This is actually all we have to do to get the MVC template system to generate a hidden field named ReportFormTypeName that will contain the name of the concrete Type. Since both RequestReportForm and OrderReportForm inherit from ReportForm and since the default template for object will loop through all properties (including inherited properties) the hidden field will be included for all report forms.
So what have we accomplished? We can generate different report forms by only creating a new form model, the view rendering is shared and the controller action to generate the report is also shared. The controller action that handles the viewing of the report is in my case only responsible for concatenating all report parameters into a querystring to then pass to the report system, since this is so generic it can be handled in the same controller action for all report forms. I hope some of you understand what I am trying to show here, and I also hope to expand on the ideas in this post in later posts. Specifically how to make the report parameters more rich and how to control and override the layout using custom editor templates and custom metadata.