【问题标题】:How to validate a date using 3 dropdowns( day, month, year ) using jquery unobtrusive validation?如何使用 3 个下拉菜单(日、月、年)使用 jquery 不显眼的验证来验证日期?
【发布时间】:2012-07-13 14:38:45
【问题描述】:

我有一个模型要验证,问题是出生日期字段。 它必须由 3 个下拉菜单(日、月、年)组成。

<div id="dob-editor-field" class="model-field-editor">
      @Html.LabelFor(m => m.DateOfBirth, new { @class = "label-div" })
      @Html.Telerik().DropDownList().Name("DobDay").BindTo((SelectList)ViewData["Days"]).HtmlAttributes(new {id = "DobDaySel"})
      @Html.Telerik().DropDownList().Name("DobMonth").BindTo((SelectList)ViewData["Months"]).HtmlAttributes(new { id = "DobMonthSel"})
      @Html.Telerik().DropDownList().Name("DobYear").BindTo((SelectList)ViewData["Years"]).HtmlAttributes(new { id = "DobYearSel" })
      @Html.ValidationMessageFor(m => m.DateOfBirth)
</div>

在服务器端我这样做

        [HttpPost]
        public ActionResult Register(RegistrationModel regInfo, int DobDay, int DobMonth, int DobYear)
        {
            SetRegisterViewData(DobDay, DobMonth, DobYear);
            if (DobDay == 0 || DobMonth == 0 && DobYear == 0)
            {
                ModelState.AddModelError("DateOfBirth", "Date of birth is required");
            }
            else
            {
                DateTime dt = new DateTime(DobYear, DobMonth, DobDay);
                long ticks = DateTime.Now.Ticks - dt.Ticks;
                int years = new DateTime(ticks).Year;
                if (years < 18)
                {
                    ModelState.AddModelError("DateOfBirth", "You must be at least 18");
                }
            }            
            if (ModelState.IsValid)
            {
                //register user
                return RedirectToAction("Index", "Home");
            }
            return View(regInfo);
        }

问题:

  1. 服务器端:如何让它变得更好? (我正在考虑添加 dob, 月和年属性 RegistrationModel 并添加属性 DateOfBirth 检查这些属性)
  2. 客户端:我在看Perform client side validation for custom attribute,但它让我感到困惑。制作方法是什么?

乐: 我为这样的日期创建了一个自定义模型绑定器:

    public class DobModelBinder : DefaultModelBinder
    {
        protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
        {
            if (propertyDescriptor.Name == "DateOfBirth")
            {
                DateTime dob = DateTime.MinValue;
                var form = controllerContext.HttpContext.Request.Form;
                int day = Convert.ToInt32(form["DobDay"]);
                int month = Convert.ToInt32(form["DobMonth"]);
                int year = Convert.ToInt32(form["DobYear"]);
                if (day == 0 || month == 0 || year == 0)
                {
                    SetProperty(controllerContext, bindingContext, propertyDescriptor, DateTime.MinValue);
                }
                else
                {
                    SetProperty(controllerContext, bindingContext, propertyDescriptor, new DateTime(year, month, day));
                }
            }
            else
            {
                base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
            }
        }
    }

我是这样注册的:

ModelBinders.Binders.Add(typeof(DateTime), new DobModelBinder());

我是这样使用的:

public ActionResult Register([ModelBinder(typeof(DobModelBinder))]RegistrationModel regInfo)

DateOfBirth 绑定得很好。

LE2:

我为出生日期创建了这样的验证属性:

 public override bool IsValid(object value)
    {
        DateTime date = Convert.ToDateTime(value);
        return date != DateTime.MinValue;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        yield return new ModelClientValidationRule
        {
            ErrorMessage = this.ErrorMessage,
            ValidationType = "dateRequired"
        };
    }
}

public class DateGraterThanEighteen : ValidationAttribute, IClientValidatable
{
    public override bool IsValid(object value)
    {
        DateTime date = Convert.ToDateTime(value);
        long ticks = DateTime.Now.Ticks - date.Ticks;
        int years = new DateTime(ticks).Year;
        return years >= 18;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        yield return new ModelClientValidationRule
        {
            ErrorMessage = this.ErrorMessage,
            ValidationType = "dateGraterThanEighteen"
        };
    }
}

我应用了这样的属性

        [DateGraterThanEighteen(ErrorMessage="You must be at least 18")]
        [DateRequired(ErrorMessage = "Date of birth is required")]
        public DateTime DateOfBirth { get; set; }

LE3:

在客户端我这样做:

      $(function () {
            jQuery.validator.addMethod('dobRequired', function (value, element, params) {
                if (!/Invalid|NaN/.test(new Date(value))) {
                    return true;
                }
                else {
                    return false;
                }
            }, '');
            jQuery.validator.unobtrusive.adapters.add('dateRequired', {}, function (options) {
                options.rules['dobRequired'] = true;
                options.messages['dobRequired'] = options.message;
            });
        });

客户端验证似乎不起作用。 我该如何解决?我对这些适配器的工作方式有点困惑。

【问题讨论】:

  • 您可以在模型上使用自定义模型绑定器和 DateTime 字段。就客户端验证而言,您可以在此属性上使用自定义验证属性,该属性实现 IClientValidatable 和自定义非侵入式适配器。听起来怎么样?可行还是我必须提供一个例子?
  • 你的任何例子都非常清楚。谢谢。

标签: jquery asp.net-mvc-3 unobtrusive-validation


【解决方案1】:

您可以使用自定义编辑器模板。

在进入实施细节之前,让我们先看看最终解决方案的外观。

所以我们可以有一个视图模型(一如既往)装饰有一些数据注释属性,指示我们想要附加到它的元数据:

public class MyViewModel
{
    [DisplayName("Date of birth:")]
    [TrippleDDLDateTime(ErrorMessage = "Please select a valid DOB")]
    [Required(ErrorMessage = "Please select your DOB")]
    [MinAge(18, ErrorMessage = "You must be at least 18 years old")]
    public DateTime? Dob { get; set; }
}

那么我们可以有一个控制器:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new MyViewModel();
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(MyViewModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        return Content(
            string.Format(
                "Thank you for selecting your DOB: {0:yyyy-MM-dd}", 
                model.Dob
            )
        );
    }
}

一个视图 (~/Views/Home/Index.cshtml):

@model MyViewModel
@using (Html.BeginForm())
{
    @Html.EditorFor(x => x.Dob)
    <button type="submit">OK</button>
}

以及相应的编辑器模板,它允许我们显示 3 个下拉列表来编辑 DateTime 字段,而不是简单的文本框 (~/Views/Shared/EditorTemplates/TrippleDDLDateTime.cshtml):

@{
    var now = DateTime.Now;
    var years = Enumerable.Range(0, 150).Select(x => new SelectListItem { Value = (now.Year - x).ToString(), Text = (now.Year - x).ToString() });
    var months = Enumerable.Range(1, 12).Select(x => new SelectListItem { Value = x.ToString("00"), Text = x.ToString() });
    var days = Enumerable.Range(1, 31).Select(x => new SelectListItem { Value = x.ToString("00"), Text = x.ToString() });

    var result = ViewData.ModelState[ViewData.TemplateInfo.HtmlFieldPrefix];
    if (result != null)
    { 
        var values = result.Value.RawValue as string[];
        years = new SelectList(years, "Value", "Text", values[0]);
        months = new SelectList(months, "Value", "Text", values[1]);
        days = new SelectList(days, "Value", "Text", values[2]);
        result.Value = null;
    }
}

<div class="trippleddldatetime">
    @Html.Label("")

    @Html.DropDownList("", years, "-- year --")
    @Html.DropDownList("", months, "-- month --")
    @Html.DropDownList("", days, "-- day --")

    @Html.ValidationMessage("")
</div>

现在让我们看看如何实现[TrippleDDLDateTime] 属性:

public class TrippleDDLDateTimeAttribute : ValidationAttribute, IMetadataAware
{
    public void OnMetadataCreated(ModelMetadata metadata)
    {
        metadata.TemplateHint = "TrippleDDLDateTime";
    }

    public override bool IsValid(object value)
    {
        // It's the custom model binder that is responsible for validating 
        return true;
    }
}

注意属性是如何实现IMetadataAware 接口的,它允许我们将视图模型属性与我们编写的自定义编辑器模板(TrippleDDLDateTime.cshtml)相关联。

接下来是[MinAge] 属性:

public class MinAgeAttribute : ValidationAttribute
{
    private readonly int _minAge;
    public MinAgeAttribute(int minAge)
    {
        _minAge = minAge;
    }

    public override bool IsValid(object value)
    {
        if (value == null)
        {
            return true;
        }

        DateTime date = Convert.ToDateTime(value);
        long ticks = DateTime.Now.Ticks - date.Ticks;
        int years = new DateTime(ticks).Year;
        return years >= _minAge;
    }
}

最后一块拼图是编写一个自定义模型绑定器,该绑定器将与用[TrippleDDLDateTime] 属性修饰的属性相关联,以便执行解析:

public class TrippleDDLDateTimeModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var metadata = bindingContext.ModelMetadata;
        var trippleDdl = metadata.ContainerType.GetProperty(metadata.PropertyName).GetCustomAttributes(typeof(TrippleDDLDateTimeAttribute), true).FirstOrDefault() as TrippleDDLDateTimeAttribute;
        if (trippleDdl == null)
        {
            return base.BindModel(controllerContext, bindingContext);
        }

        var prefix = bindingContext.ModelName;
        var value = bindingContext.ValueProvider.GetValue(prefix);
        var parts = value.RawValue as string[];
        if (parts.All(string.IsNullOrEmpty))
        {
            return null;
        }

        bindingContext.ModelState.SetModelValue(prefix, value);

        var dateStr = string.Format("{0}-{1}-{2}", parts[0], parts[1], parts[2]);
        DateTime date;
        if (DateTime.TryParseExact(dateStr, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out date))
        {
            return date;
        }

        bindingContext.ModelState.AddModelError(prefix, trippleDdl.ErrorMessage);

        return null;
    }
}

如果字段没有使用自定义属性修饰,请注意活页夹如何简单地使用默认活页夹。这样它就不会干扰我们不希望三重 ddl 行为的其他 DateTime 字段。模型绑定器将简单地与Application_Start 中的DateTime? 类型相关联:

ModelBinders.Binders.Add(typeof(DateTime?), new TrippleDDLDateTimeModelBinder());

好的,到目前为止,我们有一个执行服务器端验证的解决方案。这总是你应该开始的。因为那是您也可以停下来并仍然拥有安全和工作场所的地方。

当然,如果您有时间,现在可以通过实施客户端验证来改善用户体验。客户端验证不是强制性的,但它可以节省带宽并避免服务器往返。

所以我们首先让我们的 2 个自定义属性实现 IClientValidatable 接口,这是启用不显眼的客户端验证的第一步。

[TrippleDDLDateTime]:

public class TrippleDDLDateTimeAttribute : ValidationAttribute, IMetadataAware, IClientValidatable
{
    public void OnMetadataCreated(ModelMetadata metadata)
    {
        metadata.TemplateHint = "TrippleDDLDateTime";
    }

    public override bool IsValid(object value)
    {
        // It's the custom model binder that is responsible for validating 
        return true;
    }

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

[MinAge]:

public class MinAgeAttribute : ValidationAttribute, IClientValidatable
{
    private readonly int _minAge;
    public MinAgeAttribute(int minAge)
    {
        _minAge = minAge;
    }

    public override bool IsValid(object value)
    {
        if (value == null)
        {
            return true;
        }

        DateTime date = Convert.ToDateTime(value);
        long ticks = DateTime.Now.Ticks - date.Ticks;
        int years = new DateTime(ticks).Year;
        return years >= _minAge;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule();
        rule.ErrorMessage = ErrorMessage;
        rule.ValidationType = "minage";
        rule.ValidationParameters["min"] = _minAge;
        yield return rule;
    }
}

好的,所以我们在两个属性上都实现了GetClientValidationRules。剩下的就是编写相应的不显眼的适配器。

这当然应该在一个单独的 javascript 文件中完成。例如可能是trippleddlAdapters.js:

(function ($) {
    $.fn.getDateFromTrippleDdls = function () {
        var year = this.find('select:nth(0)').val();
        var month = this.find('select:nth(1)').val();
        var day = this.find('select:nth(2)').val();
        if (year == '' || month == '' || day == '') {
            return NaN;
        }

        var y = parseInt(year, 10);
        var m = parseInt(month, 10);
        var d = parseInt(day, 10);

        var date = new Date(y, m - 1, d);
        var isValidDate = date.getFullYear() == y && date.getMonth() + 1 == m && date.getDate() == d;
        if (isValidDate) {
            return date;
        }

        return NaN;
    };

    $.validator.unobtrusive.adapters.add('trippleddldate', [], function (options) {
        options.rules['trippleddldate'] = options.params;
        if (options.message) {
            options.messages['trippleddldate'] = options.message;
        }
    });

    $.validator.addMethod('trippleddldate', function (value, element, params) {
        var parent = $(element).closest('.trippleddldatetime');
        var date = parent.getDateFromTrippleDdls();
        console.log(date);
        return !isNaN(date);
    }, '');

    $.validator.unobtrusive.adapters.add('minage', ['min'], function (options) {
        options.rules['minage'] = options.params;
        if (options.message) {
            options.messages['minage'] = options.message;
        }
    });

    $.validator.addMethod('minage', function (value, element, params) {
        var parent = $(element).closest('.trippleddldatetime');
        var birthDate = parent.getDateFromTrippleDdls();
        if (isNaN(birthDate)) {
            return false;
        }

        var today = new Date();
        var age = today.getFullYear() - birthDate.getFullYear();
        var m = today.getMonth() - birthDate.getMonth();
        if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
            age--;
        }
        return age >= parseInt(params.min, 10);
    }, '');
})(jQuery);

最后,我们在页面中加入了 3 个必要的脚本来启用不显眼的客户端验证:

<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 src="@Url.Content("~/Scripts/trippleddlAdapters.js")" type="text/javascript"></script>

【讨论】:

  • 有一个问题 - 它与默认验证器冲突,所以当我使用这些下拉列表时,我偶尔会收到“出生日期字段必须是日期”。在浏览器中。有什么办法可以去掉默认的日期验证规则?
  • 您应该在验证器中使用ErrorMessageString 而不是ErrorMessage,因为前者也适用于资源文件中的错误消息。
  • 对于任何偶然发现默认验证并显示“出生日期必须是日期”的人。消息,您应该做的是通过验证插件从这三个字段中删除日期验证,例如: $('select[name = "Date"]').rules("remove", "date");
  • 它就像一个魅力。对于澳大利亚(dd-MM-yyyy)日期,您只需重新排序 EditorTemplate 中的下拉列表,然后将 TrippleDDL....Binder 类更改为: if (DateTime.TryParseExact(dateStr, "dd-MM-yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out date))
【解决方案2】:

我想向Darin Dmitrov's answer 补充一点,当您选择年、月和日时,用于验证年和月的红色边框仍然有效。我们希望其他两个组件在输入有效日期时也能同步,所以我调整了 JavaScript,如下所示。 (removeChildValidationErrors & addChildValidationErrors 两个函数,根据日期验证结果添加调用。)

(function ($) {
    $.fn.getDateFromTrippleDdls = function () {
        var year = this.find('select:nth(0)').val();
        var month = this.find('select:nth(1)').val();
        var day = this.find('select:nth(2)').val();
        if (year == '' || month == '' || day == '') {
            return NaN;
        }

        var y = parseInt(year, 10);
        var m = parseInt(month, 10);
        var d = parseInt(day, 10);

        var date = new Date(y, m - 1, d);
        var isValidDate = date.getFullYear() == y && date.getMonth() + 1 == m && date.getDate() == d;
        if (isValidDate) {
            return date;
        }

        return NaN;
    };

    $.fn.removeChildValidationErrors = function () {

        var year = this.find('select:nth(0)');
        var month = this.find('select:nth(1)');
        var day = this.find('select:nth(2)');

        $(year).removeClass("input-validation-error");
        $(month).removeClass("input-validation-error");
        $(day).removeClass("input-validation-error");


    };

    $.fn.addChildValidationErrors = function () {

        var year = this.find('select:nth(0)');
        var month = this.find('select:nth(1)');
        var day = this.find('select:nth(2)');

        $(year).addClass("input-validation-error");
        $(month).addClass("input-validation-error");
        $(day).addClass("input-validation-error");


    };

    $.validator.unobtrusive.adapters.add('trippleddldate', [], function (options) {
        options.rules['trippleddldate'] = options.params;
        if (options.message) {
            options.messages['trippleddldate'] = options.message;
        }
    });

    $.validator.addMethod('trippleddldate', function (value, element, params) {
        var parent = $(element).closest('.trippleddldatetime');
        var date = parent.getDateFromTrippleDdls();

        if (!isNaN(date))
        {
          parent.removeChildValidationErrors();
        }
        else
        {
           parent.addChildValidationErrors();
        }


        return !isNaN(date);
    }, '');


})(jQuery);

function removeDefaultDateValidators(selector, validatorToRemove) {
    $('form').each(function () {
        var settings = $(this).validate().settings;
        $(selector, this).each(function () {
            // rules and messages seem to be keyed by element name, not id
            var elmName = $(this).attr('name');
            delete settings.rules[elmName][validatorToRemove];
            delete settings.messages[elmName][validatorToRemove];
        });
    });
}

$(function () {
    removeDefaultDateValidators('select[data-val-trippleddldate]', 'date');
});

【讨论】:

    【解决方案3】:

    从一开始我想说的是,我在这里写的东西是在 MVC 4 中测试的。

    为了实现基于 3 个下拉列表的自定义日期选择器,我尝试了不同的解决方案。一切都很完美,但正如本文前面有人在回复中提到的那样,有时标准日期验证器也会使用标准消息启动(这件事真的让我发疯了)。

    为了解决这个问题并且不要永远禁用标准日期验证器,我找到了以下解决方案:

    a) 在自定义日期模型属性中使用以下 GetClientValidationRules 版本:

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            ModelClientValidationRule rule = new ModelClientValidationRule();
            rule.ErrorMessage = this.ErrorMessageString;
            rule.ValidationType = "extendeddate";
            rule.ValidationParameters.Add("isrequired", metadata.IsRequired.ToString().ToLower());
            rule.ValidationParameters.Add("disablestandardvalidation", true.ToString().ToLower());
    
            yield return rule;
        }
    

    注意:最重要的一行是添加的最后一个验证参数 - 稍后将解释原因。现在,请记住,这将转换为名为“data-val-extendeddate-disablestandardvalidation”的 HTML 属性。

    b) 在外部 js 文件中(不管在哪里 - 只是一个示例,但最好在加载所有外部库之后),编写以下代码块:

    $(document).ready(function () {
    var currentCulture = $("meta[name='accept-language']").prop("content");
    
    // Set Globalize to the current culture driven by the meta tag (if any)
    if (currentCulture) {
        Globalize.culture(currentCulture);
    }
    
    $.validator.methods.date = function (value, element) {
        var isDateValidationDisabled = $(element).data("val-extendeddate-disablestandardvalidation");
    
        if (typeof isDateValidationDisabled != "undefined") {
            return true;
        }
    
        var val = Globalize.parseDate(value);
        return this.optional(element) || (val);
    };
    
    $.validator.methods.number = function (value, element) {
        var val = Globalize.parseFloat(value);
        return this.optional(element) || ($.isNumeric(val));
    }; });
    

    注意:在此代码块中,我还将为我的应用程序的其他可能文化加载 globalize jquery 插件。

    最后一个代码块最有趣的部分是,如果我在经过验证的控件上找到该数据属性,我将通过标准验证并返回 true。

    底线以及我这样做的原因 - 当您有一个应该作为一个整体进行验证的复杂控件时,标准验证器将无法工作,因为它会尝试从每个修改后的下拉列表中提取日期。我花了 8 小时才意识到为什么标准验证器仍然起作用。

    祝你好运!

    PS:我希望你理解我的 cmets - 我仍然对我真的修复它感到兴奋!

    【讨论】:

      【解决方案4】:

      我尝试了 Darin Dimitrov 的解决方案,但它有一些小问题。

      其中一个问题是与默认的 MVC 4 Javascript 日期验证器冲突 - 有时它甚至会针对有效日期启动并让网站用户感到困惑。我发明了一个解决方案,可以在这里找到: How to remove default client-side validators?

      第二个问题是该解决方案为所有三个下拉菜单生成相同的 id 属性,这并不好 - 每个 HTML 页面的 id 应该是唯一的。这是我修复它的方法:

      <div class="trippleddldatetime">
          @Html.DropDownList("", years, "Year:", new { id = @ViewData.TemplateInfo.HtmlFieldPrefix + "_y" })
          @Html.DropDownList("", months, "Month:", new { id = @ViewData.TemplateInfo.HtmlFieldPrefix + "_m" })
          @Html.DropDownList("", days, "Day:", new { id = @ViewData.TemplateInfo.HtmlFieldPrefix + "_d" })
      </div>
      

      最后一个问题是,当我尝试从 Controller 中为它们预设一些值时,这些下拉列表引发了异常。这是我修复它的方法:

      var result = ViewData.ModelState[ViewData.TemplateInfo.HtmlFieldPrefix];
      if (result != null && result.Value != null)
      { 
          var values = result.Value.RawValue as string[];
          years = new SelectList(years, "Value", "Text", values[0]);
          months = new SelectList(months, "Value", "Text", values[1]);
          days = new SelectList(days, "Value", "Text", values[2]);
          result.Value = null;
      }
      else
      {
          var currentValue = ViewData.Model;
          if (currentValue != null)
          {
              years = new SelectList(years, "Value", "Text", currentValue.Year);
              months = new SelectList(months, "Value", "Text", currentValue.Month.ToString("00"));
              days = new SelectList(days, "Value", "Text", currentValue.Day.ToString("00"));
          }
      }
      

      最后的改进 - 月份名称作为文本:

      var months = Enumerable.Range(1, 12).Select(x => new SelectListItem { Value = x.ToString("00"), Text = System.Threading.Thread.CurrentThread.CurrentUICulture.DateTimeFormat.GetMonthName(x) });
      

      【讨论】:

      • 第二个代码块放在哪里并不是很明显:var result = ViewData.ModelState...。它进入模型活页夹吗?或者在您尝试自己设置默认值的控制器中?什么最终消耗了result var?
      • 我帖子中的所有代码都进入 \Views\Shared\EditorTemplates\TriippleDDLDateTime.cshtml
      猜你喜欢
      • 1970-01-01
      • 2013-12-01
      • 2012-01-02
      • 1970-01-01
      • 2011-10-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多