ASP.NET Core MVC Ajax Form requests using jquery-unobtrusive

This article shows how to send Ajax requests in an ASP.NET Core MVC application using jquery-unobtrusive. This can be tricky to setup, for example when using a list of data items with forms using the onchange Javascript event, or the oninput event.

Code: https://github.com/damienbod/AspNetCoreBootstrap4Validation

Setting up the Project

The project uses the npm package.json file, to add the required front end packages to the project. jquery-ajax-unobtrusive is added as well as the other required dependencies.

{
  "version": "1.0.0",
  "name": "asp.net",
  "private": true,
  "devDependencies": {
    "bootstrap": "4.1.3",
    "jquery": "3.3.1",
    "jquery-validation": "1.17.0",
    "jquery-validation-unobtrusive": "3.2.10",
    "jquery-ajax-unobtrusive": "3.2.4"
  }
}

bundleconfig.json is used to package and build the Javascript and the css files into bundles. The BuildBundlerMinifier NuGet package needs to be added to the project for this to work.

The Javascript libraries are packaged into 2 different bundles, vendor-validation.min.js and vendor-validation.min.js.

// Vendor JS
{
    "outputFileName": "wwwroot/js/vendor.min.js",
    "inputFiles": [
      "node_modules/jquery/dist/jquery.min.js",
      "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    "sourceMap": false
},
// Vendor Validation JS
{
    "outputFileName": "wwwroot/js/vendor-validation.min.js",
    "inputFiles": [
      "node_modules/jquery-validation/dist/jquery.validate.min.js",
      "node_modules/jquery-validation/dist/additional-methods.js",
      "node_modules/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js",
      "node_modules//jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    "sourceMap": false
}

The global bundles can be added at the end of the _Layout.cshtml file in the ASP.NET Core MVC project.


... 
    <script src="~/js/vendor.min.js" asp-append-version="true"></script>
    <script src="~/js/site.min.js" asp-append-version="true"></script>
    @RenderSection("scripts", required: false)
</body>
</html>

And the validation bundle is added to the _ValidationScriptsPartial.cshtml.

<script src="~/js/vendor-validation.min.js" asp-append-version="true"></script>

This is then added in the views as required.

@section Scripts  {
    @await Html.PartialAsync("_ValidationScriptsPartial")
}

Simple AJAX Form request

A form request can be sent as an Ajax request, by adding the html attributes to the form element. When the request is finished, the div element with the id attribute defined in the data-ajax-update parameter, will be replaced with the partial result response. The Html.PartialAsync method calls the initial view.

@{
    ViewData["Title"] = "Ajax Test Page";
}

<h4>Ajax Test</h4>

<form asp-action="Index" asp-controller="AjaxTest" 
      data-ajax="true" 
      data-ajax-method="POST"
      data-ajax-mode="replace" 
      data-ajax-update="#ajaxresult" >

    <div id="ajaxresult">
        @await Html.PartialAsync("_partialAjaxForm")
    </div>
</form>

@section Scripts  {
    @await Html.PartialAsync("_ValidationScriptsPartial")
}

The _partialAjaxForm.cshtml view implements the form contents. The submit button is required to send the request as an Ajax request.

@model AspNetCoreBootstrap4Validation.ViewModels.AjaxValidationModel 

<div asp-validation-summary="All" class="text-danger"></div>

<div class="form-group">
  <label for="name">Name</label>
  <input type="text" class="form-control" asp-for="Name" 
     id="AjaxValidationModelName" aria-describedby="nameHelp" 
     placeholder="Enter name">
  <small id="nameHelp" class="form-text text-muted">
    We'll never share your name ...
  </small>
  <span asp-validation-for="Name" class="text-danger"></span>
</div>

<div class="form-group">
  <label for="age">Age</label>
  <input type="number" class="form-control" 
    id="AjaxValidationModelAge" asp-for="Age" placeholder="0">
  <span asp-validation-for="Age" class="text-danger"></span>
</div>

<div class="form-check ten_px_bottom">
  <input type="checkbox" class="form-check-input big_checkbox"
      asp-for="IsCool" id="AjaxValidationModelIsCool">
  <label class="form-check-label ten_px_left" for="IsCool">IsCool</label>
  <span asp-validation-for="IsCool" class="text-danger"></span>
</div>

<button type="submit" class="btn btn-primary">Submit</button>

The ASP.NET Core MVC controller handles the requests from the view. The first Index method in the example below, just responds to a plain HTTP GET.

The second Index method accepts a POST request with the Anti-Forgery token which is sent with each request. When the result is successful, a partial view is returned. The model state must also be cleared, otherwise the validation messages will not be reset.

If the page returns the incorrect result, ie just the content of the partial view, then the request was not sent asynchronously, but as a full page request. You need to check, that the front end packages are included correctly.

public class AjaxTestController : Controller
{
  public IActionResult Index()
  {
    return View(new AjaxValidationModel());
  }

  [HttpPost]
  [ValidateAntiForgeryToken]
  public IActionResult Index(AjaxValidationModel model)
  {
    if (!ModelState.IsValid)
    {
      return PartialView("_partialAjaxForm", model);
    }

    // the client could validate this, but allowed for testing server errors
    if(model.Name.Length < 3)
    {
      ModelState.AddModelError("name", "Name should be longer than 2 chars");
      return PartialView("_partialAjaxForm", model);
    }

    ModelState.Clear();
    return PartialView("_partialAjaxForm");
  }
}

Complex AJAX Form request

In this example, a list of data items are returned to the view. Each item in the list will have a form to update its data, and also the data will be updated using a checkbox onchange event or the input text oninput event and not the submit button.

Because a list is used, the div element to be updated must have a unique id. This can be implemented by creating a new GUID with each item, and can be used then in the name of the div to be updated, and also the data-ajax-update parameter.

@using AspNetCoreBootstrap4Validation.ViewModels
@model AjaxValidationListModel
@{
    ViewData["Title"] = "Ajax Test Page";
}

<h4>Ajax Test</h4>

@foreach (var item in Model.Items)
{
    string guid = Guid.NewGuid().ToString();

    <form asp-action="Index" asp-controller="AjaxComplexList" 
          data-ajax="true" 
          data-ajax-method="POST"
          data-ajax-mode="replace" 
          data-ajax-update="#complex-ajax-@guid">

        <div id="complex-ajax-@guid">
            @await Html.PartialAsync("_partialComplexAjaxForm", item)
        </div>
    </form>
}


@section Scripts  {
    @await Html.PartialAsync("_ValidationScriptsPartial")
}

The form data will send the update with an onchange Javascript event from the checkbox. This could be required for example, when the UX designer wants instant updates, instead of an extra button click. To achieve this, the submit button is not displayed. A unique id is used to identify each button, and the onchange event from the checkbox triggers the submit event using this. Now the form request will be sent using Ajax like before.

@model AspNetCoreBootstrap4Validation.ViewModels.AjaxValidationModel
@{
    string guid = Guid.NewGuid().ToString();
}

<div asp-validation-summary="All" class="text-danger"></div>

<div class="form-group">
    <label for="name">Name</label>

    <input type="text" class="form-control" asp-for="Name" 
      id="AjaxValidationModelName" aria-describedby="nameHelp" placeholder="Enter name"
      oninput="$('#submit-@guid').trigger('submit');">

    <small id="nameHelp" class="form-text text-muted">We'll never share your name ...</small>
    <span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
    <label for="age">Age</label>

    <input type="number" asp-for="Age" 
      class="form-control" id="AjaxValidationModelAge" placeholder="0"
      oninput="$('#submit-@guid').trigger('submit');">

    <span asp-validation-for="Age" class="text-danger"></span>
</div>
<div class="form-check ten_px_bottom">

    @Html.CheckBox("IsCool", Model.IsCool,
        new { onchange = "$('#submit-" + @guid + "').trigger('submit');", @class = "big_checkbox" })

    <label class="form-check-label ten_px_left" >Check the checkbox to send a request</label>
</div>

<button style="display: none" id="submit-@guid" type="submit">Submit</button>

The ASP.NET Core controller returns the HTTP GET and POST like before.

using AspNetCoreBootstrap4Validation.ViewModels;

namespace AspNetCoreBootstrap4Validation.Controllers
{
    public class AjaxComplexListController : Controller
    {
        public IActionResult Index()
        {
            return View(new AjaxValidationListModel {
                Items = new List<AjaxValidationModel> {
                    new AjaxValidationModel(),
                    new AjaxValidationModel()
                }
            });
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Index(AjaxValidationModel model)
        {
            if (!ModelState.IsValid)
            {
                return PartialView("_partialComplexAjaxForm", model);
            }

            // the client could validate this, but allowed for testing server errors
            if(model.Name.Length < 3)
            {
                ModelState.AddModelError("name", "Name should be longer than 2 chars");
                return PartialView("_partialComplexAjaxForm", model);
            }

            ModelState.Clear();
            return PartialView("_partialComplexAjaxForm", model);
        }
    }
}

When the requests are sent, you can check this using the F12 developer tools in the browser using the network tab. The request type should be xhr.

Links

https://dotnetthoughts.net/jquery-unobtrusive-ajax-helpers-in-aspnet-core/

https://www.mikesdotnetting.com/article/326/using-unobtrusive-ajax-in-razor-pages

https://www.learnrazorpages.com/razor-pages/ajax/unobtrusive-ajax

https://damienbod.com/2018/07/08/updating-part-of-an-asp-net-core-mvc-view-which-uses-forms/

https://ml-software.ch/blog/extending-client-side-validation-with-fluentvalidation-and-jquery-unobtrusive-in-an-asp-net-core-application

https://ml-software.ch/blog/extending-client-side-validation-with-dataannotations-and-jquery-unobtrusive-in-an-asp-net-core-application

Advertisements

One comment

  1. […] ASP.NET Core MVC Ajax Form requests using jquery-unobtrusive (Damien Bowden) […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: