SingingEels : Development Community & Resource

Login

Articles

  • ADO.NET (2)
  • ASP.NET (36)
  • LINQ (4)
  • Security (2)
  • Silverlight (3)
  • SQL (7)
  • Standards (5)
  • WCF (2)

Syndication

  • Articles RSS
  • Blogs RSS

Contribute

  • Our Authors List
  • Member Sign-Up
  • Suggestions Box
ASP.NET Hosting with MS SQL 2008 – Click Here!

Model Binders in ASP.NET MVC

(Aug 29 2008 - 08:40:20 AM by Timothy Khouri) - [print article]
ASP.NET Hosting with MS SQL 2008 – Click Here!

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"];

   // ... other properties ...


   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 ID wasn't supplied, so redirect out.

   }

   User currentUser = GetUserFromDatabase(userID);

   if (currentUser == null)
   {
       // Bad User ID, so redirect out.

   }

   // ...

}

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)
   {
       // User not supplied, so redirect out.

   }

   // ...

}

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()
   {
       // I could just return "ID.ToString()", but I

       // want to be a little silly to prove that I have full

       // control over how my memento is going to look :)


       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)
   {
       // This class is only meant to convert Customer objects.

       if (destinationType != typeof(Customer))
       {
           return base.ConvertType(culture, value, destinationType);
       }

       // Get the ID that is being passed in.

       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);

       // Grab the customer from the database.

       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:

// Register our ModelBinder.
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>");

       // Create a link that poitns to the "Details" method, passing

       // in the *customer* object. The link should display the full

       // name of the customer.

       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:

MVC Model Binders Demo Screen Shot

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.

MVC Model Binders Demo Screen Shot (Details Page)

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

Update: Sep 17 2008

For a more indepth explaination of testing, including a demo project, continue reading here: Test Driven Development with ASP.NET MVC.

  • Aug 30 2008 - 01:38:32 AM AnonymousCow

    Timothy, I am almost certain you've completely missed the mark in extending DefaultModelBinder the way you have. Model binding, or data binding in general, is used to sync the properties of one object to another. For example, you would sync the value of a text field to a model property.

    Not only that, but you should be using more restful-style URLs, so you would have Customer/Details/1 instead of what you have.

    One last point, and it is major--you wouldn't every want any database interaction going on during the binding process.

    Ugh.. it is a nice article in spirit, but the risk is people are going to make these mistakes in real code...

  • Aug 30 2008 - 08:26:53 AM Timothy Khouri

    Good points - I'll address them in order of easy to more lengthy :)

    1) About the URL's - You're absolutely right, I basically cut that corner for the sake of time (not much time, I know).

    2) Regarding the database interaction during binding: This one is difficult for me to address, because I think it would take a 3 hour conversation in front of a white board. In many ways, and in many circumstances I would agree with you. I think that in time, MVC will have a more expressed way of doing what I'm trying to achieve in the article, and using a ModelBinder may not be it.

    Lastly, I might have missed the original intent of the extending the DefaultModelBinder... but all that is in the release notes on the subject is "Custom binders allow you to define complex types as parameters to an action method." - again, I think more clarity will come in time, and I'll jump all over that when it does.

    I really appreciate your comments. I'm always worried that my articles won't be clear on what I was thinking, and I would hate to see someone doing bad code based on what I said.

    Thanks,

    -Timothy

  • Sep 19 2008 - 06:46:50 AM MatsL

    I thought the whole point of ModelBinders was to create objects from fields in the form. The only responsibility of the binder would then be to match form fields to properties on the object.

    In your example I think the action method should take an integer as a parameter and fetch the customer from the database there. That would play nice with restful url's.

  • Sep 20 2008 - 10:45:03 AM Tomek

    Hi Timothy,

    Overall nice job describing custom model binders, I think comparing model binders to ViewState is a bit of a stretch though. Model binders are about mapping form fields and their values to your server side objects, something the ViewState has no role in doing.

You must be logged in to add comments. If you have not already done so, you can create an account here. If you already are a member, you first need to login before you can comment.

Developer / Architect / Author

People to Follow

Experts in the categories related to this article.

  • Jonathan Carter

Related Blogs

These are the most recent blog posts related to this article.

  • A Change to the MVC "ActionSelectionAttribute"?
  • How to Handle "Side Content" in ASP.NET MVC
  • LINQ to SQL - Am I Hitting The Database?
  • ASP.NET MVC - Issue with CSS Class Name on ActionLinks
  • Site Updates, Bug Fixes and Syndication in .NET 3.5

Related Ads

SingingEels.com as of Jan 05 2009 - 09:45:55 PM - (0.1875324)