mirind4
mirind4

Reputation: 1573

ASP.net core field of viewmodel is required despite the fact that it is nullable

I am building an web-app with Asp.net core. I am using the build-in core validation methodology. I have a form where I would like to apply client-side validation using data annotations. My model looks like the following:

[Display(Name= "Address")] 
[Required]
[StringLength(80, MinimumLength = 5)]
public string Address { get; set; }

[Display(Name = "When it is needed")]
[Required]
[DataType(DataType.Date)]
public DateTime TakeDownDate { get; set; }

[Display(Name = "Exact time")]
[Required]
[DataType(DataType.Time)]
public TimeSpan ExactTimeToTakeDown { get; set; }

[Display(Name = "Content of container")]
public ContainerContentEnum? Content { get; set; }

[Display(Name = "When to take up")]
[DataType(DataType.Date)]
public DateTime? TakeUpDate { get; set; }   

[Display(Name= "Other comment")]
[StringLength(250)]
public string Other { get; set; }

[Display(Name = "Multiple round")]
public bool MaybeMoreRound { get; set; }

Let's see the TakeUpDate property. It is nullable, so I allow it for the user to not type anything in the field to submit the form. After the form is submitted, it is processed by the corresponding controller method:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult SaveNewClaim(NewClaimViewModel newClaim)
{
    if (ModelState.IsValid)
    {
        _claimsRepository.Insert(_newClaimViewModelToClaimConverterService.Convert(newClaim));
        return CreateJsonResult(true, "Az igény sikeresen lementésre került");
    }

    return CreateJsonResult(false, GetErrorMessages());
}

private JsonResult CreateJsonResult(bool isSuccess, string responseMessage)
{
    return Json(new
        {
            success = isSuccess,
            responseText = responseMessage
        }
    );
}

private string GetErrorMessages()
{
    return string.Join(";\n", ModelState.Values
        .SelectMany(x => x.Errors)
        .Select(x => x.ErrorMessage));
}

And as for the view, the .cshtml file looks like the following:

@using App.ViewModels.ClaimViewModels.ClaimModels
@model App.ViewModels.ClaimViewModels.ClaimModels.NewClaimViewModel

<div id="calendarModal" class="modal fade">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true"></span> <span class="sr-only">close</span></button>
                <h3><b>Új igény felvétele</b></h3>
            </div>
            <div id="modalBody" class="modal-body">
                <div asp-validation-summary="ModelOnly" class="text-danger"></div>
                <form asp-action="SaveNewClaim" id="EventForm" class="well">
                    <input type="hidden" id="eventID" class="form-control">
                    <div class="form-group">
                        <label asp-for="Address" class=""></label>
                        <div class="">
                            <input asp-for="Address" class="form-control">
                            <span asp-validation-for="Address" class="text-danger"> </span>
                        </div>
                    </div>
                    <div class="form-group">
                        <label asp-for="TakeDownDate" class=""></label>
                        <div class="">
                            <input asp-for="TakeDownDate" class="form-control" />
                            <span asp-validation-for="TakeDownDate" class="text-danger"> </span>
                        </div>
                    </div>
                    <div class="form-group">
                        <label asp-for="ExactTimeToTakeDown" class=""></label>
                        <div class="">
                            <input asp-for="ExactTimeToTakeDown" class="form-control" />
                            <span asp-validation-for="ExactTimeToTakeDown" class="text-danger"> </span>
                        </div>
                    </div>
                    <div class="form-group">
                        <label asp-for="Content" class=""></label>
                        <div class="">
                            <select asp-for="Content" asp-items="@Html.GetEnumSelectList<MokaKukaMap.Domain.Model.ContainerContentEnum>()" class="form-control">
                                <option selected="selected" value="">Kérem válassz</option>
                            </select>
                            <span asp-validation-for="Content" class="text-danger"> </span>
                        </div>
                    </div>
                    <div class="form-group">
                        <label asp-for="TakeUpDate" class=""></label>
                        <div class="">
                            <input asp-for="TakeUpDate" class="form-control" />
                            <span asp-validation-for="TakeUpDate" class="text-danger"> </span>
                        </div>
                    </div>
                    <div class="form-group">
                        <label asp-for="Other" class=""></label>
                        <div class="">
                            <input asp-for="Other" class="form-control" />
                            <span asp-validation-for="Other" class="text-danger"> </span>
                        </div>
                    </div>
                    <div class="form-group">
                        <label asp-for="MaybeMoreRound" class=""></label>
                        <div class="">
                            <input asp-for="MaybeMoreRound" type="checkbox" class="form-control" />
                            <span asp-validation-for="MaybeMoreRound" class="text-danger"> </span>
                        </div>
                    </div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" id="btnPopupCancel" data-dismiss="modal" class="btn">Vissza</button>
                <button type="button" id="btnPopupSave" class="btn btn-primary">Igény mentése</button>
            </div>
        </div>
    </div>
</div>

Last but not least my ajax post (click event) on submit button:

$('#btnPopupSave').click(function () {
    var dataRow = {
        Address: $('#Address').val(),
        TakeDownDate: $('#TakeDownDate').val(),
        ExactTimeToTakeDown: $('#ExactTimeToTakeDown').val(),
        Content: $('#Content').val(),
        TakeUpDate: $('#TakeUpDate').val(),
        Other: $('#Other').val(),
        MaybeMoreRound: $('#MaybeMoreRound').val()
    }

    console.log('Submitting form...');
    $.ajax({
        type: 'POST',
        url: 'SaveNewClaim',
        data: dataRow,
        headers:
        {
            "RequestVerificationToken": '@GetAntiXsrfRequestToken()'
        },
        dataType: 'json',
        success: function (response) {
            if (response.success) {
                $('#calendarModal').modal('hide');
                $('#calendar').fullCalendar('refetchEvents');
                alert(response.responseText);
            }
            else {
                alert(response.responseText);
            }
        },
        error: function(xMlHttpRequest, textStatus, errorThrown) {
            console.log(XMLHttpRequest.responseText);
            console.log(textStatus);
            console.log(errorThrown);
        }
    });
});

Despite the fact that I set the TakeUpDate nullable, my model is still invalid and I got the following error message: The value 'When to take up' is invalid. enter image description here

To be honest I checked every relating Q/As here in stackoverflow, and also other forums, and all of them say that it is the right way to set a field nullable.

If you need any more code, I add it to this question right away...

Upvotes: 3

Views: 2029

Answers (1)

melkisadek
melkisadek

Reputation: 1053

The fundamental problem is that you're passing an empty string back to the controller.

You either need to capture that empty string and convert it to a null before model binding takes place, or you need to work around the known 'bug' and deal with the fact your validation will always fail at this point.

You might be able to change the Ajax POST to explicitly send a null.

Alternatively (and probably what I'd do) is just sanity check your date in the controller. If it's an empty string, then remove it from the ModelState so it isn't flagged as invalid (as you already know what it is and why). The ModelState will still validate the other fields as normal.

Something like the following:

public IActionResult SaveNewClaim(NewClaimViewModel newClaim)
{
    if (newClaim.TakeUpDate == "")
    {
        ModelState.Remove("TakeUpDate");
    }

    if (ModelState.IsValid)
    {
        newClaim.TakeUpDate = null;

        _claimsRepository.Insert(_newClaimViewModelToClaimConverterService.Convert(newClaim));
        return CreateJsonResult(true, "Az igény sikeresen lementésre került");
    }

    return CreateJsonResult(false, GetErrorMessages());
}

Upvotes: 3

Related Questions