2

I need to create a dynamic input form based on a derived type but I cannot get complex properties bound properly when passed to the POST method of my controller. Other properties bind fine. Here is a contrived example of what I have:

Model

public abstract class ModelBase {}

public class ModelDerivedA : ModelBase
{       
    public string SomeProperty { get; set; }       
    public SomeType MySomeType{ get; set; }

    public ModelDerivedA()
    {
        MySomeType = new SomeType();
    }
}

public class SomeType 
{             
    public string SomeTypeStringA { get; set; }
    public string SomeTypeStringB { get; set; }         
}

Custom Model Binder

The binder is based on this answer: polymorphic-model-binding

public class BaseViewModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
        var type = Type.GetType(
            (string)typeValue.ConvertTo(typeof(string)),
            true
        );
        if (!typeof(ModelBase).IsAssignableFrom(type))
        {
            throw new InvalidOperationException("The model does not inherit from mode base");
        }
        var model = Activator.CreateInstance(type);
        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
        return model;
    }
}

Controller

[HttpPost]
public ActionResult    GetDynamicForm([ModelBinder(typeof(BaseViewModelBinder))] ModelBase model)
{
   // model HAS values for SomeProperty 
   // model has NO values for MySomeType
}

View Excerpt

@Html.Hidden("ModelType", Model.GetType())
@Html.Test(Model);

JavaScript

The form is posted using $.ajax using data: $(this).serialize(), which, if I debug shows the correct populated form data.

All properties are populated in the model excluding those of SomeType. What do I need to change to get them populated?

Thanks

Community
  • 1
  • 1
davy
  • 4,312
  • 9
  • 43
  • 64

3 Answers3

1

Values are not being populated because you are creating new instance of type like following:

var model = Activator.CreateInstance(type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
return model;

and returning the same model instead which is not correct.

do something like below.

ValueProviderResult valueResult;
bindingContext.ModelState.SetModelValue("ModelType", valueResult);       
return valueResult;

Here is very good discussion on modelBinder.

http://odetocode.com/blogs/scott/archive/2009/05/05/iterating-on-an-asp-net-mvc-model-binder.aspx

Jenish Rabadiya
  • 6,480
  • 5
  • 31
  • 57
  • thanks @Jenish but wouldn't all the properties be empty then (not just the complex property)? – davy Feb 18 '15 at 13:29
  • yes, I couldn't get this to work but I haven't spent a lot of time on it today. – davy Feb 18 '15 at 14:26
0

Try to add default constructor to your ModelDerivedA to initialize MySomeType

public class ModelDerivedA : ModelBase
{
    public ModelDerivedA()
    {
        MySomeType = new SomeType();
    }
}
DavidG
  • 95,392
  • 10
  • 185
  • 181
Max Brodin
  • 3,712
  • 1
  • 11
  • 21
0

I have solved my immediate issue by:

  1. get an instance of FormvalueProvider (to get access to what has been posted)
  2. recursively going through my model and setting each property value to the matching value in the FormValueProvider

    private FormValueProvider vp;
    
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
    
        var type = Type.GetType(
            (string)typeValue.ConvertTo(typeof(string)),
            true
        );
        if (!typeof(ModelBase).IsAssignableFrom(type))
        {
            throw new InvalidOperationException("Bad Type");
        }
    
        var model = Activator.CreateInstance(type);
    
        vp = new FormValueProvider(controllerContext);
    
        bindingContext.ValueProvider = vp;
        SetModelPropertValues(model);
    
        return model;        
    }
    

And the recursion, based on this answer here to print properties in nested objects

    private void SetModelPropertValues(object obj)
    {
        Type objType = obj.GetType();
        PropertyInfo[] properties = objType.GetProperties();
        foreach (PropertyInfo property in properties)
        {
            object propValue = property.GetValue(obj, null);
            var elems = propValue as IList;
            if (elems != null)
            {
                foreach (var item in elems)
                {
                    this.SetModelPropertValues(item);
                }
            }
            else
            {                   
                if (property.PropertyType.Assembly == objType.Assembly)
                {                        
                    this.SetModelPropertValues(propValue);
                }
                else
                {
                  property.SetValue(obj, this.vp.GetValue(property.Name).AttemptedValue, null);
                }
            }
        }
    }

Anyone using this may need to make it more robust for their needs.

I would be very intersted to hear of any drawbacks to this as a general approach to this kind of problem.

However, I'm hoping that this post helps in some situations.

Community
  • 1
  • 1
davy
  • 4,312
  • 9
  • 43
  • 64