5

Sono ancora abbastanza nuovo per ASP.NET e MVC e nonostante i giorni di ricerca su Google e di sperimentazione, sto disegnando uno spazio vuoto sul modo migliore per risolvere questo problema .ASP.NET MVC - Preservare la selezione DateTime non valida con 3 elenchi a discesa

Ho scritto un CompleannoAttributo che voglio lavorare in modo simile a EmailAddressAttribute. L'attributo compleanno imposta l'hint UI in modo che il compleanno DateTime venga reso utilizzando un modello di editor con 3 elenchi a discesa. L'attributo può anche essere utilizzato per impostare alcuni metadati aggiuntivi che indicano il numero di anni in cui deve essere visualizzato.

So che potrei usare il selezionatore di date di jQuery, ma nel caso di un compleanno trovo i 3 menu a discesa molto più utilizzabili.

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

Ho anche un ModelBinder per ricostruire la data in seguito. Ho rimosso il contenuto delle mie funzioni di supporto per brevità, ma tutto funziona alla grande fino a questo punto. Date normali e valide, funzionano bene per creare o modificare i miei membri.

Ora che lo sto guardando, probabilmente dovrei usare un valore DateTime nullable, ma non è né qui né lì.

Il problema riscontrato è la convalida molto semplice di date non valide come il 30 febbraio o il 31 settembre. La convalida stessa funziona alla grande, ma le date non valide non vengono mai salvate e persistono quando il modulo viene ricaricato.

Quello che vorrei è ricordare la data non valida del 30 febbraio e visualizzarla di nuovo con il messaggio di convalida invece di reimpostare i menu a discesa sul loro valore predefinito. Altri campi, come l'indirizzo email (decorato con EmailAddressAttribute) conservano le voci non valide subito bene fuori dalla scatola.

Al momento sto solo cercando di far funzionare la validazione lato server. Ad essere onesti, non ho ancora iniziato a pensare alla convalida del lato client.

So che ci sono molte cose che potrei fare con javascript e ajax per rendere questo problema un punto controverso, ma preferirei comunque avere la validazione sul lato server appropriata in loco per ripiegare.

+0

se si aggiungono eventi di modifica al menu a discesa, è possibile creare una data completa in un campo nascosto e convalidarlo, è quindi possibile riselezionare i valori nel menu a discesa dal valore della casella di testo nascosta, tuttavia questo è e sarebbe codice lato client – davethecoder

risposta

2

Sono finalmente riuscito a risolvere il mio problema, quindi ho voluto condividere la mia soluzione.

DISCLAIMER: Anche se ero abituato ad essere fantastico con .NET 2.0 nel corso della giornata, sto solo ora aggiornando le mie competenze alle ultime versioni di C#, ASP.NET, MVC ed Entity Framework. Se ci sono modi migliori per fare qualsiasi cosa ho fatto qui sotto per favore sono sempre aperto al feedback.

TODO:

  • Implementare validazione lato client per le date non valide, come 30 feb. validazione lato client per [Required] attributo è già costruito.

  • aggiungere il supporto per le culture in modo che la data si presenta in formato desiderato

La soluzione mi è venuta quando ho capito che il problema che ho Stava avendo è che DateTime non permetterà a se stesso di essere costruito con una data non valida come il 30 febbraio. Semplicemente lancia un'eccezione. Se la mia data non venisse costruita, non sapevo come trasferire i dati non validi attraverso il raccoglitore al ViewModel.

Per risolvere questo problema, ho dovuto eliminare il DateTime nel mio modello di vista e sostituirlo con la mia classe Date personalizzata. La soluzione seguente fornirà una convalida lato server completamente funzionante nel caso in cui Javascript sia disabilitato. Nel caso di un errore di convalida, le selezioni non valide verranno mantenute dopo la visualizzazione del messaggio di convalida, consentendo all'utente di correggere facilmente il proprio errore.

Dovrebbe essere abbastanza semplice mappare questa classe di visualizzazione data-ish a DateTime nel modello di data.

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

Questa è solo una classe data base che si può costruire da un DateTime. La classe ha proprietà per Anno, Mese e Giorno e un getter DateTime che può provare a recuperare una classe DateTime assumendo che tu abbia una data valida. Altrimenti restituisce null.

Quando il predefinito DefaultModelBinder esegue il mapping del modulo su questo oggetto Date, si prenderà cura della convalida Richiesto e Intervallo. Tuttavia, avremo bisogno di un nuovo ValidationAtribute per assicurarci che date non valide come il 30 febbraio non siano consentite.

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

Questa è una ValidationAttribute che si può mettere sui vostri campi e proprietà data. Se passi la classe del file di risorse e la chiave della risorsa, cercherà il file di risorse corrispondente nella cartella "App_GlobalResources" per il messaggio di errore.

All'interno del metodo IsValid, una volta verificato che stiamo convalidando una data, controlliamo la sua proprietà DateTime per vedere se non è nulla per confermare che sia valida. Getto un assegno per DateTime.MinValue e MaxValue per buona misura.

Quindi questo è tutto. Con questa classe Date, sono riuscito a eliminare completamente il ModelBinder personalizzato. Questa soluzione si basa completamente su DefaultModelBinder, il che significa che tutte le convalide funzionano immediatamente. Apparentemente controlla anche il mio nuovo DateValidationAttribute, cosa di cui ero davvero entusiasta. Ho insistito per sempre pensando che potrei dover ammazzare con i validatori in un raccoglitore personalizzato. Questo sembra molto più pulito.

Ecco il codice completo per la vista parziale che sto utilizzando.

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

sarò includono anche l'attributo che uso che imposta il suggerimento modello e il numero di anni visibili per mostrare.

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

Penso che la soluzione sia risultata molto più pulita di quanto mi aspettassi. Risolve tutti i miei problemi nel modo esatto in cui speravo. Vorrei essere in qualche modo in grado di mantenere il DateTime, ma questo è l'unico modo per capire come mantenere una selezione non valida usando solo il codice lato server.

Esistono miglioramenti da apportare?