Data validation for every item in a list of my ViewModel

|▌冷眼眸甩不掉的悲伤 提交于 2019-12-22 07:02:21

问题


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?


回答1:


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)



回答2:


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

  1. Writing a shared editor template for all Collections
  2. Setting the current ModelMetadataProvider to DataAnnotationsModelMetadataProvider
  3. 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.




回答3:


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>();
    }


来源:https://stackoverflow.com/questions/23337170/data-validation-for-every-item-in-a-list-of-my-viewmodel

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!