我写了一个我想要工作的BirthdayAttribute类似于EmailAddressAttribute. birthday属性设置UI提示,以便使用具有3个下拉列表的编辑器模板呈现生日DateTime.该属性还可用于设置一些额外的元数据,告知年份下拉列表应显示的年数.
我知道我可以使用jQuery的日期选择器,但在生日的情况下,我发现3个下拉菜单更有用.
@model DateTime @using System; @using System.Web.Mvc; @{ UInt16 numberOfVisibleYears = 100; if (ViewData.ModelMetadata.AdditionalValues.ContainsKey("NumberOfVisibleYears")) { numberOfVisibleYears = Convert.ToUInt16(ViewData.ModelMetadata.AdditionalValues["NumberOfVisibleYears"]); } var Now = DateTime.Now; var years = Enumerable.Range(0,numberOfVisibleYears).Select(x => new SelectListItem { Value = (Now.Year - x).ToString(),Text = (Now.Year - x).ToString() }); var months = Enumerable.Range(1,12).Select(x => new SelectListItem{ Text = new DateTime( Now.Year,x,1).ToString("MMMM"),Value = x.ToString() }); var days = Enumerable.Range(1,31).Select(x => new SelectListItem { Value = x.ToString("00"),Text = x.ToString() }); } @Html.DropDownList("Year",years,"<Year>") / @Html.DropDownList("Month",months,"<Month>") / @Html.DropDownList("Day",days,"<Day>")
我还有一个ModelBinder来重建我的约会.为简洁起见,我删除了辅助函数的内容,但到目前为止一切都很好.正常,有效的日期,适用于创建或编辑我的成员.
public class DateSelector_DropdownListBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext,ModelBindingContext bindingContext) { if (controllerContext == null) throw new ArgumentNullException("controllerContext"); if (bindingContext == null) throw new ArgumentNullException("bindingContext"); if (IsDropdownListBound(bindingContext)) { int year = GetData(bindingContext,"Year"); int month = GetData(bindingContext,"Month"); int day = GetData(bindingContext,"Day"); DateTime result; if (!DateTime.TryParse(string.Format("{0}/{1}/{2}",year,month,day),out result)) { //Todo: SOMETHING MORE USEFUL??? bindingContext.ModelState.AddModelError("",string.Format("Not a valid date.")); } return result; } else { return base.BindModel(controllerContext,bindingContext); } } private int GetData(ModelBindingContext bindingContext,string propertyName) { // parse the int using the correct value provider } private bool IsDropdownListBound(ModelBindingContext bindingContext) { //check model Meta data UI hint for above editor template } }
现在我正在看它,我应该使用一个可以为空的DateTime,但那既不在这里也不在那里.
我遇到的问题是对无效日期进行非常基本的验证,例如2月30日或9月31日.验证本身效果很好,但是在重新加载表单时,永远不会保存和保留无效日期.
我想要的是记住2月30日的无效日期,并使用验证消息重新显示它,而不是将下拉列表重置为默认值.其他字段,如电子邮件地址(使用EmailAddressAttribute修饰)保留了无效的条目,开箱即用.
目前我只是想让服务器端验证工作.说实话,我还没有开始考虑客户端验证.
我知道有很多我可以使用javascript和ajax来解决这个问题,但我仍然宁愿让适当的服务器端验证到位.
解决方法
免责声明:
虽然我以前在.NET 2.0上做得很好,但我现在才将我的技能更新到最新版本的C#,ASP.NET,MVC和Entity Framework.如果有更好的方法可以做我在下面做过的任何事情,那么我总是愿意接受反馈.
去做:
>为无效日期(如2月30日)实施客户端验证.已内置[required]属性的客户端验证.
>添加对文化的支持,以便日期以所需格式显示
当我意识到我遇到的问题是DateTime不允许使用无效日期(如2月30日)构建时,解决方案就出现了.它只是抛出异常.如果我的日期不会构建,我知道无法将无效数据通过绑定器传递回viewmodel.
为了解决这个问题,我不得不在视图模型中取消DateTime并将其替换为我自己的自定义Date类.如果禁用Javascript,下面的解决方案将提供功能完备的服务器端验证.如果出现验证错误,则在显示验证消息后,无效选择将保持不变,从而允许用户轻松修复错误.
将此view-ish Date类映射到日期模型中的DateTime应该很容易.
Date.cs
public class Date { public Date() : this( System.DateTime.MinValue ) {} public Date(DateTime date) { Year = date.Year; Month = date.Month; Day = date.Day; } [required] public int Year { get; set; } [required,Range(1,12)] public int Month { get; set; } [required,31)] public int Day { get; set; } public DateTime? DateTime { get { DateTime date; if (!System.DateTime.TryParseExact(string.Format("{0}/{1}/{2}",Year,Month,Day),"yyyy/M/d",CultureInfo.InvariantCulture,DateTimeStyles.None,out date)) return null; else return date; } } }
这只是一个可以从DateTime构造的基本日期类.该类具有Year,Month和Day属性以及DateTime getter,可以尝试在您拥有有效日期时检索DateTime类.否则返回null.
当内置的DefaultModelBinder将表单映射回此Date对象时,它将为您处理required和Range验证.但是,我们需要一个新的ValidationAtribute来确保不允许使用诸如2月30日之类的无效日期.
DateValidationAttribute.cs
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field,AllowMultiple = false,Inherited = true)] public sealed class DateValidationAttribute : ValidationAttribute { public DateValidationAttribute(string classKey,string resourceKey) : base(HttpContext.GetGlobalResourceObject(classKey,resourceKey).ToString()) { } public override bool IsValid(object value) { bool result = false; if (value == null) throw new ArgumentNullException("value"); Date tovalidate = value as Date; if (tovalidate == null) throw new ArgumentException("value is an invalid or is an unexpected type"); //DateTime returns null when date cannot be constructed if (tovalidate.DateTime != null) { result = (tovalidate.DateTime != DateTime.MinValue) && (tovalidate.DateTime != DateTime.MaxValue); } return result; } }
这是一个ValidationAttribute,您可以将其放在Date字段和属性上.如果传入资源文件类和资源键,它将在“App_GlobalResources”文件夹中搜索相应的资源文件以查找错误消息.
在IsValid方法内部,一旦我们确定我们正在验证Date,我们检查它的DateTime属性,看它是否为null,以确认它是否有效.我会检查DateTime.MinValue和MaxValue以获得良好的衡量标准.
这就是真的.有了这个Date类,我设法完全取消了自定义的ModelBinder.此解决方案完全依赖于DefaultModelBinder,这意味着所有验证都可以立即使用.它显然甚至检查我的新DateValidationAttribute,我非常兴奋.我一直强调我可能不得不在自定义绑定器中使用验证器.这感觉更干净.
这是我正在使用的局部视图的完整代码.
DateSelector_DropdownList.cshtml
@model Date @{ UInt16 numberOfVisibleYears = 100; if (ViewData.ModelMetadata.AdditionalValues.ContainsKey("NumberOfVisibleYears")) { numberOfVisibleYears = Convert.ToUInt16(ViewData.ModelMetadata.AdditionalValues["NumberOfVisibleYears"]); } var Now = DateTime.Now; var years = Enumerable.Range(0,12).Select(x => new SelectListItem { Text = new DateTime(Now.Year,31).Select(x => new SelectListItem { Value = x.ToString(),"<Day>")
我还将包含我使用的属性,用于设置模板提示和要显示的可见年份数.
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field,AllowMultiple = false)] public sealed class DateSelector_DropdownListAttribute : DataTypeAttribute,IMetadataAware { public DateSelector_DropdownListAttribute() : base(DataType.Date) { } public void OnMetadataCreated(ModelMetadata Metadata) { Metadata.AdditionalValues.Add("NumberOfVisibleYears",NumberOfVisibleYears); Metadata.TemplateHint = TemplateHint; } public string TemplateHint { get; set; } public int NumberOfVisibleYears { get; set; } }
我认为解决方案比我预期的要清洁得多.它以我希望的确切方式解决了我的所有问题.我确实希望我能以某种方式保留DateTime,但这是我能够弄清楚如何仅使用服务器端代码来维护无效选择的唯一方法.
你会做出什么改进吗?