user3720939
user3720939

Reputation: 119

Using a custom DefaultModelBinder for MVC4 with interface does not populate additional properties

OK.. So I have an interface that I'm passing back to the controller when saving that looks kind of like this (removing most just to get the idea across). I can add more details if that is needed, but I'd rather not overwhelm everyone.

public interface IProjectElement
{        
    string Name { get; set; }
    int ElementTypeID { get; set; }
    ...
}
public class BaseProjectElement : IProjectElement
{

    public int ElementTypeID { get; set; }
    public string AdditionalInformation { get; set; }
    ... 
}    

And then I have several implementations of the interface, but here's one for example...

public class SQLElement : BaseProjectElement
{
    public int? DatabasePlatformID { get; set; }
}

From within Global.asax - start

ModelBinders.Binders.Add(typeof(IProjectElement), new ProjectElementModelBinder());

So here's the issue. The code below determines the type of object and creates the appropriate object. What I think happens is that I can see that all the values get sent in the bindingContent.ValueProvider data, but what is interesting is that the properties collection only lists the ones for the interface. The base.CreateModel will create the type I need, but only populate the object with the data from the interface. I can get around it with some ugly code to set additional properties where I am setting DatabasePlatformID, but some of my classes have several additional properties. I'm probably missing something obvious here. This isn't very maintainable setting the additional properties like this.

public class ProjectElementModelBinder : DefaultModelBinder 
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        string modelName = bindingContext.ModelName;
        var elementType = (int)bindingContext.ValueProvider.GetValue(string.Format("{0}.ElementType", modelName)).ConvertTo(typeof(int));
        Type instantiationType = null;
        if (elementType == 1)
        {
            instantiationType = typeof(IISElement);
        }
        else if (elementType == 2)
        {
            instantiationType = typeof(SharePointElement);
        }
        else if (elementType == 3)
        {
            instantiationType = typeof(SQLElement);
        }
        else if (elementType == 12)
        {
            instantiationType = typeof(SharedElement);
        }
        else
        {
            instantiationType = typeof(BaseProjectElement);
        }
        var obj = base.CreateModel(controllerContext, bindingContext, instantiationType);
        if (elementType == 3)
        {
            PropertyInfo pi = obj.GetType().GetProperty("DatabasePlatformID");
            int databasePlatformID = (int)bindingContext.ValueProvider.GetValue(string.Format("{0}.DatabasePlatformID", modelName)).ConvertTo(typeof(int));
            pi.SetValue(obj, databasePlatformID, null);
        }
        return obj;
    }
}

Note that I also tried to do it using the code I see everywhere else, but what I am seeing there is even worse where the object does not get populated at all.

        var obj=Activator.CreateInstance(instantiationType);
        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType);
        bindingContext.ModelMetadata.Model = obj;
        return obj;

Here is my controller

public ActionResult SaveElements(Guid projectID, List<IProjectElement> selectedElements, List<ProjectElementRelationshipsDAO> selectedRelationships)
    {
        using (ManagedHostingHelper helper = new ManagedHostingHelper())
        {
            BasePageViewModel viewModel = helper.SaveElements(projectID, selectedElements, selectedRelationships);
            return Json(new
            {
                ViewModel = viewModel,
                JsonRequestBehavior.AllowGet
            });
        } 
    }

And finally, here is the javascript that calls the controller. Using knockout.js..

self.SaveElements = function () {
    var saveURL = new UrlHelper().SaveElements();
        data: ko.toJSON({ projectID: self.ProjectID(), selectedElements: self.SelectedElements(), selectedRelationships: self.SelectedRelationships() }),
        type: "post", contentType: "application/json",
        success: function (result) {
            if (result.ReturnStatus == true) {
                self.GoForward();
            }
            else { alert('error'); }
        }
    });
};

SelectedElements is a knockout.js observableArray. Below are the javascript objects that could get passed.. Note that this array will hold a collection of elements that implement IProjectElement.. I'm also not showing all the different types, but they all have a basic set of data and add additional properties.

// base class - see class diagram in folder as types should match this fairly closely
function BaseProjectElement(data) {
    var self = this;
    self.ProjectElementID = ko.observable(data.ProjectElementID);
    self.ElementName = ko.observable(data.ElementName);
    ... more properties
    self.BaseElementValidationGroup = ko.validatedObservable({
        Name: self.Name
    });
    self.IsBaseElementValid = ko.computed(function () {
        return self.BaseElementValidationGroup().errors().length == 0;
    });
};
function SQLElement(data) {
    var self = this;
    ko.utils.extend(self, new BaseProjectElement(data));
    self.DatabasePlatformID = ko.observable(data.DatabasePlatformID).extend({ required: { params: true, message: ' ' } });
    self.SQLElementValidationGroup = ko.validatedObservable({
        DatabasePlatformID: self.DatabasePlatformID
    });
    self.IsSQLElementValid = ko.computed(function () {
        return (self.SQLElementValidationGroup().errors().length == 0) && self.IsBaseElementValid();
    });
};

Upvotes: 0

Views: 151

Answers (1)

user3720939
user3720939

Reputation: 119

OK.. I thought I did something similar to this at one point, but here is what worked for me.. The setting of the ModelMetaData vs the line I've seen in several places like `

bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType);

VS

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        string modelName = bindingContext.ModelName;
        var elementType = (int)bindingContext.ValueProvider.GetValue(string.Format("{0}.ElementType", modelName)).ConvertTo(typeof(int));
        if (elementType == 1)
        {
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => new IISElement(), typeof(IISElement));
        }
        else if (elementType == 2)
        {
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => new SharePointElement(), typeof(SharePointElement));
        }
        else if (elementType == 3)
        {
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => new SQLElement(), typeof(SQLElement));
        }
        else if (elementType == 12)
        {
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => new SharedElement(), typeof(SharedElement));
        }
        else
        {
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => new BaseProjectElement(), typeof(BaseProjectElement));
        }
        return base.BindModel(controllerContext, bindingContext);
    }

Upvotes: 0

Related Questions