我有一个模型可以验证,问题是出生日期。 它必须由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);
}
问题:
LE: 我为这样的日期创建了一个自定义模型绑定器:
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;
});
});
客户端验证似乎不起作用。 我该如何解决?我对这些适配器的工作方式感到困惑。
答案 0 :(得分:14)
您可以使用自定义编辑器模板。
让我们先看看最终解决方案在进入实施细节之前的表现。
因此我们可以使用一些数据注释属性来装饰一个视图模型(一如既往),这些属性指示我们想要附加到它的元数据:
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;
}
}
注意如果字段未使用自定义属性修饰,则绑定器如何仅使用默认绑定器。这样它就不会干扰我们不想要tripple ddl行为的其他DateTime字段。模型活页夹只会与DateTime?
中的Application_Start
类型相关联:
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>
答案 1 :(得分:2)
从一开始我想说我在这里写的是在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:我希望你能理解我的评论 - 我真的很高兴我已经解决了这个问题!答案 2 :(得分:0)
我想添加Darin Dmitrov's answer,当您选择年,月和日时,用于验证年份和月份的红色边框仍然有效。我们希望在输入有效日期时其他两个组件也同步,所以我调整了JavaScript,如下所示。 (根据日期验证结果添加和调用两个函数removeChildValidationErrors&amp; 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');
});