Dive Deep Into MVC - IModelBinder Part 1

Update note: To increase the article quality and regard topics precedency, I've updated some parts of this article.

In these series I tried to demonstrate some aspects of ASP.NET MVC as an issue and solution. It's not simple at all. On one hand, finding a good example is a challenge. On the other hand, The DataAnnotationModelValidator and DefaultModelBinder resolve many validating and binding problems through a subtle design. Therefore we exempt writing further code.

What's a model binder?

As you know, you can pass data from action to view through view data and view model. It's not one way and you can post data from view to action via form parameters, querystring, route parameters and etc. Model binders bind model to these data. All model binders implement IModelBinder interface directly or indirectly. The following list describes available ASP.NET MVC model binders and associated data types.

  • ByteArrayModelBinder(System.byte[]): Maps a browser request to a byte array.
  • LinqBinaryModelBinder(System.Data.Linq.Binary): Maps a browser request to a LINQ System.Data.Linq.Binary object.
  • FormCollectionModelBinder: Maps a browser request to a System.Web.Mvc.FormCollection. (It's internal)
  • HttpPostedFileBaseModelBinder(System.Web.HttpPostedFileBase): Binds a model to a posted file.
  • DefaultModelBinder: Maps a browser request to a data object.

The default binder class is called DefaultModelBinder and will use for any type which you haven't set special binder for that.

The straightforward structure of IModelBinder is as follows.

public interface IModelBinder
{
    object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext);
}

The IModelBinder interface has only a method called BindModel. Most of times you don't need to write a custom model binder. Anyway, I've written a simple example. Check it out.

A model

public class Article
{

    public Article() { }
    public Article(string subject, string @abstract, string url, string[] tags)
    {
        // The code is omitted
    }

    public string Subject { get; set; }
    public string Abstract { get; set; }
    public string Url { get; set; }
    public string[] Tags { get; set; }

}

ModelBinder

public class ArticleModelBinder : IModelBinder
{

    // IModelBinder members
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        HttpRequestBase request = controllerContext.HttpContext.Request;

        string subject = request.Form.Get("Subject");

        if (string.IsNullOrEmpty(subject))
            bindingContext.ModelState.AddModelError("Subject", "The subject is required.");

        string @abstract = request.Form.Get("Abstract");

        if (string.IsNullOrEmpty(@abstract))
            bindingContext.ModelState.AddModelError("Abstract", "The abstract is required.");

        string url = request.Form.Get("Url");

        if (string.IsNullOrEmpty(url))
            bindingContext.ModelState.AddModelError("Url", "The url is required.");

        return new Article(
            subject,
            @abstract,
            url,
            request.Form.Get("Tags").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select((t) => t.Trim()).ToArray()
        );
    }

}

IMO, it's better to use a simple TypeConverter to convert comma delimited string to a string array instead of writing custom binder, in this case. Anyway it's just an instance.

The BindModel method has two parameters that are controllerContext and bindingContext. ControllerContext gives some information about current request and associated controller. And bindingContext presents some information about model and its metadata. In the above example I validated data manually. But ASP.NET MVC 2 provides some facilities for model validation that I'll talk about them in the next parts of this article.

What's a model metadata provider?

To bind a model, we should have some information about that. For instance, type of model, model's properties, model's validators and etc. These information gather by model metadata providers that ModelMetaDataProvider is their base class. As far as I mentioned before, you can access to model's metadata via bindingContext parameter during model binding.

public abstract class SimpleModelBinder : IModelBinder
{

    // Abstract methods
    public abstract object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext);

    // Methods
    protected virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
	// Create model
        object model = this.CreateModel(controllerContext, bindingContext);

	// Iterate through model properties
        foreach (ModelMetadata property in bindingContext.PropertyMetadata.Values)
        {
            // Get property value
            property.Model = bindingContext.ModelType.GetProperty(property.PropertyName).GetValue(model, null);

            // Get property validator
            foreach (ModelValidator validator in property.GetValidators(controllerContext))
            {
                // Validate property
                foreach (ModelValidationResult result in validator.Validate(model))
                {
                    // Add error message into model state
                    bindingContext.ModelState.AddModelError(property.PropertyName + "." + result.MemberName, result.Message);
                }
            }
        }

        return model;
    }

    // IModelBinder members
    object IModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        return this.BindModel(controllerContext, bindingContext);
    }

}

In the above code, I've created a simple base class for custom model binders. It uses model validators to validate bound model. As illustrated, I've used model metadata to get model's properties and their validators. As far as I said, it's better to use DefaultModelBinder. Whereas that works perfectly in different situations. For example, binding array property, complex property and etc. in the next part of this article, I'll talk about writing custom ModelMetadataProvider as well.

Usage

To map a binder to a special type, should register it during application start as follows.

protected void Application_Start()
{
    ModelBinders.Binders.Add(typeof(Article), new ArticleModelBinder());
}

But if you are after to change the default binder, should use something like the code below.

protected void Application_Start()
{
    ModelBinders.Binders.DefaultBinder = new MyDefaultModelBinder();
}

Download source

The article example is available here.

blog comments powered by Disqus