EditorFor on nullable DateTime - “Nullable object

2020-02-29 23:38发布

问题:

I am attempting to display a form that allows a user to input a new assignment for a person. I'm using a DateTime.cshtml EditorTemplate to handle DateTime values for the assignment. The non-nullable DateTime works fine. The nullable DateTime causes an "InvalidOperationException: Nullable object must have a value."

I have a simple viewmodel that looks like this:

AssignmentViewModel.cs:

public Person Person { get; set; }
public Assignment NewAssignment { get; set; }

Assignment.cs contains:

public DateTime AssignmentStartDate { get; set; }
public DateTime? AssignmentEndDate { get; set; }

My AssignmentController Create() method looks like:

public ViewResult Create(int personId)
{
    Person person = personRepository.GetPersonById(personId);
    var newAssignment = new AssignmentViewModel { Person = person, NewAssignment = new Assignment() };
    return View(newAssignment);
}

My Create.cshtml view looks like this:

@model AssignmentViewModel

@using (Html.BeginForm("Create", "Assignment"))
{
    @Html.Hidden("NewAssignment.PersonId", Model.Person.PersonId)
    @Html.LabelFor(x => x.NewAssignment.AssignmentStartDate):
    @Html.EditorFor(x => x.NewAssignment.AssignmentStartDate.Date, new { cssClass = "datePicker" })
    <br />
    @Html.LabelFor(x => x.NewAssignment.AssignmentEndDate):
    @Html.EditorFor(x => x.NewAssignment.AssignmentEndDate.Value.Date, new { cssClass = "datePicker" })
    <br />
    <input type="submit" value="Send />
}

My DateTime.cshtml EditorTemplate looks like:

@model DateTime?

@{
    String modelValue = "";
    if (Model.HasValue)
    {
        if (Model.Value != DateTime.MinValue)
        {
            modelValue = Model.Value.ToShortDateString();
        }
    }
}

@Html.TextBox("", modelValue, new { @class = "datePicker" })

When I attempt to load the Create view, I get the exception mentioned above on the line "@Html.EditorFor(x => x.NewAssignment.AssignmentEndDate.Value)".

You may be wondering why I'm passing in AssignmentEndDate.Value.Date instead of just passing in AssignmentEndDate; the reason is because I'm trying to get to the point where I'm splitting DateTime into Date and a TimeOfDay field and recombine them with a DateTimeModelBinder. I am using a similar technique to the one shown here and here.

I -can- bypass the error, by changing my controller Create() method to instantiate the ViewModel with AssignmentEndDate set to DateTime.MinValue, but this seems completely wrong for a nullable DateTime:

var newAssignment = new AssignmentViewModel 
                        { 
                            Person = person, 
                            NewAssignment = new Assignment { AssignmentEndDate = DateTime.MinValue } 
                        };

Something strange happens after I "bypass" the error by supplying a value for the nullable DateTime; the un-required nullable DateTime property (AssignmentEndDate.Date) fails client side validation. Trying to submit the form highlights the field in red.

How can I handle this correctly?

回答1:

The problem is that you're trying to retrieve the AssignmentEndDate.Value.Date, but AssignmentEndDate is null, which results in this error.

Since your editor template accepts a DateTime?, you should just pass along the AssignmentEndDate. In other words, remove the .Value.Date from the view:

@Html.EditorFor(x => x.NewAssignment.AssignmentEndDate, new { cssClass = "datePicker" })

Since your editor template is using ToShortDateString(), there's no need to "truncate" the time from the date at all.

Update

Regarding your desire to have separate "Date" and "Time" editors:

You can do this 2 ways.

1 - Your current DateTime? editor renders a field for the Model.Value.Date, so you could simply extend this to also render a field for the Model.Value.TimeOfDay. Example:

@{
  DateTime? modelDate = (Model == null) ? (DateTime?)null : Model.Value.Date;
  TimeSpan? modelTime = (Model == null) ? (TimeSpan?)null : Model.Value.TimeOfDay;
}
@Html.TextBox(..., modelDate, new{@class="datePicker"})
@Html.TextBox(..., modelTime, new{@class="timePicker"})

2 - You could split the above functionality into 2 separate editors, "DateOnly" and "TimeOnly". Then, update your view to call both editors:

@Html.EditorFor(x => x.NewAssignment.AssignmentEndDate, "DateOnly")
@Html.EditorFor(x => x.NewAssignment.AssignmentEndDate, "TimeOnly")

The choice is up to you, and whether you want to keep the Date and Time parts separate or together, but this is how I'd go about solving this problem.



回答2:

create a DateTime.cshtml in your Shared/DisplayTemplate folder

@model Nullable<DateTime>
@(Model != null ? string.Format(ViewData.ModelMetadata.DisplayFormatString ?? "{0:d}", Model) : string.Empty)

this supports metadata from datannotations to be used if found.



回答3:

UPDATE: GetValueOrDefault treats it as a DateTime and therefore the required Field validators are getting attached, because the original expression is for a datetime not a nullable datetime.

Therefore the solution below doesn't work.

Like the asker, I also used the DateTimeModelBinder from here: Here's the Link

This is how I solved a similar situation:

@Html.EditorFor(x => x.NewAssignment.AssignmentEndDate.GetValueOrDefault().Date)

And this is what my DateTime EditorTemplate looks like:

@model DateTime

@Html.TextBox("", Model != default(DateTime) ? Model.ToShortDateString() : String.Empty, new { @class = "datepicker", @maxlength = "10" })