Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom Model Binder inheriting from DefaultModelBinder

I'm attempting to build a custom model binder for MVC 4 that will inherit from DefaultModelBinder. I'd like it to intercept any interfaces at any binding level and attempt to load the desired type from a hidden field called AssemblyQualifiedName.

Here's what I have so far (simplified):

public class MyWebApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        ModelBinders.Binders.DefaultBinder = new InterfaceModelBinder();
    }
}

public class InterfaceModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, 
        ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType.IsInterface 
            && controllerContext.RequestContext.HttpContext.Request.Form.AllKeys.Contains("AssemblyQualifiedName"))
        {
            ModelBindingContext context = new ModelBindingContext(bindingContext);

            var item = Activator.CreateInstance(
                Type.GetType(controllerContext.RequestContext.HttpContext.Request.Form["AssemblyQualifiedName"]));

            Func<object> modelAccessor = () => item;
            context.ModelMetadata = new ModelMetadata(new DataAnnotationsModelMetadataProvider(),
                bindingContext.ModelMetadata.ContainerType, modelAccessor, item.GetType(), bindingContext.ModelName);

            return base.BindModel(controllerContext, context);
        }

        return base.BindModel(controllerContext, bindingContext);
    }
}

Example Create.cshtml file (simplified):

@model Models.ScheduledJob

@* Begin Form *@
@Html.Hidden("AssemblyQualifiedName", Model.Job.GetType().AssemblyQualifiedName)

@Html.Partial("_JobParameters")
@* End Form *@

The above partial _JobParameters.cshtml looks at the Model.Job's properties and builds the edit controls, similar to @Html.EditorFor(), but with some extra markup. The ScheduledJob.Job property is of type IJob (interface).

Example ScheduledJobsController.cs (simplified):

[HttpPost]
public ActionResult Create(ScheduledJob scheduledJob)
{
    //scheduledJob.Job here is not null, but has only default values
}

When I save the form, it interprets the object type correctly and gets a new instance, but the properties of the object are not being set to their appropriate values.

What else do I need to do to this to tell the default binder to take over the property binding of the specified type?

like image 396
Albert Bori Avatar asked May 25 '13 07:05

Albert Bori


2 Answers

This article showed me that I was over-complicating the model binder. The following code works:

public class InterfaceModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType.IsInterface)
        {
            Type desiredType = Type.GetType(
                EncryptionService.Decrypt(
                    (string)bindingContext.ValueProvider.GetValue("AssemblyQualifiedName").ConvertTo(typeof(string))));
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, desiredType);
        }

        return base.BindModel(controllerContext, bindingContext);
    }
}
like image 78
Albert Bori Avatar answered Oct 16 '22 12:10

Albert Bori


With MVC 4 it is easy to override the messages, if that is all you might need in a custom model binder:

    protected void Application_Start(object sender, EventArgs e)
    {
        //set mvc default messages, or language specifc
        ClientDataTypeModelValidatorProvider.ResourceClassKey = "ValidationMessages";
        DefaultModelBinder.ResourceClassKey = "ValidationMessages";
    }

Then create resource file named ValidationMessages with entries like this:

NAME: FieldMustBeDate 
VALUE: The field {0} must be a date. 
NAME: FieldMustBeNumeric 
VALUE: The field {0} must be a number

.

We did this for a compliance failure. Our security scan did not like that a javascript injection would come back and appear in the Validation Messages and execute. By using this implementation we are overriding the default messages which return the user provided value.

like image 27
Joe Kahl Avatar answered Oct 16 '22 13:10

Joe Kahl