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")) {
var typeName = (string) bindingContext
.ValueProvider
.GetValue("ModelType")
.ConvertTo(typeof(string));
var modelType = Type.GetType(typeName);
bindingContext.ModelMetadata =
ModelMetadataProviders
.Current
.GetMetadataForType(null, modelType);
}
return base.BindModel(controllerContext, bindingContext);
}
Do You Have the 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):
[TestFixture, RunOnWeb]
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
});
Assert.IsInstanceOf<ArticleModel>(
response.ActionMethodParameters["model"]);
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!
Codeproject