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?
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);
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With