Model Binders in ASP.NET MVC
(
Aug 29 2008 - 08:40:20 AM by
Timothy Khouri) - [
print article]
Hot off the presses, and new to ASP.NET MVC (Preview 5) is an awesome capability that (in my opinion) revolutionizes the way we design web applications. This feature is being touted (by me) as "the ViewState for MVC".
First, the Challenge
A common challenge of web applications is passing complex types and (more difficultly) stateful objects from one page to another. This difficulty is due to the disconnected nature of the web. There have been many attempts to solve this problem (sessions, ViewState, etc), but ASP.NET MVC's new "Model Binders" functionality definitely takes the gold medal for being the cleanest, simplest and most powerful.
We're going to build a couple of simple MVC pages, one that display a list of customers and the other displays their address details. You can download the solution at the end of the article (compiled for Visual Studio 2008 SP1 / ASP.NET MVC Preview 5).
What Are the Benefits?
ASP.NET MVC Model Binders give you the following benefits:
- Complete control over the deserialization of complex types passed into your Action methods. This in itself has many sub-benefits: less bloat in your HTML and in passing objects back to the server, cleaner URL's, etc.
- Unified and testable code. This feature really holds to the "Single Responsibility" (or separation of concerns) principle. This one place is where you will handle restoring your objects from the memento you come up with.
The idea of using a ModelBinder to pass tokenized objects that live in your data model is not the typical usage. You could use them to define how other complex objects that aren't serialized into the query string get passed into your methods, but purpose of this article is to show a more extreme example to demonstrate it's power and capabilities.
A more common usage for ModelBinders would be to build your complex object from a form post. Example:
public override object GetValue(ControllerContext ctx,
string modelName, Type modelType, ModelStateDictionary state)
{
Customer customer = new Customer();
customer.FirstName = ctx.HttpContext.Request["FirstName"];
customer.LastName = ctx.HttpContext.Request["LastName"];
return customer;
}
By the end of this article, we'll see how easy it is pass complex "Customer" objects around our application and we'll talk about how to create unit tests to ensure the quality of our methods. Our customer information will be stored in a SQL database, and the customer class will be generated by the LINQ to SQL as a concrete representation of a record in our database.
Before we dive into our application, let's take a look at a snippet of code and see how MVC model binders make a difference.
MVC ModelBinder vs. Traditional ASP.NET
To demonstrate this functionality simply, consider the following scenario. You're looking at the "List" page for your users. The list of users is in an HTML <table>, and the first column is a link to that user's "Edit" page.
Let's assume our link will produce the following URL: "/Users/Edit.aspx?UserID=123". In traditional ASP.NET, we would probably have the following code at the top of our page.
private void Page_Load(object sender, EventArgs e)
{
int userID = 0;
if (int.TryParse(Request["UserID"], out userID) == false)
{
}
User currentUser = GetUserFromDatabase(userID);
if (currentUser == null)
{
}
}
This may not seem that bad, but if you think about it, we'll have to re-write this code in every page that we want to pass a user object to and for every parameter that we need to have passed to the page. This approach isn't very object oriented, and you're constantly reminded of the fact that you're just grabbing data out of the query string.
Now, notice the difference between what you have to do in traditional ASP.NET, and ASP.NET MVC (thanks to Model Binders):
public ActionResult Edit(User currentUser)
{
if (currentUser == null)
{
}
}
Notice how much simpler that was? OK, so you might be wondering "where is the code?"... "How is the 'currentUser' parameter being passed in?" Now it's time to see the magic.
Creating and Registering Model Binders
Because ASP.NET MVC is designed to make things simple and easy to use, creating and registering your own Model Binders is a quick task. There are really only two things you have to do to handle the 'serialization / deserialization' part, and there is one simple line of code to register your custom model binder with the MVC framework. Let's break it down here.
First of all, we need to decide how we want to 'tokenize' our object (meaning, what string representation do we want to make so that we can get our object back later). You currently have three options here, but I would like to see a fourth one by the time MVC goes live (I'll explain in a minute).
Essentially, the three choices you have all boil down to the same thing:
- Make your class implement the IConvertible interface, and provide your code for how it should 'convert' to a string.
- Make your class implement the IFormattable interface, and do the same as the above.
- Or, simply override "ToString" and put your logic there.
I'm going to use this third approach because it's simple, but you certainly could go with one of the above. In some cases, it would be best to use the first choice (the IConvertible). You may want to choose this so that you can free up your "ToString" method to provide a user-friendly looking string representation of your object, and then use the ConvertTo method to provide your memento.
Example: 'ToString()' might return "Khouri, Timothy", and 'ConvertTo()' might return 'ID:1234'.
I mentioned that I would like to see a fourth option soon, and this one I think is the most powerful. As you will see below, ModelBinders give you the option to "restore" your object from the string token that is provided. I would like to also see the code that converts *to* that token in this same model binder.
There are many great reasons for this, but the most important I can think of is to give you (the developer) the ability to convert classes that you didn't write into whatever format you want. i.e.: DateTime to serialize as "yyyyMMdd".
Because my "Customer" class is being auto-generated by LINQ-to-SQL, I'm going to create a class file that extends it a little to provide my own "ToString" method. This is not an "extension method", but rather just a partial class file. Here's my code:
public partial class Customer
{
public override string ToString()
{
return Convert.ToBase64String(
BitConverter.GetBytes(this.ID)
).Replace("=", "_");
}
}
Basically, this will convert any instance of a Customer class to a string that looks something like "CwAAB0__". Now, I need to make my ModelBinder that will take that string, convert it back to the ID of the Customer, and then re-create the Customer object (which in this case, will be a simple task for LINQ-to-SQL).
As was mentioned before, creating our own custom ModelBinder is very simple. We're going to inherit from "DefaultModelBinder", and then override the "ConvertType" method. The 'value' object that gets passed in could be a string, or an array of strings, so I'll have to check for that. Here's the code:
public class MyCustomerBinder : DefaultModelBinder
{
protected override object ConvertType(CultureInfo culture, object value, Type destinationType)
{
if (destinationType != typeof(Customer))
{
return base.ConvertType(culture, value, destinationType);
}
string customerIDString = value as string;
if (customerIDString == null && value is string[])
{
customerIDString = ((string[])value)[0];
}
customerIDString = customerIDString.Replace("_", "=");
int customerID = BitConverter.ToInt32(Convert.FromBase64String(customerIDString), 0);
return MyDataAccessLayer.GetCustomerByID(customerID);
}
}
That's it! We've now created a ModelBinder that will tell MVC how to convert that token back into a real, tangible Customer object. Next we'll see how to register and use our custom parameter converting model binder.
In our global application file (Global.asax), we are already registering our routing information for MVC (which tells the framework where to look for our Controllers and Actions). Now we're going to add this simple line of code that tells the MVC framework to how to reconstitute our Customer class. Here's the code:
ModelBinders.Binders.Add(typeof(Customer),
new MyCustomerBinder());
Seeing the Results
There is no better way to fully appreciate what is happening here other than seeing the result for yourself. In my 'list customers' page, I'm building the link to the details page in a very object-oriented way:
<%
foreach (Customer customer in this.ViewData.Model)
{
this.Writer.Write("<li>");
this.Writer.Write(this.Html.ActionLink<HomeController>(
c => c.Details(customer), customer.FullName));
this.Writer.Write("</li>");
}
%>
Here's is what our page will look like:
And here is the details page. Notice the URL. If you scroll up you'll see that I never told MVC to call the parameter "targetCustomer", but MVC automatically figured that out because that's what the parameter is named in my "Details" method.
Testing Your Converter
As I stated above, I wanted to mention why this approach is good in terms of testability. Basically, because this bit of functionality is so small and specific, it is fully testable. You can use your favorite 'mock' framework to create a FakeHttp request with your token in the query string, and see if it correctly creates the Customer class.
As a side note: don't forget that my model binder is hitting the data access layer, so if you are going to create unit tests you'll want to also mock the DAL as well.
And now, here is the solution (compiled for Visual Studio 2008 SP1, ASP.NET MVC Preview 5): SingingEels_MVC_ModelBinders.zip