MVC 3 Complicated validation of models - asp.net-mvc-3

Current validation method for use in MVC 3 seems to be ValidationAttributes. I have a class validation that is very specific to that model and has interactions between a few properties.
Basically the model has a collection of other models and they are edited all in the same form. Let's call it ModelA and it has a collection of ModelB. One thing I might have to validate is that the sum of the some property of ModelB is less then a property of ModelA. The user has X number of points he can divide among some options.
ValidationAttributes are very generic and I'm not sure they are suited for this job.
I have no idea how IDateErrorInfo is supported in MVC 3 and whether it works straight out of the box.
One way would be to validate through a method but that means I can't do a clientside validation.
What is the proper way to do something like this? Are there any more options I have? Am I underestimating the power of ValidationAttribute?

IDateErrorInfo
IDateErrorInfo is supported by the MVC framework (a Microsoft tutorial can be found here). The default model binder will be reponsible for recreating model objects by binding the html form elements to the model. If the model binder detects that the model implements the interface then it will use the interface methods to validate each property in the model or to validate the model as a whole. See the tutorial for more information.
If you wanted to use client side validation using this method then (to quote Steve Sanderson) 'the most direct way to take advantage of additional validation rules is to manually generate the required attributes in the view':
<p>
#Html.TextBoxFor(m.ClientName, new { data_val = "true", data_val_email = "Enter a valid email address", data_val_required = "Please enter your name"})
#Html.ValidationMessageFor(m => m.ClientName)
</p>
This can then be used to trigger any client side validation that has been defined. See below for an example of how to define client side validation.
Explicit Validation
As you mentioned, you could explicity validate the model in the action. For example:
public ViewResult Register(MyModel theModel)
{
if (theModel.PropertyB < theModel.PropertyA)
ModelState.AddModelError("", "PropertyA must not be less then PropertyB");
if (ModelState.IsValid)
{
//save values
//go to next page
}
else
{
return View();
}
}
In the view you would then need to use #Html.ValidationSummary to display the error message as the above code would add a model level error and not a property level error.
To specify a property level error you can write:
ModelState.AddModelError("PropertyA", "PropertyA must not be less then PropertyB");
And then in the view use:
#Html.ValidationMessageFor(m => m.PropertyA);
to display the error message.
Again, any client side validation would need to be linked in by manually linking in the client side validation in the view by defining properties.
Custom Model Validation Attribute
If I understand the problem correctly, you are trying to validate a model which contains a single value and a collection where a property on the collection is to be summed.
For the example I will give, the view will present to the user a maximum value field and 5 value fields. The maximum value field will be a single value in the model where as the 5 value fields will be part of a collection. The validation will ensure that the sum of the value fields is not greater than the maximum value field. The validation will be defined as an attribute on the model which will also link in nicely to the javascript client side valdation.
The View:
#model MvcApplication1.Models.ValueModel
<h2>Person Ages</h2>
#using (#Html.BeginForm())
{
<p>Please enter the maximum total that will be allowed for all values</p>
#Html.EditorFor(m => m.MaximumTotalValueAllowed)
#Html.ValidationMessageFor(m => m.MaximumTotalValueAllowed)
int numberOfValues = 5;
<p>Please enter #numberOfValues different values.</p>
for (int i=0; i<numberOfValues; i++)
{
<p>#Html.EditorFor(m => m.Values[i])</p>
}
<input type="submit" value="submit"/>
}
I have not added any validation against the value fields as I do not want to overcomplicate the example.
The Model:
public class ValueModel
{
[Required(ErrorMessage="Please enter the maximum total value")]
[Numeric] //using DataAnnotationExtensions
[ValuesMustNotExceedTotal]
public string MaximumTotalValueAllowed { get; set; }
public List<string> Values { get; set; }
}
The Actions:
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Index(ValueModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
else
{
return RedirectToAction("complete"); //or whatever action you wish to define.
}
}
The Custom Attribute:
The [ValuesMustNotExceedTotal] attribute defined on the model can be defined by overriding the ValidationAttribute class:
public class ValuesMustNotExceedTotalAttribute : ValidationAttribute
{
private int maxTotalValueAllowed;
private int valueTotal;
public ValuesMustNotExceedTotalAttribute()
{
ErrorMessage = "The total of all values ({0}) is greater than the maximum value of {1}";
}
public override string FormatErrorMessage(string name)
{
return string.Format(ErrorMessageString, valueTotal, maxTotalValueAllowed);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
PropertyInfo maxTotalValueAllowedInfo = validationContext.ObjectType.GetProperty("MaximumTotalValueAllowed");
PropertyInfo valuesInfo = validationContext.ObjectType.GetProperty("Values");
if (maxTotalValueAllowedInfo == null || valuesInfo == null)
{
return new ValidationResult("MaximumTotalValueAllowed or Values is undefined in the model.");
}
var maxTotalValueAllowedPropertyValue = maxTotalValueAllowedInfo.GetValue(validationContext.ObjectInstance, null);
var valuesPropertyValue = valuesInfo.GetValue(validationContext.ObjectInstance, null);
if (maxTotalValueAllowedPropertyValue != null && valuesPropertyValue != null)
{
bool maxTotalValueParsed = Int32.TryParse(maxTotalValueAllowedPropertyValue.ToString(), out maxTotalValueAllowed);
int dummyValue;
valueTotal = ((List<string>)valuesPropertyValue).Sum(x => Int32.TryParse(x, out dummyValue) ? Int32.Parse(x) : 0);
if (maxTotalValueParsed && valueTotal > maxTotalValueAllowed)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
}
//if the maximum value is not supplied or could not be parsed then we still return that the validation was successful.
//why? because this attribute is only responsible for validating that the total of the values is less than the maximum.
//we use a [Required] attribute on the model to ensure that the field is required and a [Numeric] attribute
//on the model to ensure that the fields are input as numeric (supplying appropriate error messages for each).
return null;
}
}
Adding Client Side Validation to the Custom Attribute:
To add client side validation to this attribute it would need to implement the IClientValidatable interface:
public class ValuesMustNotExceedTotalAttribute : ValidationAttribute, IClientValidatable
{
//...code as above...
//this will be called when creating the form html to set the correct property values for the form elements
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule {
ValidationType = "valuesmustnotexceedtotal", //the name of the client side javascript validation (must be lowercase)
ErrorMessage = "The total of all values is greater than the maximum value." //I have provided an alternative error message as i'm not sure how you would alter the {0} and {1} in javascript.
};
yield return rule;
//note: if you set the validation type above to "required" or "email" then it would use the default javascript routines (by those names) to validate client side rather than the one we define
}
}
If you were to run the application at this point and view the source html for the field defining the attribute you will see the following:
<input class="text-box single-line" data-val="true" data-val-number="The MaximumTotalValueAllowed field is not a valid number." data-val-required="Please enter the maximum total value" data-val-valuesmustnotexceedtotal="The total of all values is greater than the maximum value." id="MaximumTotalValueAllowed" name="MaximumTotalValueAllowed" type="text" value="" />
In particular notice the validation attribute of data-val-valuesmustnotexceedtotal. This is how our client side validation will link to the validation attribute.
Adding Client Side Validation:
To add client side validation you need to add the following similar library references in the tag of the view:
<script src="#Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
You need to also ensure that the client side validation is switched on in the web.config although I think this should be on by default:
<add key="ClientValidationEnabled" value="true"/>
<add key="UnobtrusiveJavaScriptEnabled" value="true"/>
All that is left is to define the client side validation in the view. Note that the validation added here is defined in the view but if it was defined in a library then the custom attribute (maybe not this one) could be added to other models for other views:
<script type="text/javascript">
jQuery.validator.unobtrusive.adapters.add('valuesmustnotexceedtotal', [], function (options) {
options.rules['valuesmustnotexceedtotal'] = '';
options.messages['valuesmustnotexceedtotal'] = options.message;
});
//note: this will only be fired when the user leaves the maximum value field or when the user clicks the submit button.
//i'm not sure how you would trigger the validation to fire if the user leaves the value fields although i'm sure its possible.
jQuery.validator.addMethod('valuesmustnotexceedtotal', function (value, element, params) {
sumValues = 0;
//determine if any of the value fields are present and calculate the sum of the fields
for (i = 0; i <= 4; i++) {
fieldValue = parseInt($('#Values_' + i + '_').val());
if (!isNaN(fieldValue)) {
sumValues = sumValues + fieldValue;
valueFound = true;
}
}
maximumValue = parseInt(value);
//(if value has been supplied and is numeric) and (any of the fields are present and are numeric)
if (!isNaN(maximumValue) && valueFound) {
//perform validation
if (sumValues > maximumValue)
{
return false;
}
}
return true;
}, '');
</script>
And that should be it. I'm sure that there are improvements that can be made here and there and that if i've misunderstood the problem slightly that you should be able to tweak the validation for your needs. But I believe this validation seems to be the way that most developers code custom attributes including more complex client side validation.
Hope this helps. Let me know if you have any questions or suggestions regarding the above.

This is what you are looking for:
http://www.a2zdotnet.com/View.aspx?Id=182

I got some similar situation too, I need to compare the value of property A with property B, and I get it done by:
public sealed class PropertyAAttribute : ValidationAttribute
{
public string propertyBProperty { get; set; }
// Override the isValid function
public override bool IsValid(object value)
{
// Do your comparison here, eg:
return A >= B;
}
}
Then just use the custom validation attribute like this:
[PropertyA(propertyBProperty = "PropertyB")]
public string Property A {get; set;}
I also tried very hard and get this solution from others, Hope this help!

Your model class can implement the IValidatableObject interface.
This way you have access to all the properties of your model class and can perform all your custom validations.
You also have the IClientValidatable interface, for client side validations, but I'm not sure if by implementing it directly in the model class the client validations are picked by MVC since I only ever used this interface to specify client validations in custom validation attributes.

Related

How can I extend a DataAnnotation attribute and have client side validation work?

In a large number of places on my model, I have a property public int CarrotLength {get; set;} I want to put a [Range(5, 100)] attribute on it.
Rather than have to specify this millions of times for every CarrotLength property, because the carrot length might change, it makes sense to make a [CarrotLength] attribute instead so I can just change the values 5 and 10 in one place.
I made a public class CarrotLengthAttribute : RangeAttribute and call the base class with 5, 100. However the client side unobtrusive javascript validation doesn't trigger.
Is there a way to make it work? Possibly I need to register the attribute somehow?
My unobtrusive javascript validation works for normal attributes, as well as custom ones that I have created.
If it's not possible I will just define the values as constants and reference them as [Range(MYCONSTS.CARROTLENGTHMIN...)] but I think it's nicer to have a custom attribute.
Try customing the attribute class that inherits from the ValidationAttribute and IClientModelValidator like below :
public class AgeRangeAttribute: ValidationAttribute , IClientModelValidator
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var age = Convert.ToInt32(value.ToString());
if (age<5||age>100)
{
return new ValidationResult(GetErrorMessage());
}
return ValidationResult.Success;
}
public string GetErrorMessage()
{
return $"Age must be between 5 and 100.";
}
public void AddValidation(ClientModelValidationContext context)
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-age", GetErrorMessage());
}
}
Add a new JavaScript file named AgeValidate.js into the wwwroot folder "/lib/jquery-validation-unobtrusive/AgeValidate.js" :
jQuery.validator.addMethod("age",
function (value, element, param) {
if (value < 5 || value > 100) {
return false;
}
else {
return true;
}
});
jQuery.validator.unobtrusive.adapters.addBool("age");
The above code adds a method to jQuery validation library. It uses addMethod() method to specify our own validation function. The validation function receives the value entered in the age textbox. It then performs the validation and returns a boolean value.
Reference the AgeValidate.js in _ValidationScriptsPartial.cshtml
<environment include="Development">
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js">
</script>
<script src="~/lib/jquery-validation-unobtrusive/AgeValidate.js"></script>
</environment>
Add the below code in your view
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
For more details on customizing client-side validation , you could refer to the official documentation: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-2.2#custom-client-side-validation

How can I make ASP.NET MVC 3 render Html.CheckBoxFor and the corresponding hidden element with reversed values?

I'm using ASP.NET MVC3 with Razor.
I have a boolean value in my model which I would like to render as a check box in my view. However, I would like the check box to indicate the reverse of the boolean state. In other words, selecting the check box should set the bound model object to false, not true, when the form is submitted. And vice versa.
I can do this to set the value attribute on the rendered check box input element:
#Html.CheckBoxFor(model => model.MyBoolean, new { value = "false" })
But the hidden input element that is automatically created still has a value of false. Thus they both have a value of false, which means the bound model object is always set to false.
So how can I force the HTML helper to set the hidden element to true and the check box element to false?
I know that (a) I could alter the model and the database, (b) I could alter the values with javascript just prior to submission, and (c) I could swap whatever value is received in the controller after submission. I may do one of these, but I'm not asking for possible solutions; I'm asking whether it is possible to make the HTML helper do as I wish. I have searched extensively and haven't seen this addressed anywhere in official or unofficial sources. It seems like they should have a "swap" option or something.
class ViewModel {
public bool MyBoolean {get;set;}
public bool DisplayValue {
get {
return ! MyBoolean ;
}
set {
MyBoolean = !value;
}
}
}
And bind to the DisplayValue as it's setter updates you MyBoolean property anyway.
EDIT:
After reading your question again:.
You could use HtmlHelper to do that - but instead of using a CheckBox you could use a dropdown. The dropdown will define the "oppisite" values and text.
myModelInstance.PossibleValues = new[] { new SelectListItem { Value = "false", Text = "Not false" }, new SelectListItem { Value = "true", Text = "Not true" } };
Notice how the description is the opposite meaning of what you want the model to be. So for eg. for true you may have text as "Hidden" and false you may have the text as "Visible", true for "Disabled" and false for "Enabled" etc.
Then in your View:
#Html.DropDownList(Model.MyBoolean.ToString(), Model.PossibleValues)
The model will be updated with the correct value without have to do boolean toggles before viewing or updating.
For future readers, in my own opinion, HtmlHelpers are designed to render Html (as the name suggests). My preference for creating different way to render items is to create EditFor and DisplayFor templates. To make sure this is highly reusable, I also create model designed specifically for these templates. With your design, my models and templates might look like:
/Models/Controller/ControllerActionViewModel.cs
public class ControllerActionViewModel
{
public ControllerActionViewModel()
{
this.CheckboxBoolTemplate = new CheckboxBoolTemplate(false, true);
}
[Display(Name = "My Boolean")]
public SelectBoolTemplate MyBoolean { get; set; }
}
/TemplateModels/ControllerActionViewModel.cs
public sealed class SelectBoolTemplate
{
private bool _valuesSwapped = false;
private bool? _value;
private bool _defaultValue = false;
public SelectBoolTemplate()
{
}
public SelectBoolTemplate(bool valuesSwapped)
{
this._valuesSwapped = valuesSwapped)
}
public SelectBoolTemplate(bool defaultValue, bool valuesSwapped)
{
this._defaultValue = defaultValue;
this._valuesSwapped = valuesSwapped)
}
public bool Value
{
get
{
if (this._value.HasValue)
{
return this._value.Value
}
return this._defaultValue;
}
set
{
this._value = value;
}
}
}
/Views/Shared/EditorTemplates/SelectBoolTemplate.cshtml
#model SelectBoolTemplate
#{
string propertyName = ViewContext.ViewData.ModelMetadata.PropertyName;
string fullPropertyName = ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix;
string labelText = ViewContext.ViewData.ModelMetadata.DisplayName
?? ViewContext.ViewData.ModelMetadata.PropertyName;
}
#Html.LabelForModel()
#Html.Checkbox(fullPropertyName, Model.Value)
I know this is too late for the original query but for anyone reading in future there's an alternative way to handle checkboxes with reversed false/true for checked/unchecked which requires no changes to the model - create a checkbox for false and a hidden field for true
<input id="#Html.IdFor(model => model.BoolProperty)" name="#Html.NameFor(model => model.BoolProperty)" type="checkbox" value="false" #(Model.BoolProperty? "" : "checked=\"checked\"" ) />
<input name="#Html.NameFor(model => model.BoolProperty)" type="hidden" value="true" />

Single property not getting bound on HttpPost

I'm working on the first MVC3 project at our company, and I've hit a block. No one can seem to figure out what's going on.
I have a complex Model that I'm using on the page:
public class SpaceModels : List<SpaceModel> {
public bool HideValidation { get; set; }
[Required(ErrorMessage=Utilities.EffectiveDate + Utilities.NotBlank)]
public DateTime EffectiveDate { get; set; }
public bool DisplayEffectiveDate { get; set; }
}
In the Controller, I create a SpaceModels object with blank SpaceModels for when Spaces get combined (this would be the destination Space).
// Need a list of the models for the View.
SpaceModels models = new SpaceModels();
models.EffectiveDate = DateTime.Now.Date;
models.DisplayEffectiveDate = true;
models.Add(new SpaceModel { StoreID = storeID, SiteID = siteID, IsActive = true });
return View("CombineSpaces", models);
Then in the View, I am using that SpaceModels object as the Model, and in the form making a TextBox for the Effective Date:
#model Data.SpaceModels
#using (Html.BeginForm("CombineSpaces", "Space")) {
<div class="EditLine">
<span class="EditLabel LongText">
New Space Open Date
</span>
#Html.TextBoxFor(m => m.EffectiveDate, new {
size = "20",
#class = "datecontrol",
// Make this as a nullable DateTime for Display purposes so we don't start the Calendar at 1/1/0000.
#Value = Utilities.ToStringOrDefault(Model.EffectiveDate == DateTime.MinValue ? null : (DateTime?)Model.EffectiveDate, "MM/dd/yyyy", string.Empty)
})
#Html.ValidationMessageFor(m => m.EffectiveDate)
</div>
<hr />
Html.RenderPartial("_SpaceEntry", Model);
}
The Partial View that gets rendered iterates through all SpaceModels, and creates a containing the Edit fields for the individual SpaceModel objects. (I'm using the List to use the same Views for when the Spaces get Subdivided as well.)
Then on the HttpPost, the EffectiveDate is still back at it's DateTime.MinValue default:
[HttpPost]
public ActionResult CombineSpaces(SpaceModels model, long siteID, long storeID, DateTime? effectiveDate) {
// processing code
}
I added that DateTime? effectiveDate parameter to prove that the value when it gets changed does in fact come back. I even tried moving the rendering of the TextBox into the _SpaceEntry Partial View, but nothing worked there either.
I did also try using the #Html.EditorFor(m => m.EffectiveDate) in place of the #Html.TextBoxFor(), but that still returned DateTime.MinValue. (My boss doesn't like giving up the control of rendering using the #Html.EditorForModel by the way.)
There has to be something simple that I'm missing. Please let me know if you need anything else.
Looking at the source code for DefaultModelBinder, specifically BindComplexModel(), if it detects a collection type it will bind the individual elements but will not attempt to bind properties of the list object itself.
What model binding does is attempt to match the names of things or elements in the view to properties in your model or parameters in your action method. You do not have to pass all of those parameters, all you have to do is add them to your view model, then call TryUpdateModel in your action method. I am not sure what you are trying to do with SpaceModel or List but I do not see the need to inherit from the List. Im sure you have a good reason for doing it. Here is how I would do it.
The view model
public class SpacesViewModel
{
public DateTime? EffectiveDate { get; set; }
public bool DisplayEffectiveDate { get; set; }
public List<SpaceModel> SpaceModels { get; set; }
}
The GET action method
[ActionName("_SpaceEntry")]
public PartialViewResult SpaceEntry()
{
var spaceModels = new List<SpaceModel>();
spaceModels.Add(
new SpaceModel { StoreID = storeID, SiteID = siteID, IsActive = true });
var spacesVm = new SpacesViewModel
{
EffectiveDate = DateTime.Now,
DisplayEffectiveDate = true,
SpaceModels = spaceModels
};
return PartialView("_SpaceEntry", spacesVm);
}
The POST action method
[HttpPost]
public ActionResult CombineSpaces()
{
var spacesVm = new SpacesViewModel();
// this forces model binding and calls ModelState.IsValid
// and returns true if the model is Valid
if (TryUpdateModel(spacesVm))
{
// process your data here
}
return RedirectToAction("Index", "Home");
}
And the view
<label>Effective date: </label>
#Html.TextBox("EffectiveDate", Model.EffectiveDate.HasValue ?
Model.EffectiveDate.Value.ToString("MM/dd/yyyy") : string.empty,
new { #class = "datecontrol" })
Sometimes you need to explicitly bind form data using hidden fields such as
#Html.HiddenField("EffectiveDate", Model.EfectiveDate.)
In order to bind the properties of the SpaceModel object you can add individual properties such as SiteID to the view model or add a SpaceModel property for a single SpaceModel. If you want to successfully bind a complex model, add it as a Dictionary populated with key-value pairs rather than a List. You should then add the dictionary to the view model. You can even add a dictionary of dictionaries for hierarchical data.
I hope this helps :)

Get full name of Complex Type from ModelClientValidationRequiredIfRule method in custom ValidationAttribute

I am using the example at The Complete Guide To Validation In ASP.NET MVC 3 to create a RequiredIf validation attribute (it's about 1/3 down the page under the heading of "A more complex custom validator"). It all works fine with the exception of one scenario, and that is if I have the need to validate against a complex type. For example, I have the following model:
public class MemberDetailModel
{
public int MemberId { get; set; }
// Other model properties here
public MemberAddressModel HomeAddress { get; set; }
public MemberAddressModel WorkAddress { get; set; }
}
public class MemberAddressModel
{
public bool DontUse { get; set; }
// Other model properties here
[RequiredIf("DontUse", Comparison.IsEqualTo, false)]
public string StreetAddress1 { get; set; }
}
The problem is that when the attribute validation for the StreetAddress property is rendered, it get's decorated with the attribute of data-val-requiredif-other="DontUse". Unfortunately, since the address is a sub-type of the main model, it needs to be decorated with a name of HomeAddress_DontUse and not just DontUse.
Strangely enough, the validation works fine for server-side validation, but client-side unobtrusive validation fails with an JS error because JS can't find the object with a name of just "DontUse".
Therefore, I need to find a way to change the ModelClientValidationRequiredIfRule method to know that the property it is validating is a sub-type of a parent type, and if so, prepend the ParentType_ to the "otherProperty" field (e.g. otherProperty becomes HomeAddress_DontUse.
I have tried passing in typeof(MemberAddressModel) as a parameter of the attribute, but even when debugging the attribute creation, I can't seem to find any reference to the parent type of HomeAddress or WorkAddress from that type.
Based on the suggestion from The Flower Guy, I was able to come up with the following which seems to work. I simply modified the following in the customValidation.js file:
jQuery.validator.addMethod("requiredif", function (value, element, params) {
if ($(element).val() != '') return true;
var prefix = getModelPrefix(element.name); // NEW LINE
var $other = $('#' + prefix + params.other); // MODIFIED LINE
var otherVal = ($other.attr('type').toUpperCase() == "CHECKBOX") ? ($other.attr("checked") ? "true" : "false") : $other.val();
return params.comp == 'isequalto' ? (otherVal != params.value) : (otherVal == params.value);
});
I also added the following method to that file (within the JQuery block so as to be only privately accessible):
function getModelPrefix(fieldName) {
return fieldName.substr(0, fieldName.lastIndexOf(".") + 1).replace(".","_");
}
Cannot do it exactly right now, but the problem is in the client javascript function:
jQuery.validator.addMethod("requiredif" ...
The js is not sophisticated enough to cope with complex view models where there may be a model prefix. If you take a look at Microsoft's jquery.validate.unobstrusive.js (in the Scripts folder over every MVC3 application), you will find some useful methods including getModelPrefix and appendModelPrefix. You can take a similar approach and change the requiredIf validation method - take a look at the equalto method in jquery.validate.unobstrusive.js for a helping hand.

MVC Model Binding a Complex Type to a Simple Type and Vice Versa

Here's a scenario:
I have an autocomplete plugin (custom) that keeps a hidden field of JSON objects (using a specific struct).
I've created an Html helper that helps me easily bind to a specific custom model (basically, it has a JSON property that is for two-way binding and a property that lets me deserialize the JSON into the appropriate struct):
public class AutoCompleteModel {
public string JSON { get; set; }
public IEnumerable<Person> People {
get {
return new JavaScriptSerializer().Deserialize<Person>(this.JSON);
}
set {
this.JSON = new JavaScriptSerializer().Serialize(value);
}
}
}
This works great and I can model bind using the default binder #Html.Autocomplete(viewModel => viewModel.AutoCompleteModelTest). The HTML helper generates HTML like:
<input type="text" id="AutoCompleteModelTest_ac" name="AutoCompleteModelTest_ac" value="" />
<input type="hidden" id="AutoCompleteModelTest_JSON" name="AutoCompleteModelTest.JSON" value="{JSON}" />
The problem is this is not the best way for consumers. They have to manually set the People property to an array of Person structs. In my data layer, my domain objects probably will not be storing the full struct, only the person's ID (a corporate ID). The autocomplete will take care of looking up the person itself if only given an ID.
The best scenario will be to call it like this:
#Html.Autocomplete(domainObject => domainObject.PersonID) or
#Html.Autocomplete(domainObject => domainObject.ListOfPersonIDs
I would like it to work against the string property AND against the custom AutoCompleteModel. The autocompleter only updates a single hidden field, and that field name is passed back on postback (the value looks like: [{ "Id":"12345", "FullName":"A Name"},{ "Id":"12347", "FullName":"Another Name" }]).
The problem is, of course, that those domain object properties only have an ID or array of IDs, not a full Person struct (so cannot be directly serialized into JSON). In the HTML helper, I can transform those property values into a struct, but I don't know how to transform it back into a simple type on POST. The solution I need would transform an ID into a new Person struct on page load, serializing it into the hidden field. On POST, it would deserialize the generated JSON back into a simple array of IDs.
Is a custom model binder the solution I need? How can I tell it to work both with a custom model AND simple types (because I don't want it applied to EVERY string property, just need it to deal with the values given by the HTML helper).
I figured it out, it's possible!
To clarify, I needed to: transform a string or string array (of IDs) into a JSON structure for my hidden field value, then on post back, deserialize the JSON in the hidden field and transform the struct back into a simple string or string array (of IDs) for my domain object's property.
Step 1: Create a HTML helper
I had done this already, but only for accepting my custom AutoCompleteModel type. I needed one for a string and an Enumerable of string type.
All I did was generate my Person struct(s) from the value of the property and serialize them into JSON for the hidden field the Autocompleter uses (this is an example of the string helper, I also have a nearly identical one for IEnumerable<string>):
public static MvcHtmlString AutoComplete<TModel>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, string>> idProp)
where TModel : class
{
TModel model = htmlHelper.ViewData.Model;
string id = idProp.Compile().Invoke(model);
string propertyName = idProp.GetPropertyName();
Person[] people = new Person[] {
new Person() { ID = id }
};
// Don't name the textbox the same name as the property,
// otherwise the value will be whatever the textbox is,
// if you care.
MvcHtmlString textBox = htmlHelper.TextBox(propertyName + "_ac", string.Empty);
// For me, the JSON is the value I want to postback
MvcHtmlString hidden = htmlHelper.Hidden(propertyName, new JavaScriptSerializer().Serialize(people));
return MvcHtmlString.Create(
"<span class=\"AutoComplete\">" +
textBox.ToHtmlString() +
hidden.ToHtmlString() +
"</span>");
}
Usage: #Html.AutoComplete(model => model.ID)
Step 2: Create a custom model binder
The crux of my issue was that I needed this binder to only apply to certain properties, and they were strings or string arrays.
I was inspired by this article because it used Generics. I decided, hey, we can just ask people what property they want to apply the binder for.
public class AutoCompleteBinder<T> : DefaultModelBinder
where T : class
{
private IEnumerable<string> PropertyNames { get; set; }
public AutoCompleteBinder(params Expression<Func<T, object>>[] idProperties)
{
this.PropertyNames = idProperties.Select(x => x.GetPropertyName());
}
protected override object GetPropertyValue(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor,
IModelBinder propertyBinder)
{
var submittedValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (submittedValue != null && this.PropertyNames.Contains(propertyDescriptor.Name))
{
string json = submittedValue.AttemptedValue;
Person[] people = new JavaScriptSerializer().Deserialize<Person[]>(json);
if (people != null && people.Any())
{
string[] IDs = people.Where(x => !string.IsNullOrEmpty(x.ID)).Select(x => x.ID).ToArray();
bool isArray = bindingContext.ModelType != typeof(string) &&
(bindingContext.ModelType == typeof(string[]) ||
bindingContext.ModelType.HasInterface<IEnumerable>());
if (IDs.Count() == 1 && !isArray)
return IDs.First(); // return string
else if (IDs.Count() > 0 && isArray)
return IDs.ToArray(); // return string[]
else
return null;
}
else
{
return null;
}
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
GetPropertyName() (translate LINQ expression into a string, i.e. m => m.ID = ID) and HasInterface() are just two utility methods I have.
Step 3: Register
Register the binder on your domain objects and their properties in Application_Start:
ModelBinders.Binders.Add(typeof(Employee), new AutoCompleteBinder<Employee>(e => e.ID, e => e.TeamIDs));
It's only a little bit annoying to have to register the binder for specific properties, but it's not the end of the world and provides a nice, smooth experience working with my autocompleter.
Any comments are welcome.

Resources