Consider this view model that uses two custom validators. It posts back to the controller in an enumerable collection.
public class FormFieldViewModel
{
public Guid FormFieldKey { get; set; }
public string Name { get; set; }
// Utilize a string[] array since multiple-selection items will require this (we can assume array[0] for all others)
[RequiredIf("IsRequired", true, ErrorMessage = "This field is required")]
[RegExRemote("RegEx", ErrorMessage = "Your entry is not valid")]
public string[] Value { get; set; }
//public string Options { get; set; }
public IEnumerable<SelectListItem> Options { get; set; }
public string ListEnum { get; set; }
// These fields assist the custom validators used in this view model
public bool IsRequired { get; set; }
public string RegEx { get; set; }
public string RegExError { get; set; }
}
Consider the controller method:
[HttpPost]
public ActionResult Display(IEnumerable<FormFieldViewModel> formresponses, Guid id)
{
// TODO: Make a custom modelbinder
if (ModelState.IsValid)
{
// Do stuff ...
}
return View(formresponses);
}
How can one make a custom ModelBinder that can navigate the IEnumerable, populate some missing values, etc., and allow the ModelState.IsValid to be evaluated correctly (within a custom ModelBinder context)?
This one has had me searching for days... As a note: I have been able to get the following IModelBinder implementation working to CREATE the entire model that I need. However it still has two key issues. (1) It doesn't perform validation correctly to get the ModelState.IsValid set correctly, and (2) It seems that the IModelBinder instance doesn't allow easy IoC due to its inherent constructors. Help!
(Data-specific binder)
public class FormFieldModelBinder : SimpleModelBinder
{
public override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
List<FormFieldViewModel> formresponses = new List<FormFieldViewModel>();
var form = controllerContext.HttpContext.Request.Form;
foreach (var key in form.AllKeys)
{
// All that should be required is the FormFieldKey and its updated Value
if (key.IndexOf(".FormFieldKey") > -1)
{
var thisGuid = Guid.Empty;
if (Guid.TryParse(form[key], out thisGuid))
{
var formresponse = new FormFieldViewModel();
// Todo: find a way to use IoC for this rather than direct instantiation
var dbForm = new EntityFormRepository();
var thisField = dbForm.GetFieldByKey(thisGuid);
// Populate values ...
formresponses.Add(formresponse);
}
}
}
return formresponses;
}
(Custom binder)
public abstract class SimpleModelBinder : IModelBinder
{
// Abstract methods
public abstract object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext);
// Methods
protected virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//if (bindingContext.ModelType == typeof(IEnumerable<FormFieldViewModel>))
//{
object model = this.CreateModel(controllerContext, bindingContext);
//foreach (var formField in (List<FormFieldViewModel>)model)
//{
foreach (ModelMetadata property in bindingContext.PropertyMetadata.Values)
{
// Get property value
property.Model = bindingContext.ModelType.GetProperty(property.PropertyName).GetValue(model, null);
// Get property validator
foreach (ModelValidator validator in property.GetValidators(controllerContext))
{
// Validate property
foreach (ModelValidationResult result in validator.Validate(model))
{
// Add error message into model state
bindingContext.ModelState.AddModelError(property.PropertyName + "." + result.MemberName, result.Message);
}
}
}
//}
return model;
//}
//return this.CreateModel(controllerContext, bindingContext);
}
// IModelBinder members
object IModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
return CreateModel(controllerContext, bindingContext);
// return this.BindModel(controllerContext, bindingContext);
}
}