Water Cooler v2
Water Cooler v2

Reputation: 33850

Make MVC not post back one of the members in the model which is an interface

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

Answers (2)

Robert Snyder
Robert Snyder

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

Rowan Freeman
Rowan Freeman

Reputation: 16358

You can exclude properties from binding.

Via the model

[Bind(Exclude="Foo")]
class MyViewModel
{
    public string Stuff { get; set; }

    public IFoo Foo { get; set; }
}

Via the action

[HttpPost]
public ActionResult MyGreatAction([Bind(Exclude="Foo")]MyViewModel model)
{

}

Upvotes: 3

Related Questions