I have the following "date" fields in my model that are representing as a string:
I have the following in my view:
My question is how do I get cast the string properly to fit the date picker?
I'm not sure if it should be done in the Model or View.
My pontifical points are in no particular permutation:
String
when a better value-type is available. DateOnly?
(a.k.a. Nullable<DateOnly>
). Nullable<T>
should be used because it allows for empty date inputs, which <input type="date" value="" />
allows, whereas a non-nullable DateOnly
view-model property is harder to work with when using ASP.NET's model-binding because any form field can be empty or invalid - or just plain undefined. You might think that the [Required]
attribute solves this problem but it doesn't: the [Required]
attribute doesn't stop a property being empty, it just stops ModelStateDictionary.IsValid
returning true
when a property is empty, which means you will always need to correctly (if not gracefully) handle empty/undefined form fields and differentiate that from whatever the default(T)
of a value-type is (e.g. default(DateOnly)
is .class MyPageViewModel
, which can expose the data as DateOnly?
-typed properties.<input type="date" />
requires that the value=""
parameter be formatted as yyyy-MM-dd
.<input asp-for="YMDEFF" type="date" />
instead of Html.TextBoxFor
. asp-for=""
is a specialModelExpression
attribute just like TextBoxFor
's Expression<>
parameter and it's used to generate the id=""
and name=""
attributes at runtime.public String SomeProperty { get; set; } = null!
- it's just wrong on so many levels. String
property can ever be null
then its type must be String?
- not String
- and the initialization to null
is just redundant.String
property will never be null
then initialize it to = String.Empty
or = ""
, not null!
, yikes.YMDEFF
and YMDEND
- in C#/.NET we use PascalCase
and camelCase
such that all words and acronyms 3 letters or longer are always TitleCase
, while all 2-letter initialisms are always uppercase (like IO
) while 2-letter abbreviations (like Db
for "Database") are not. YMDEFF
looks like "YMD Eff...", I assume YMD
refers to the date-format and that "Eff" is short for "Effective", so just named it EffectiveDate
.YMDEND
looks like "YMD End", by the same logic this should be named EndDate
.Like so:
public class MyPageViewModel { // [Required] // <-- Uncomment or delete this line, based on your actual requirements. [DisplayFormat( DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true )] // <-- This attribute is necessary to ensure the `<input value=""/>` attribute has the correct format that HTML requires. public DateOnly? EffectiveDate { get; set; } // [Required] [DisplayFormat( DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true )] public DateOnly? EndDate { get; set; } }
Your Razor markup could look like this:
@model MyPageViewModel // etc <form asp-controller="Foobar" asp-action="PostForm"> <div class="field"> <label asp-for="EffectiveDate"></label> <input asp-for="EffectiveDate" type="date" /> <span asp-validation-for="EffectiveDate"></span> </div> <div class="field"> <label asp-for="EndDate"></label> <input asp-for="EndDate" type="date" /> <span asp-validation-for="EndDate"></span> </div> <button type="submit">Submit</button> </form>
Your Controller Action would hold the logic for marshaling between your database's (abominable) String
-based dates and DateOnly
dates:
public sealed class FoobarController : Controller { [Route( "/my-form" )] // <-- Use [Route] for GET actions instead of [HttpGet] because it means ASP.NET can handle HEAD, and other request verbs, for you automagically. public async Task<IActionResult> GetForm() { SomeEntityClass? e = await this.db.GetThingAsync(); if( e is null ) return this.NotFound(); MyPageViewModel vm = new MyPageViewModel() { EffectiveDate = DateOnly.TryParseExact( s: e.YMDEFF, format: "yyyyMMdd", provider: CultureInfo.InvariantCulture, style: 0, out DateOnly parsed1 ) ? parsed1 : (DateOnly?)null, EndDate = DateOnly.TryParseExact( s: e.YMDEND, format; "yyyyMMdd", provider: CultureInfo.InvariantCulture, style: 0, out DateOnly parsed2 ) ? parsed2 : (DateOnly?)null }; return this.View( model: vm ); } [HttpPost( "/my-form" )] public async Task<IActionResult> PostForm( [FromForm] MyPageViewModel model ) { SomeEntityClass? e = await this.db.GetThingAsync(); if( e is null ) return this.NotFound(); if( !this.ModelState.IsValid ) return this.View( model: model ); Debug.Assert( model.EffectiveDate.HasValue ); // <-- This is to workaround how the C# compiler isn't aware that `ModelState.IsValid` indicates (for example) non-nullability of those View-Model properties. Debug.Assert( model.EndDate.HasValue ); // e.YMDEFF = model.EffectiveDate.Value.Tostring( format: "yyyyMMdd" ); e.YMDEND = model.EndDate .Value.Tostring( format: "yyyyMMdd" ); await this.db.SaveChangesAsync(); return this.RedirectToAction( nameof(GetForm) ); } }
EffectiveDate = DateOnly.TryParseExact( format; "yyyyMMdd", value: e.YMDEFF, out DateOnly parsed1 ) ? parsed1 : (DateOnly?)null,
is not valid for .NET 6 or 8??? 3) SomeEntityClass? e = await this.db.GetThingAsync();
...So I'm getting the data from the database (string from DB to DateOnly in the model) and then I'm converting the fields to a DateOnly again?SomeEntityClass
is just an example - your post didn't say where/how you got those values from your database so I had to make assumptions. I can adjust my answer to suit your situation but only if you add those details to your question post, which you haven't yet done.
DateOnly
orDateTime
in your application-code.= null!
with a non-nullableString
property's initializer is just wrong. If a property can benull
then it should be typed asString?
and reference-types don't need initializing tonull
because it's implicit: learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/… . - what your code is doing right-now is sabotaging itself and hiding the evidence of sabotage under the rug.