4

I'm still fairly new to ASP.NET and MVC and despite days of googling and experimenting, I'm drawing a blank on the best way to solve this problem.

I wrote a BirthdayAttribute that I want to work similar to the EmailAddressAttribute. The birthday attribute sets the UI hint so that the birthday DateTime will be rendered using an editor template that has 3 dropdown lists. The attribute can also be used to set some additional meta data that tells the year dropdown how many years it should display.

I know I could use jQuery's date picker, but in the case of a birthday I find the 3 dropdowns much more usable.

@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>")

I also have a ModelBinder to rebuild my date afterwards. I've removed the content of my helper functions for brevity, but everything works great up to this point. Normal, valid dates, work just fine for creating or editing my members.

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
    }
}

Now that I'm looking at it, I should probably be using a nullable DateTime, but that's neither here nor there.

The problem I'm having is with very basic validation of invalid dates such as February 30th, or September 31st. The validation itself works great, but the invalid dates aren't ever saved and persisted when the form is reloaded.

What I'd like is to remember the invalid date of February 30th and redisplay it with the validation message instead of resetting the dropdowns to their default value. Other fields, like the email address (decorated with the EmailAddressAttribute) preserve invalid entries just fine out of the box.

At the moment I am just trying to get the server side validation working. To be honest, I haven't even started thinking about the client side validation yet.

I know there is lots I could do with javascript and ajax to make this problem a moot point, but I would still rather have the proper server side validation in place to fall back on.

4

1 回答 1

2

我终于设法解决了我的问题,所以我想分享我的解决方案。

免责声明:虽然我过去在 .NET 2.0 上表现出色,但我现在才将我的技能更新到最新版本的 C#、ASP.NET、MVC 和 Entity Framework。如果有更好的方法来做我在下面所做的任何事情,我总是愿意接受反馈。

去做:

  • 对无效日期(例如 2 月 30 日)实施客户端验证。[Required] 属性的客户端验证已内置。

  • 添加对文化的支持,以便日期以所需格式显示

当我意识到我遇到的问题是 DateTime 不允许使用无效的日期(例如 2 月 30 日)构建自身时,我找到了解决方案。它只是抛出一个异常。如果我的日期无法构建,我就无法将无效数据通过活页夹传递回 ViewModel。

为了解决这个问题,我不得不在我的视图模型中取消 DateTime 并用我自己的自定义 Date 类替换它。在禁用 Javascript 的情况下,以下解决方案将提供功能齐全的服务器端验证。在验证错误的情况下,无效的选择将在显示验证消息后持续存在,从而允许用户轻松修复他们的错误。

将这个视图式的 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。我一直强调我可能不得不在自定义活页夹中使用验证器。这感觉干净了很多。

这是我正在使用的部分视图的完整代码。

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,但这是我可以弄清楚如何仅使用服务器端代码来维护无效选择的唯一方法。

你会做任何改进吗?

于 2013-04-19T02:26:38.600 回答