Asbestos Supply

2008-11-07 URI Modelbinders and Updatemodel

So you're developing an application using ASP.NET MVC and you got a Person class with properties:

  1. string FirstName
  2. string LastName
  3. string Email
  4. Uri Webpage

And of course you have a form that collects these 4 pieces of information.  And you build an Action in your PersonController called Create like so:

ActionResult Create(FormCollection form) {
    Person p = new Person();
    UpdateModel(p, form);
}

and you run it thinking you're done and ready to go home and.... get an Exception on the line UpdateModel(p, form).  Why?

The answer has to do with ModelBinders and the IModelBinder interface and how UpdateModel() works.  That was something I couldn't find too much information about so I thought I would write this post.

Basically, the MVC framework provides you with a cool method called UpdateModel that will take all the data from an IValueProvider (not gonna get into this right now) and update the model you send it with that data.  If the model is a complex type, it will iterate over all the properties and try to set them automagically.  But it isn't actually magic.  The reason why your UpdateModel(p, form) line is crapping out is because the ModelBinder doesn't know how to transform a string (e.g. "http://www.google.com") into an object of type Uri.

The nice thing is that you can add your own custom binders by extending IModelBinder.  Here's the code I used for UriModelBinder:

class UriModelBinder : IModelBinder {
    public ModelBinderResult BindModel(ModelBindingContext bindingContext) {
        ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (valueProviderResult != null && !string.IsNullOrEmpty(valueProviderResult.AttemptedValue)) {
            Uri url = new Uri(valueProviderResult.AttemptedValue);
            return new ModelBinderResult(url);
        }


        return new ModelBinderResult(null);
    }
}
  • keep in mind that this will throw an error if the url is not formatted properly (because that's what happens when you try instantiating a new Uri with an improperly formatted url) but will set the Uri to null if the string representing it is blank.  This is intentional, but not necessarily what YOU want.

Very simple here.  Just get the string value that we will use to set the property value and check if it's null or empty.  If so, just return null.  If not, create a new Uri using the attempted value and return that.

Now to get this to work we just need one more thing -- an entry in the Global.asax to register this Binder (otherwise it just sits there and nobody knows about it):

ModelBinders.Binders.Add(typeof(Uri), new UriModelBinder());

that means -- "use this UriModelBinder to convert values to type Uri".

Pretty cool.  You could also create a binder for the entire Person object using this same basic idea.  Google that and you'll see numerous examples.

Here's a free one -- I also built a binder for DateTime? (that's Nullable DateTime):

internal class NullableDateTimeModelBinder : IModelBinder {
    public ModelBinderResult BindModel(ModelBindingContext bindingContext) {
        ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (valueProviderResult != null && !string.IsNullOrEmpty(valueProviderResult.AttemptedValue)) {
            DateTime date = DateTime.Parse(valueProviderResult.AttemptedValue);
            return new ModelBinderResult(date);
        }
        return new ModelBinderResult(null);
    }
}

Pretty simple, huh?