4

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

3 Answers 3

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

Sign up to request clarification or add additional context in comments.

2 Comments

thanks @Jenish but wouldn't all the properties be empty then (not just the complex property)?
yes, I couldn't get this to work but I haven't spent a lot of time on it today.
1

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.

Comments

0

Try to add default constructor to your ModelDerivedA to initialize MySomeType

public class ModelDerivedA : ModelBase
{
    public ModelDerivedA()
    {
        MySomeType = new SomeType();
    }
}

4 Comments

What do you mean, please explain? This constuctor should create an instance of the SomeType.
But will it solve the problem by populating the form field inside?
@Max, thanks, this is an issue in my example, when fixed it does not fix the binding issue.
@davy it does not fix binding issue.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.