0
votes

I have a partial view on my main page that loads a form, the model is below:

public class CreateRequestViewModel
    {
        [Required]
        public short ClientId { get; set; }
        [Required]
        public Guid SystemId { get; set; }

        [Required]
        public string RequestedUsername { get; set; }
        public string TicketReference { get; set; }
        public string Notes { get; set; }

        public List<SelectListItem> Clients { get; set; }
        public List<SelectListItem> Systems { get; set; }
    }

This is the partial view:

@model Models.CreateRequestViewModel

@Html.AntiForgeryToken()
@Html.ValidationSummary(true, "", new { @class = "text-danger" })

<div class="row">
    <div class="col-lg-4 col-md-4 col-sm-12">
        <h1>Create a Request</h1>
    </div>
    <div class="col-lg-8 col-md-8 col-sm-12 right">
        <div class="form-group">
            @Html.DropDownListFor(m => m.ClientId, Model.Clients, htmlAttributes: new { @class = "form-control form-control-lg", @id = "ClientSelect" })
            @Html.ValidationMessageFor(m => m.ClientId, "", htmlAttributes: new { @class = "text-danger" })
        </div>
        <div class="form-group">
            @Html.DropDownListFor(m => m.SystemId, Model.Systems, htmlAttributes: new { @class = "form-control form-control-lg", @id = "ClientSystemSelect" })
            @Html.ValidationMessageFor(m => m.SystemId, "", htmlAttributes: new { @class = "text-danger" })
        </div>
        <div class="form-group">
            @Html.TextBoxFor(m => m.RequestedUsername, htmlAttributes: new { @class = "form-control form-control-lg", @placeholder = "Username" })
            @Html.ValidationMessageFor(m => m.RequestedUsername, "", htmlAttributes: new { @class = "text-danger" })
        </div>
        <div class="form-group">
            @Html.TextBoxFor(m => m.TicketReference, htmlAttributes: new { @class = "form-control form-control-lg", @placeholder = "Ticket reference" })
        </div>
        <div class="form-group">
            @Html.TextAreaFor(m => m.Notes, htmlAttributes: new { @class = "form-control form-control-lg", @rows = 3, @placeholder = "Notes..." })
        </div>
        <input type="Submit" class="btn btn-secondary btn-block send-request" value="Submit" name="">
    </div>
</div>

This is how I'm loading the page:

<div class="container">
    <div class="row">
        <div class="col-lg-6">
            <form asp-action="CreateRequest" asp-controller="Access"
                data-ajax="true"
                data-ajax-method="POST"
                data-ajax-mode="replace"
                data-ajax-update="#createRequest">
                <div id="createRequest">
                    @await Html.PartialAsync("_CreateRequest", Model.CreateRequestModel)
                </div>
            </form>
        </div>
    </div>
</div>

With the model as it is and using the unobtrusive javascript, leaving the RequestedUsername blank for example will result in the form not being submitted and a validation message appearing for it. This is great.

However, I have a requirement to check the form data against entries in a database first and throw an error if there's an existing record. I thought that, with all the client side validation passing, I'd use ModelState.AddModelError in the controller like so:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult CreateRequest(CreateRequestViewModel model)
{
    if(model.RequestedUsername == "someincorrectvalue"){ //actual logic removed for brevity
        ModelState.AddModelError("RequestedUsername", "Already in use");
    }

    if(!ModelState.IsValid)
    {
        //reset lists on model, removed
        return PartialView("_CreateRequest", model);
    }

    _logger.LogInformation("CreateRequest successful");

    return RedirectToAction(nameof(Index));
}

However, if I use ModelState.AddModelError, the return PartialView("_CreateRequest", model) call ends up reloading the whole page as if it's returning a full View.

I'm at a loss as to why this is happening. The difference I can see is that i'm adding a ModelState error inside the controller, whereas validation is happening client side otherwise.

Anyone have an idea?

1

1 Answers

0
votes

So, this turned out to be a combination of problems. To begin with, the unobtrusive Ajax scripts I had within my solution were not performing. I don't know why but I replaced them with one from the CDN: https://ajax.aspnetcdn.com/ajax/jquery.unobtrusive-ajax/3.2.5/jquery.unobtrusive-ajax.min.js

That solved the problem of the whole page reloading instead of the partial being returned via unobtrusive ajax.

The second problem was that, on success, I was redirecting to the Index controller action instead of returning a Partial again. This was causing the entire Index page to be rendered inside the div i'd chosen as my ajax target. My controller action now looks like this:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult CreateRequest(CreateRequestViewModel model)
{
    if(model.RequestedUsername == "someincorrectvalue"){ //actual logic removed for brevity
        ModelState.AddModelError("RequestedUsername", "Already in use");
    }

    if(!ModelState.IsValid)
    {
        //reset lists on model, removed
        return PartialView("_CreateRequest", model);
    }

    _logger.LogInformation("CreateRequest successful");

    // reset lists on model, removed
    ModelState.Clear(); // get rid ofany model details to make way for a new request
    return PartialView("_CreateRequest", model);
}