Search code examples
c#model-view-controllermodel-bindingcustom-model-binder

How to bind complex properties within polymorphic model in Asp.Net MVC 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


Solution

  • 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.