1

我需要将自定义验证器应用于帐单地址字段。该视图显示多个地址字段,包括国家下拉列表(带有美国和加拿大选项)和 BillingPostalCode 文本框。最初,我将正则表达式应用于允许美国或加拿大邮政编码的消息合同,如下所示:

[MessageBodyMember]
[Display(Name = "Billing Postal Code")]
[Required]
[StringLength(10, MinimumLength = 5)]
[RegularExpression("(^\\d{5}(-\\d{4})?$)|(^[ABCEGHJKLMNPRSTVXY]{1}\\d{1}[A-Z]{1} *\\d{1}[A-Z]{1}\\d{1}$)", ErrorMessage = "Zip code is invalid.")] // US or Canada
public string BillingPostalCode
{
   get { return _billingPostalCode; }
   set { _billingPostalCode = value; }
}

以上将允许美国或加拿大的邮政编码。但是,只有在 BillingCountry 下拉列表中分别选择了美国或加拿大时,企业主才希望表单允许使用美国或加拿大的邮政编码。在他的测试用例中,他选择了加拿大并输入了美国的邮政编码。这种情况应该被禁止。

我最初的尝试是将其放在视图中,尽管我对创建 2 个文本框字段不满意。我应该只需要 1 个字段。

<div style="float: left; width: 35%;">
   Zip Code<br />
   <span id="spanBillingZipUS">
      @Html.TextBoxFor(m => m.BillingPostalCode, new { @class = "customer_input", @id = "BillingPostalCode" })
   </span>
   <span id="spanBillingZipCanada">
      @Html.TextBoxFor(m => m.BillingPostalCode, new { @class = "customer_input", @id = "BillingPostalCodeCanada" })
   </span>
   @Html.ValidationMessageFor(m => m.BillingPostalCode)
   <br />
</div>

我的想法是,当 Country 下拉列表被切换时,我将使用 jQuery 来显示或隐藏适当的跨度。那块很容易。

但是我遇到了两个文本框都应用了单个验证器的问题,该验证器映射到上面粘贴的 MessageBodyMember。我知道如何在 jQuery 中编写验证代码,但也希望将验证也应用于服务器端。

我对 MVC 相当陌生,来自网络表单。“老派”网络表单自定义验证很容易实现。我在网上找到的用于 MVC 中的自定义验证的示例非常复杂。起初,这似乎是一个非常基本的要求。代码需要评估一个变量(选定的国家/地区)并将该国家/地区的适当正则表达式应用于 BillingPostalCode 字段。

如何使用 MVC3 以简单的方式满足此要求?谢谢。

4

3 回答 3

1

好吧,我实现了这个人所做的,它作为一个带有Data Annotations的 chram 工作。为了更改下拉值的检查,您确实需要做一些工作,但这是我发现使用Data AnnotationsUnobtrusive实现验证的更优雅的方式。


这里有一个例子:

模型

...
        [RequiredIf("IsUKResident", true, ErrorMessage = "You must specify the City if UK resident")]
        public string City { get; set; }
...

自定义属性

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace Mvc3ConditionalValidation.Validation
{
    public class RequiredIfAttribute : ValidationAttribute, IClientValidatable
    {
        private RequiredAttribute _innerAttribute = new RequiredAttribute();

        public string DependentProperty { get; set; }
        public object TargetValue { get; set; }

        public RequiredIfAttribute(string dependentProperty, object targetValue)
        {
            this.DependentProperty = dependentProperty;
            this.TargetValue = targetValue;
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            // get a reference to the property this validation depends upon
            var containerType = validationContext.ObjectInstance.GetType();
            var field = containerType.GetProperty(this.DependentProperty);

            if (field != null)
            {
                // get the value of the dependent property
                var dependentvalue = field.GetValue(validationContext.ObjectInstance, null);

                // compare the value against the target value
                if ((dependentvalue == null && this.TargetValue == null) ||
                    (dependentvalue != null && dependentvalue.Equals(this.TargetValue)))
                {
                    // match => means we should try validating this field
                    if (!_innerAttribute.IsValid(value))
                        // validation failed - return an error
                        return new ValidationResult(this.ErrorMessage, new[] { validationContext.MemberName });
                }
            }

            return ValidationResult.Success;
        }

        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            var rule = new ModelClientValidationRule()
            {
                ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
                ValidationType = "requiredif",
            };

            string depProp = BuildDependentPropertyId(metadata, context as ViewContext);

            // find the value on the control we depend on;
            // if it's a bool, format it javascript style 
            // (the default is True or False!)
            string targetValue = (this.TargetValue ?? "").ToString();
            if (this.TargetValue.GetType() == typeof(bool))
                targetValue = targetValue.ToLower();

            rule.ValidationParameters.Add("dependentproperty", depProp);
            rule.ValidationParameters.Add("targetvalue", targetValue);

            yield return rule;
        }

        private string BuildDependentPropertyId(ModelMetadata metadata, ViewContext viewContext)
        {
            // build the ID of the property
            string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(this.DependentProperty);
            // unfortunately this will have the name of the current field appended to the beginning,
            // because the TemplateInfo's context has had this fieldname appended to it. Instead, we
            // want to get the context as though it was one level higher (i.e. outside the current property,
            // which is the containing object (our Person), and hence the same level as the dependent property.
            var thisField = metadata.PropertyName + "_";
            if (depProp.StartsWith(thisField))
                // strip it off again
                depProp = depProp.Substring(thisField.Length);
            return depProp;
        }
    }
}

客户端

...
<script src="@Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>

<script>
    $.validator.addMethod('requiredif',
        function (value, element, parameters) {
            var id = '#' + parameters['dependentproperty'];

            // get the target value (as a string, 
            // as that's what actual value will be)
            var targetvalue = parameters['targetvalue'];
            targetvalue = 
              (targetvalue == null ? '' : targetvalue).toString();

            // get the actual value of the target control
            // note - this probably needs to cater for more 
            // control types, e.g. radios
            var control = $(id);
            var controltype = control.attr('type');
            var actualvalue =
                controltype === 'checkbox' ?
                control.attr('checked').toString() :
                control.val();

            // if the condition is true, reuse the existing 
            // required field validator functionality
            if (targetvalue === actualvalue)
                return $.validator.methods.required.call(
                  this, value, element, parameters);

            return true;
        }
    );

    $.validator.unobtrusive.adapters.add(
        'requiredif',
        ['dependentproperty', 'targetvalue'], 
        function (options) {
            options.rules['requiredif'] = {
                dependentproperty: options.params['dependentproperty'],
                targetvalue: options.params['targetvalue']
            };
            options.messages['requiredif'] = options.message;
        });

</script>
...
    <div class="editor-label">
        @Html.LabelFor(model => model.City)
    </div>
    <div class="editor-field">
        @Html.EditorFor(model => model.City)
        @Html.ValidationMessageFor(model => model.City)
    </div>

    <div class="editor-label">
        @Html.LabelFor(model => model.IsUKResident)
    </div>
    <div class="editor-field">
        @Html.EditorFor(model => model.IsUKResident)
        @Html.ValidationMessageFor(model => model.IsUKResident)
    </div>
...
于 2012-09-05T20:17:52.373 回答
0

这听起来不像是使用 ModelBinding 和验证属性很容易完成的事情。

一个直接的方法可能是保留验证属性,并简单地验证它是美国还是加拿大的邮政编码。然后当它到达服务器时,进行一些手动验证。

例如

[Post]
public ActionResult SaveInfo(MyViewModel viewModel)
{
    var isValid = true;
    if (ModelState.IsValid)
    {
        if (!IsValidPostCode(viewModel))
        {
            isValid = false;
            ModelState.AddModelError("BillingPostalCode", "The billing postcode appears to be invalid.");
        }

        if (isValid)
        {
            return RedirectToAction("success");
        }
    }

    return View(viewModel);
}

private static IDictionary<string, string> countryPostCodeRegex = new Dictionary<string, string>
    {
        { "USA", "USAPostCodeRegex" },
        { "Canada", "CanadianPostCodeRegex" },
    }

private bool IsValidPostCode(MyViewModel viewModel)
{
    var regexString = countryPostCodeRegex[viewModel.SelectedCountry];
    var regexMatch = Regex.Match(viewModel.BillingPostalCode, regexString, RegexOptions.IgnoreCase);

    return regexMatch.Success;
}
于 2012-09-05T20:19:52.737 回答
0

I used MVC Foolproof Validation in one of my projects. You would do as you were saying with hiding and showing fields, but the validation would follow through the same logic. Have 2 fields for postal code. One for Canada, the other for USA. They each have their own regex validation to ensure the proper format.

Pseudo Code

[RequiredIf(DependentName = "Country", DependentValue="USA")]
public string PostalCodeUSA { get; set; }

[RequiredIf(DependentName = "Country", DependentValue="Canada")]
public string PostalCodeCanada { get; set; }
于 2012-09-05T20:31:24.490 回答