I came across an error message recently that I hadn’t seen before:
The error occurred when I tried submitting the following form:
The stack trace suggested it came from the model binder:
[MissingMethodException: Cannot create an instance of an interface.]
System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck) +0
System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark) +113
System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark) +232
System.Activator.CreateInstance(Type type, Boolean nonPublic) +83
System.Activator.CreateInstance(Type type) +6
System.Web.Mvc.DefaultModelBinder.CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) +540
This actually makes a lot of sense. The MVC model binder needs to create an instance of every object in the postback model in order to bind the form values to it. If the postback model contains a property of an interface type (instead of a concrete type) then the model binder won’t be able to create an instance of that property – because it won’t know which concrete type to create, hence the error.
There are several ways to work around this issue, but first let’s review the failing code. The view model contains a list of IPeople which the view loops over, outputting a checkbox for each person in the list. Note that the checkboxes are bound to the IsChecked property on the IPerson interface.
Here’s the HTML with Razor:
@model Blog.Models.PeopleViewModel <h2>People</h2> @using(Html.BeginForm()) { for (int i = 0; i < Model.People.Count; i++) { <label class="checkbox"> @Html.CheckBoxFor(x => Model.People[i].IsChecked) @Model.People[i].Name </label> } <input type="submit" class="btn" /> }
Here’s the code behind:
public class HomeController : Controller { public ActionResult People() { var model = new PeopleViewModel(); model.People = new List<IPerson>(); model.People.Add(new Person { Name = "Dave", IsChecked = false }); model.People.Add(new Person { Name = "John", IsChecked = false }); model.People.Add(new Person { Name = "Mark", IsChecked = false }); return View(model); } [HttpPost] public ActionResult People(PeopleViewModel model) { // TODO - Do something with the selected items return View(model); } } public class PeopleViewModel { public List<IPerson> People { get; set; } } public interface IPerson { bool IsChecked { get; set; } string Name { get; set; } } public class Person : IPerson { public bool IsChecked { get; set; } public string Name { get; set; } }
The problem could be fixed by moving the IsChecked property from the IPerson interface to the PeopleViewModel (implemented as a list of booleans). My preferred approach is to create a parent view model to contain the property, and have the PeopleViewModel inherit it. This allows the IsChecked property to be re-used on other view models:
public class SelectableViewModel { public List<bool> IsChecked { get; set; } } public class PeopleViewModel : SelectableViewModel { public List<IPerson> People { get; set; } }
Here’s the modified HTML with Razor:
@model Blog.Models.PeopleViewModel <h2>People</h2> @using(Html.BeginForm()) { for (int i = 0; i < Model.People.Count; i++) { <label class="checkbox"> @Html.CheckBoxFor(x => Model.IsChecked[i]) @Model.People[i].Name </label> } <input type="submit" class="btn" /> }
Finally, the IsChecked list needs to be initialized and the POST action needs the type of the model parameter changed from PersonViewModel to SelectableViewModel.
public ActionResult People() { var model = new PeopleViewModel(); model.People = new List<IPerson>(); model.People.Add(new Person { Name = "Dave" }); model.People.Add(new Person { Name = "John" }); model.People.Add(new Person { Name = "Mark" }); model.IsChecked = new List<bool> { false, false, false }; return View(model); } [HttpPost] public ActionResult People(SelectableViewModel model) { // TODO - Do something with the selected items return View(model); }