Reputation: 33850
I have a view model that looks like this:
class MyViewModel
{
public string Stuff { get; set; }
public IFoo Foo { get; set; }
}
The GET requests all work fine because I supply both, Stuff
and IFoo
to the view.
However, when posting back, MVC says it can't make an object of an interface.
Now, I have two options:
a) Change the type of the Foo
property to a concrete implementation, which is no big deal; or
b) Do the model binding myself, which is an overkill for what I want. I really don't care about posting back the IFoo
member.
How do I tell MVC not to post back IFoo? I don't have any HTML controls also representing the IFoo
. Properties from the IFoo
, I use as hidden thingies in the view.
Upvotes: 0
Views: 223
Reputation: 2409
I had a similar problem and found this link to be of super help to me.. Custom Model Binder But just in case the link goes dead sometime in the future here is the thought process behind it.
Posting a list of interfaces
If you try to post this form, MVC will throw this error: Cannot create an instance of an interface. This is because the default model binder works by creating an instance of your model (and any properties it has) and mapping the posted field names to it; Section.Title maps to the Title property on the Section object, for example. However, Section.SectionFields[0] presents a problem – you cannot create an instance of an interface (or an abstract class).
The solution is to create and register a custom model binder for the IField class...
First create a model binder and inherit from the DefaultModelBinder class:
public class IFieldModelBinder : DefaultModelBinder {
protected override object CreateModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
Type modelType) {
// Our work here
}
}
Secondly, register it with your application – this is usually done in Application_Start in Global.asax.
protected void Application_Start(Object sender, EventArgs e) {
ModelBinders.Binders.Add(typeof(IField), new IFieldModelBinder());
}
Next cast each IField into its concrete type. I think she used reflection so she added to properties to her Interface FieldClassName, and FieldAssemblyName. Then she put in 2 hidden fields for Class name and Assembly name in her editor template.
@model SectionSummary
@Html.HiddenFor(x => x.FieldClassName)
@Html.HiddenFor(x => x.FieldAssemblyName)
Now, whenever this particular field is posted, the custom model binder will have the information about the actual type available – which means we can use it to cast the IField object and return a model that can be instantiated.
public class IFieldModelBinder : DefaultModelBinder
{
protected override object CreateModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
Type modelType)
{
// Get the submitted type - should be IField
var type = bindingContext.ModelType;
// Get the posted 'class name' key - bindingContext.ModelName will return something like Section.FieldSections[0] in our particular context, and 'FieldClassName' is the property we're looking for
var fieldClassName = bindingContext.ModelName + ".FieldClassName";
// Do the same for the assembly name
var fieldAssemblyName = bindingContext.ModelName + ".FieldAssemblyName";
// Check that the values aren't empty/null, and use the bindingContext.ValueProvider.GetValue method to get the actual posted values
if (!String.IsNullOrEmpty(fieldClassName) && !String.IsNullOrEmpty(fieldAssemblyName))
{
// The value provider returns a string[], so get the first ([0]) item
var className = ((string[])bindingContext.ValueProvider.GetValue(fieldClassName).RawValue)[0];
// Do the same for the assembly name
var assemblyName =
((string[])bindingContext.ValueProvider.GetValue(fieldAssemblyName).RawValue)[0];
// Once you have the assembly and the class name, get the type - I am overwriting the IField object that came in, but I do not think you have to do that
modelType = Type.GetType(className + ", " + assemblyName);
// Finally, create an instance of this type
var instance = Activator.CreateInstance(modelType);
// Update the binding context's meta data
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, modelType);
// Return the instance - which will now be a SummaryField or CommentField - rather than an IField
return instance;
}
return null;
}
}
You will now be able to post your List object – for each IField, the custom model binder will be called to figure out what the concrete type of each object is.
Upvotes: 1
Reputation: 16358
You can exclude properties from binding.
[Bind(Exclude="Foo")]
class MyViewModel
{
public string Stuff { get; set; }
public IFoo Foo { get; set; }
}
[HttpPost]
public ActionResult MyGreatAction([Bind(Exclude="Foo")]MyViewModel model)
{
}
Upvotes: 3