This is the first of a series of posts where I will describe the features of the MVCControlsToolkit Library for MVC that can be found here. The MVC Controls Toolkit contains some Controls for MVC, and a toolset for defining easily new controls. In this first post I describe the needs that lead me to develop the MVCControlsToolkit, and its foundations.
One of the main foundational principles of MVC is the separation of concerns between the Controller that specifies what to show and the View that specifies How to show it. In the View you have access to all data that the Controller want to display through the View Model, and you can take advantage of the full power of the .Net framework to manipulate the data contained in the View Model to give to your web page the look that you prefer and also a structure that is quite different from the way data are organized in the View Model. For instance you can decide to display a date as 3 separate inputs, one for the Year, one for the Month and the other for the Day, instead of using a single TextBox for the whole date. Unluckily, if you do this the Data Binder will not be able to recompose this three separate fields into the original DateTime object when the View is posted back.
From one side you have the maximum freedom in rendering the View Model in the View and you can define helper methods to handle complex rendering tasks, but from the other side if there isn’t a one to one correspondence between the elements to post back in the model and the properties of the View Model, and a one to one correspondence between the hierarchical structure of the input fields names and the hierarchical structure of the model, the Model Binder isn’t able to reconstruct the View Model from the data posted by the View. Clearly this is not a design error but a conceptual problem: the Model Binder has simply no information about how map one structure into the other!
The example of the date can be solved by defining a custom Model Binder for the DateTime type. There, you can put the information on how to recompose the three separate fields into the original DateTime. However, this will be a poor choice for the following reasons:
- All DateTime properties will be mapped in the same way! We have not gained freedom in the Map between the View and the View Model! We have simply moved from a standard map to another standard map! The way a property is displayed may depend on the context, so we need the freedom to specify dynamically this map for each instance of a type.
- The right place where to specify information about this map is the same place where we defined what map to use: the View! Not the Global.asax where we add the custom Model Binders.
- Conceptually the only information that is needed to reconstruct the View Model is the way the two structures are mapped one into the other. By contrast to write a model binder we are required to handle MVC specific data structure. This means two things:
- More code to write
- Difficulties in the testing because of the dependencies from complex data structures that needs to be simulated by the test classes.
Conceptually, the only way to solve the above problems is by putting references to the descriptions of the maps used in the Html pages. This way, we can define dynamically the maps when we render the involved elements in the View.
The MVC Controls Toolkit uses a default Model Binder that is able to retrieve this map references and to use them to rebuild the View Model. The programmer is only required to write the code that furnishes the minimum information possible for the reconstruction of the model: the map!
The map is defined by providing an implementation of the IDisplayModel interface:
namespace MVCControlsToolkit.Core
{
public interface IDisplayModel:ISafeCreation
{
object ExportToModel(Type TargetType, params object[] context);
void ImportFromModel(object model, params object[] context);
}
}
The ImportFromModel method is called by the method that renders the control. The original data structure to be transformed is passed in the model parameter.
The ExportToModel function, instead, is called by the Data Binder to reconstruct the original data structure after that the class implementing the interface has been filled with the data extracted from the View that was posted back.
The programmer may invoke the transformation defined by the interface implementation with the use of the InvokeDisplay helper method. The class returned by this method invocation can be chained with another transformation by calling another overload of the InvokeDispaly helper that accepts as parameters the newly created class and the interface implementation of the other transformation. At the end the transformed data are rendered into a template by passing them to the RenderIn helper.
The use of the InvokeDisplay helper ensure that the inverse transformation will be applied by the Model Binder when the View is posted back.
Let go see the details of the process described above with the example of the DateTime field that we already referenced before. Obviously, you don’t need to actually implement a DateTime control because the MVCControlsToolkit already comes with a sophisticate DateTimeInput control. This is just a simple example to help the understanding of the concepts exposed.
In this case a possible simple implementation of the IDisplayModel interface is:
public class TestDateTimeDisplay : IDisplayModel
{
[Range(1000, 3000, ErrorMessage = "wrong year")]
public Nullable<int> Year { get; set; }
[Range(1, 12, ErrorMessage = "wrong month")]
public Nullable<int> Month { get; set; }
[Range(1, 31, ErrorMessage = "wrong day")]
public Nullable<int> Day { get; set; }
public object ExportToModel(Type targetType, params object[] context)
{
if (Year == 0 && Month == 0 && Day == 0) return null;
if (!Year.HasValue && !Month.HasValue && !Day.HasValue) return null;
try
{
return new DateTime(Year.Value, Month.Value, Day.Value);
}
catch (Exception ex)
{
throw (new Exception(" {0} has an incorrect date format", ex));
}
}
public void ImportFromModel(object model, params object[] context)
{
Nullable<DateTime> date = model as Nullable<DateTime>;
if(date.HasValue && date.Value != DateTime.MinValue)
{
Year = date.Value.Year;
Month = date.Value.Month;
Day = date.Value.Day;
}
}
}
The call to the ImportFromModel call is used to fill the values of the of the Year, Month and Day properties from the original DateTime. This three properties will be rendered instead of the original DateTime property. When the View is posted back, the Model Binder first fill the Year, Month and Day properties with its normal algorithm for extracting a model from the post back data and then calls the ExportToModel method of the interface to recover the initial DateTime property.
The exception thrown when the date format is wrong is intercepted by the Model Binder and its message is used to produce a validation error associate to the initial DateTime property.
The code to put in the View for rendering our control is;
<%: Html.RenderIn("YearMonthDayDate", Html.InvokeTransform(m => m.BirthDate, new TestDateTimeDisplay())) %>
YearMonthDayDate is a name of a strongly typed template for the type TestDateTimeDisplay.
In order to test this example and to do other experiments you can use a generic template for rendering classes, like this one:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<dynamic>" %>
<table cellpadding="0" cellspacing="0" border="0">
<% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForEdit && !ViewData.TemplateInfo.Visited(pm))) { %>
<% if (prop.HideSurroundingHtml) { %>
<%= Html.Editor(prop.PropertyName) %>
<% } else { %>
<tr>
<td>
<div class="editor-label" style="text-align: right;">
<%= prop.IsRequired ? "*" : "" %>
<%= Html.Label(prop.PropertyName) %>
</div>
</td>
<td>
<div class="editor-field">
<%= Html.Editor(prop.PropertyName) %>
<%= Html.ValidationMessage(prop.PropertyName, "*") %>
</div>
</td>
</tr>
<% } %>
<% } %>
</table>
The template can be put either on the Shared folder or in the folder for a specific controller. However if you put it either within a Display or Edit folder the template will not be retrieved,
In order to test the example you need just:
- Download the MVCControlsToolkit from the CodePlex site here
- Create a new MVC 2 project
- Add the interface implementation to the project
- Add the above template or a specific template as explained above
- Add a DateTime property to one of the View Models of the predefined pages that you find into a newly created project. I have chosen the RegisterModel model in the AccountModels.cs file, that is rendered into the Register.aspx View.
- Add the code for rendering the control into the adequate View, in my case the Register.aspx View. Add also a reference to the namespace where you defined your interface implementation.
- Put a breakpoint in the controller method that handles the post back of the View. In my case, the Register method of the AccountController.cs file, to see how the separated Year, Month, Day fields you insert are recomposed into the original DateTime Property, in my case the BirthDate property.
In the next post I will analyze more advanced features of the MVCControlsToolkit.
Stay Tuned!
Francesco
Tags: MVC Helpers, MVCControlsToolkit, MVC, MVC Controls, DataBinder, MVC Extension