5

對於ASP.NET和MVC,我還是比較新的,儘管數日來一直在使用Google和實驗,但我在解決此問題的最佳方法上留下了一片空白。ASP.NET MVC-使用3個下拉列表保留無效的DateTime選擇

我寫了一個BirthdayAttribute,我想工作類似於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來做這個問題的一個爭論點,但我寧願有適當的服務器端驗證到位回落。

+0

如果您將更改事件添加到下拉列表中,您可以在隱藏字段中創建完整日期並驗證它,然後可以從隱藏文本框值中重新選擇下拉列表中的值,但是這樣做並且會客戶端代碼 – davethecoder

回答

2

我終於設法解決了我的問題,所以我想分享我的解決方案。

免責聲明: 雖然我以前很喜歡.NET 2.0,但我現在只是將我的技能更新到最新版本的C#,ASP.NET,MVC和實體框架。如果有更好的方法可以做我以下做的任何事情,請隨時反饋。

TODO:

  • 無效的日期,如2月30實現客戶端驗證。客戶端驗證[必需]屬性已經內置的。

  • 添加的文化支持,使日期所需的格式

解決方案來找我顯示出來,當我意識到這個問題我因爲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, 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對象時,它將爲您處理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,你可以把你的日期字段和屬性。如果您傳入資源文件類和資源密鑰,它將在您的「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,但這是我唯一可以找出如何僅使用服務器端代碼來維護無效選擇的唯一方法。

你會做出什麼改進?