我对 ASP.NET 和 MVC 还很陌生,尽管进行了几天的谷歌搜索和试验,但我在解决这个问题的最佳方法上仍是一片空白。
我写了一个生日属性,我想像 EmailAddressAttribute 一样工作。生日属性设置 UI 提示,以便使用具有 3 个下拉列表的编辑器模板呈现生日日期时间。该属性还可用于设置一些额外的元数据,告诉年份下拉菜单应该显示多少年。
我知道我可以使用 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。如果有更好的方法来做我在下面所做的任何事情,我总是愿意接受反馈。
去做:
当我意识到我遇到的问题是 DateTime 不允许使用无效日期(例如 2 月 30 日)构建自身时,我找到了解决方案。它只是抛出一个异常。如果我的日期无法构建,我就无法将无效数据通过活页夹传递回 ViewModel。
为了解决这个问题,我不得不在我的 View 模型中取消 DateTime 并用我自己的自定义 Date 类替换它。在禁用 Javascript 的情况下,以下解决方案将提供功能齐全的服务器端验证。在验证错误的情况下,无效的选择将在显示验证消息后持续存在,从而允许用户轻松修复他们的错误。
将这个 View 式的 Date 类映射到日期模型中的 DateTime 应该很容易。
日期.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, Range(1, 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 对象时,它将为您处理所需的和范围验证。但是,我们需要一个新的 ValidationAtribute 来确保不允许使用无效日期,例如 2 月 30 日。
日期验证属性.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。我一直强调我可能不得不在自定义活页夹中使用验证器。这感觉干净了很多。
这是我正在使用的部分 View 的完整代码。
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, 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(), Text = x.ToString() });
}
@Html.DropDownList("Year", years, "<Year>") /
@Html.DropDownList("Month", months, "<Month>") /
@Html.DropDownList("Day", days, "<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,但这是我可以弄清楚如何仅使用服务器端代码来维护无效选择的唯一方法。
你会做任何改进吗?
关于validation - ASP.NET MVC - 使用 3 个下拉列表保留无效的日期时间选择,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16085132/