Custom Asp.Net Model Binders series, part 3: Subclassing your models

This is the third part in the series about custom Asp.Net MVC Model Binders and Value Providers. Part 1 is about two ways of using DateTime.Now as an Action Method parameter for better testability, and Part 2 is about building a Value Provider for the Http Header values. Read on.

What's up with subclassing your models

Often you need to use inheritance in your domain model. For example, you might have to model a book with a list bibliographic references. Each reference might be to an article in a magazine, to another book, to a Web page etc. So, you create a Reference abstract class, and a couple of subclasses for different types of references. You might also want to create a parallel inheritance chain for your models.

Now, suppose you have a list of references. Each row has a link to the page where you can edit the corresponding reference object. While different types of references have different fields to edit, you don't want to create a separate page for each type. Displaying a reference is simple: you just call Html.EditorFor() inside your form, and the fairies will generate the necessary fields for that particular kind of reference. The problem is how to get the new values back.

Suppose you have the following action method:

[HttpPost]
public ActionResult Update(ReferenceModel model) {
	//...
}

The default binder will try to create an instance of ReferenceModel, and will fail, since this is an abstract class. So, we'll have to use a different binder. The one that is smart enough so that it could create an instance of the concrete type.

Implementing the SubclassingBinder

In order to do it, we'll have to provide the name of the model's type. We'll do it via a hidden field called ModelType:

<input type="hidden" name="ModelType" value="<%=this.Model.GetType() %>"/>

One would be tempted to override the CreateModel method, but that wouldn't be enough. The model would be created, but it would not be populated with the subclass-specific properties. The binder would still use the metadata of the base abstract class, so the properties specific to the concrete class will not be picked up.

So, we're going to override the BindModel method and "correct" the model type, then let the binder create and bind a model of the requested type for us. Here's the code:

public override object BindModel(ControllerContext controllerContext, 
	ModelBindingContext bindingContext) {
	if (bindingContext.ValueProvider.ContainsPrefix("ModelType")) {
		//get the model type
		var typeName = (string) bindingContext
			.ValueProvider
			.GetValue("ModelType")
			.ConvertTo(typeof(string));
		var modelType = Type.GetType(typeName);
 
		//tell the binder to use it
		bindingContext.ModelMetadata = 
			ModelMetadataProviders
			.Current
			.GetMetadataForType(null, modelType);
	}
	return base.BindModel(controllerContext, bindingContext);
}

Do you have teh tests?

Oh yes we have! As always, we're going to test our code with the help of Ivonna, our favorite Asp.Net testing tool. Here's the test. It verifies two things: given the model type and a property value, a model of the correct type is created and the property is filled with that value (disclaimer: I'm usually opposed to several asserts in a test, but I'm doing it here for clarity):

[TestFixtureRunOnWeb]
public class ModelSubclassing {
	private const string NEW_ARTICLE_NAME 
		= "On the meaning of death";
 
	[Test]
	public void BinderCreatesArticleModelWithValues() {
		var response = new TestSession()
			.Post("/Sample/UpdateReference",
			new {
				ModelType = typeof(ArticleModel)
					.ToString(),
				ArticleName = NEW_ARTICLE_NAME
			});
			
		//verify that we have the corect model type
		Assert.IsInstanceOf<ArticleModel>(
			response.ActionMethodParameters["model"]);
		//verify that we have filled 
		//the concrete type specific properties
		var model = 
			(ArticleModel)response
				.ActionMethodParameters["model"];
		Assert.AreEqual(NEW_ARTICLE_NAME, model.ArticleName);
	}
}

That's all for today. You can grab the code from GitHub. Comments are welcome!
blog comments powered by Disqus

Latest blog posts

Powered by FeedBurner

Archives