To make a validation with a Regex, I usually do:
// In my ViewModel
[RegularExpression("MyRegex", ErrorMessageResourceName = "MyErrorMessage")]
public string MyField { get; set; }
And the HTML helper
@Html.TextBoxFor(model => model.MyField)
generates a markup that looks like this:
<input type="text" class="valid" name="MyField" value="" id="MyField" data-val="true" data-val-regex-pattern="MyRegex" data-val-regex="MyErrorMessage"></input>
The problem is that I want to have a dynamic number of fields and am now using
// In my ViewModel
[RegularExpression("MyRegex", ErrorMessageResourceName = "MyErrorMessage")]
public IList<string> MyField { get; set; }
This time
@Html.TextBoxFor(model => model.MyField[0])
will generate (without the regex html attributes)
<input id="MyField_0_" type="text" value="" name="MyField[0]"></input>
How can I ensure that data-val
html attributes are created when binding elements of a list that has a DataAnnotation validation attribute in my ViewModel?
There isn't really a way for Data Annotations to apply to the elements of a list. What you would have to do is create a wrapper class and apply the Data Annotation to the elements in the wrapper class, like so:
public IList<MyField> MyFields {get;set;}
public class MyField
{
[RegularExpression("MyRegex", ErrorMessageResourceName = "MyErrorMessage")]
public string Value
}
Usage:
@Html.TextBoxFor(model => model.MyFields[0].Value)
You are using DataAnnotations for validations. From what I understand, you are looking for a way to apply the DataAnnotation validation to each element of the list.
Whenever Html.EditorFor is called, it fetches the ModelMetadata of the model that has been passed to it and then fetches any ModelValidators associated with that model. It is the presence of these ModelValidators that result in the 'data-val-*' attributes in the HTML.
When Html.EditorFor is passed a list as a model (or any enumerable for that matter) it first fetches the ModelMetadata and the associated Validators for the property - in your case, it will fetch ModelMetadata associated with the 'MyField' property followed by the validators - 'RegularExpression' in this case. It next iterates through the list of strings and gets the ModelMetadata and Validators for each string. While ModelMetadata has been constructed for each string, there are no Validators that have been specified for these strings. This is the reason that the string is displayed but the validation attributes are not added to the HTML element.
The way I see it, what you are looking for can be achieved by adding the Validator specified on the 'MyField' property to all the list elements at runtime.
This can be done by
- Writing a shared editor template for all Collections
- Setting the current ModelMetadataProvider to DataAnnotationsModelMetadataProvider
- Overriding the 'GetValidators' methd of DataAnnotationsModelValidatorProvider'
The shared editor template for step1 is given below
@model System.Collections.Generic.IEnumerable<object>
@{
ViewBag.Title = "Collection";
var modelMetadata = this.ViewData.ModelMetadata;
var validators = modelMetadata.GetValidators(ViewContext).ToList();
ViewContext.HttpContext.Items["rootValidators"] = validators;
}
@foreach (var item in Model)
{
@Html.EditorFor(m => item)
}
You can see in the code above that we are getting all the validators that have been specified on the list. These validators will be added to the elements of the list later. They have been stored in the HttpContext.Items for use in our custom ModelValidatorProvider.
Step 2 - In Global.asax, put in the following code -
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new DAModelValidatorProvider());
ModelMetadataProviders.Current = new CachedDataAnnotationsModelMetadataProvider();
Step 3 - Write your own ModelValidatorProvider by overriding the GetValidators method as shown in the code below
public class DAModelValidatorProvider : DataAnnotationsModelValidatorProvider
{
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
var validators = base.GetValidators(metadata, context, attributes).ToList();
// get root validators of the collection. this was stored in the editor template - fetching it for use now.
// fetching the rootvalidators inside this method is a bad idea because we have to call GetValidators method on the
// containers ModelMetadata and it will result in a non-terminal recursion
var rootValidators = context.HttpContext.Items["rootValidators"] as IEnumerable<ModelValidator>;
if (rootValidators != null)
{
foreach (var rootValidator in rootValidators)
{
validators.Add(rootValidator);
}
}
return validators;
}
}
Performing the above 3 steps did work for me. However, I've used Html.EditorFor instead of Html.TextBoxFor. Using Html.EditorFor, the way I have has not given me proper id and name attributes - I reckon this to be a trivial issue in the scheme of things. I've created a solution for this and uploaded it on https://github.com/swazza85/Stackoverflow so you can give it a go and see if it fits your needs. What I've done here is not a complete solution by any means but hopefully it gets you going without having to change your models.
Cheers,
Swarup.
I used @swazza85 answer, but had to modify it for my situation. Hopefully if someone else uses his solution they can benefit from my modification. I had to change IEnumerable<object>
to IList<object>
(or in my case IList<decimal?>
because IList<object>
throws an error.). Then i had to use the for
iterator because the word item
was being added to the name attribute and the model binder did not bind those items to my model.
@model System.Collections.Generic.IList<decimal?>
@{
ViewBag.Title = "Collection";
var modelMetadata = this.ViewData.ModelMetadata;
var validators = modelMetadata.GetValidators(ViewContext).ToList();
ViewContext.HttpContext.Items["rootValidators"] = validators;
}
@for (var i = 0; i < Model.Count(); i++)
{
@Html.EditorFor(model => Model[i], new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => Model[i], "", new { @class = "text-danger" })
}
Also if you do not want to clear your providers in the Global.asax
file, just return the validators in the if statement and return an empty list outside of it, just note that this editor template must be last in your views or it will run into problems with other properties or templates. You could set ViewContext.HttpContext.Items["rootValidators"] = null
at the end of the template.
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
var validators = base.GetValidators(metadata, context, attributes).ToList();
// get root validators of the collection. this was stored in the editor template - fetching it for use now.
// fetching the rootvalidators inside this method is a bad idea because we have to call GetValidators method on the
// containers ModelMetadata and it will result in a non-terminal recursion
var rootValidators = context.HttpContext.Items["rootValidators"] as IEnumerable<ModelValidator>;
if (rootValidators != null)
{
foreach (var rootValidator in rootValidators)
{
validators.Add(rootValidator);
}
return validators;
}
return new List<ModelValidator>();
}