This article continues my series on how to enhance the user experience (UX) of your MVC applications, and how to make them faster. In the first article, entitled Enhance Your MVC Applications Using JavaScript and jQuery: Part 1, and the second article, entitled Enhance Your MVC Applications Using JavaScript and jQuery: Part 2, you learned about the starting MVC application, which was coded using all server-side C#. You then added JavaScript and jQuery to avoid post-backs and enhance the UX in various ways. If you haven't already read these articles, I highly recommend that you read them to learn about the application you're enhancing in this series of articles.

In this article, you're going to build Web API calls that you can call from the application to avoid post-backs. You're going to add calls to add, update, and delete shopping cart information. In addition, you're going to learn to work with dependent drop-down lists to also avoid post-backs. Finally, you learn to use jQuery auto-complete instead of a drop-down list to provide more flexibility to your user.

The Problem: Adding to Shopping Cart Requires a Post-Back

On the Shopping page, each time you click on an Add to Cart button (Figure 1), a post-back occurs and the entire page is refreshed. This takes time and causes a flash on the page that can be annoying to the users of your site. In addition, it takes time to perform this post-back because all the data must be retrieved from the database server, the entire page needs to be rebuilt on the server side, and then the browser must redraw the entire page. All of this leads to a poor user experience.

Figure 1: Adding an item to the shopping cart can be more efficiently handled using Ajax.
Figure 1: Adding an item to the shopping cart can be more efficiently handled using Ajax.

The Solution: Create a Web API Call

The first thing to do is to create a new Web API controller to handle the calls for the shopping cart functionality. Right mouse-click on the PaulsAutoParts project and create a new folder named ControllersApi. Right mouse-click on the ControllersApi folder and add a new class named ShoppingApiController.cs. Remove the default code in the file and add the code shown in Listing 1 to this new class file.

Listing 1: Create a new Web API controller with methods to eliminate post-backs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using PaulsAutoParts.AppClasses;
using PaulsAutoParts.Common;
using PaulsAutoParts.EntityLayer;
using PaulsAutoParts.ViewModelLayer;

namespace PaulsAutoParts.ControllersApi
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class ShoppingApiController : AppController
    {
        #region Constructor
        public ShoppingApiController(AppSession session,
        IRepository<Product, ProductSearch> repo,
        IRepository<VehicleType,
        VehicleTypeSearch> vrepo) : base(session)
        {
            _repo = repo;
            _vehicleRepo = vrepo;
        }
        #endregion

        #region Private Fields
        private readonly IRepository<Product, ProductSearch> _repo;
        private readonly IRepository<VehicleType, 
            VehicleTypeSearch> _vehicleRepo;
        #endregion

        #region AddToCart Method
        [HttpPost(Name = "AddToCart")]
        public IActionResult AddToCart([FromBody]int id)
        {
            // Set Cart from Session
            ShoppingViewModel vm = new(_repo, _vehicleRepo,
            UserSession.Cart);

            // Set "Common" View Model Properties from Session
            base.SetViewModelFromSession(vm, UserSession);

            // Add item to cart
            vm.AddToCart(id, UserSession.CustomerId.Value);

            // Set cart into session
            UserSession.Cart = vm.Cart;

            return StatusCode(StatusCodes.Status200OK, true);
        }
        #endregion
    }
}

Add two attributes before this class definition to tell .NET that this is a Web API controller and not an MVC page controller. The [ApiController] attribute enables some features such as attribute routing, automatic model validation, and a few other API-specific behaviors. When using the [ApiController] attribute, you must also add the [Route] attribute. The route attribute adds the prefix “api” to the default “[controller]/[action]” route used by your MVC page controllers. You can choose whatever prefix you wish, but the “api” prefix is a standard convention that most developers use.

In the constructor for this API controller, inject the AppSession, and the product and vehicle type repositories. Assign the product and vehicle type repositories to the corresponding private read-only fields defined in this class.

The AddToCart() method is what's called from jQuery Ajax to insert a product into the shopping cart that's stored in the Session object. This code is similar to the code written in the MVC controller class ShoppingController.Add() method. After adding the id passed in by Ajax, a status code of 200 is passed back from this Web API call to indicate that the product was successfully added to the shopping cart. At this point, you have everything you need on the back-end to add a product to the shopping cart via an Ajax call.

It's now time to modify the client-side code to take advantage of this new Web API method. You no longer want a post-back to occur when you click on the Add to Cart link, so you need to remove the asp- attributes and add code to make an Ajax call. Open the Views\Shopping\_ShoppingList.cshtml file and locate the Add to Cart <a> tag and remove the asp-action="Add" and the asp-route-id="@item.ProductId" attributes. Add id and data- attributes, and an onclick event, as shown in the code snippet below.

<a class="btn btn-info"
   id="updateCart"
   data-isadding="true"
   onclick="pageController.modifyCart(@item.ProductId, this)">
       Add to Cart
</a>

When you post back to the server, a variable in the view model class is set on each product to either display the Add to Cart link or the Remove from Cart link. When using client-side code, you're going to toggle the same link to either perform the add or the remove. Use the data-isadding attribute on the anchor tag to determine whether you're doing an add or a remove.

Add Code to Page Closure

The onclick event in the anchor tag calls a method on the pageController called modifyCart(). You pass to this cart the current product ID and a reference to the anchor tag itself. Add this modifyCart() method by opening the Views\Shopping\Index.cshtml file and adding the three private methods (Listing 2) to the pageController closure: modifyCart(), addToCart(), and removeFromCart(). The modifyCart() method is the one that's made public; the other two are called by the modifyCart() method.

Listing 2: Add three methods in the pageController closure to modify the shopping cart

function modifyCart(id, ctl) {
    // Are we adding or removing?
    if (Boolean($(ctl).data("isadding"))) {

        // Add product to cart
        addToCart(id);

        // Change the button
        $(ctl).text("Remove from Cart");
        $(ctl).data("isadding", false);
        $(ctl).removeClass("btn-info")
        .addClass("btn-danger");
    }
    else {
    // Remove product from cart
    removeFromCart(id);

    // Change the button
    $(ctl).text("Add to Cart");
    $(ctl).data("isadding", true);
    $(ctl).removeClass("btn-danger")
    .addClass("btn-info");
    }
}

function addToCart(id) {
}

function removeFromCart(id) {
}

The modifyCart() method checks the value in the data-isadding attribute to see if it's true or false. If it's true, call the addToCart() method, change the link text to “Remove from Cart”, set the data-isadding="false", remove the class “btn-info”, and add the class “btn-danger”. If false, call the removeFromCart() method and change the attributes on the link to the opposite of what you just set. Modify the return object to expose the modifyCart() method.

return {
    "setSearchArea": setSearchArea,
    "modifyCart": modifyCart
}

Create the addToCart() Method

Write the addToCart() method in the pageController closure to call the new AddToCart() method you added in the ShoppingApiController class. Because you're performing a post, you may use either the jQuery $.ajax() or $.post() methods. I chose to use the $.post() method in the code shown in following snippet.

function addToCart(id) {
    let settings = {
        url: "/api/ShoppingApi/AddToCart",
        contentType: "application/json",
        data: JSON.stringify(id)
    }
    $.post(settings)
    .done(function (data) {
        console.log("Product Added to Shopping Cart");
    })
    .fail(function (error) {
        console.error(error);
    });
}

Try It Out

Run the application and click on the Shop menu. Perform a search to display products on the Shopping page. Click on one of the Add to Cart links to add a product to the shopping cart. You should notice that the link changes to Remove from Cart immediately. Click on the “0 Items in Cart” link in the menu bar and you should see an item in the cart. Don't worry about the “0 Items in Cart” link; you'll fix that a little later in this article.

The Problem: Delete from Shopping Cart Requires a Post-Back

Now that you've added a product to the shopping cart using Ajax, it would be good to also remove an item from the cart using Ajax. The link on the product you just added to the cart should now be displaying Remove from Cart (Figure 2). This was set via the JavaScript you wrote in the addToCart() method. The data-isadding attribute has been set to a false value, so when you click on the link again, the code in the modifyCart() method calls the removeFromCart() method.

Figure 2: Removing items from a cart can be more efficiently handled using Ajax.
Figure 2: Removing items from a cart can be more efficiently handled using Ajax.

The Solution: Write Web API Method to Delete from the Shopping Cart

Open the ControllersApi\ShoppingApiController.cs file and add a new method named RemoveFromCart(), as shown in Listing 3. This method is similar to the Remove() method contained in the ShoppingController MVC class. A product ID is passed into this method and the RemoveFromCart() method is called on the view model to remove this product from the shopping cart help in the Session object. A status code of 200 is returned from this method to indicate that the product was successfully removed from the shopping cart.

Listing 3: The RemoveFromCart() method deletes a product from the shopping cart

[HttpDelete("{id}", Name = "RemoveFromCart")]
public IActionResult RemoveFromCart(int id)
{
    // Set Cart from Session
    ShoppingViewModel vm = new(_repo, 
        _vehicleRepo, UserSession.Cart);

    // Set "Common" View Model Properties from Session
    base.SetViewModelFromSession(vm, UserSession);

    // Remove item to cart
    vm.RemoveFromCart(vm.Cart, id, 
        UserSession.CustomerId.Value);

    // Set cart into session
    UserSession.Cart = vm.Cart;

    return StatusCode(StatusCodes.Status200OK, true);
}

You no longer want a post-back to occur when you click on the Remove from Cart link, so you need to remove the asp- attributes and add code to make an Ajax call. Open the Views\Shopping\_ShoppingList.cshtml file and locate the Remove from Cart <a> tag and remove the asp-action="Remove" and the asp-route-id="@item.ProductId" attributes. Add an id and data- attributes, and an onclick event, as shown in the code below.

<a class="btn btn-danger"
   id="updateCart"
   data-isadding="false"
   onclick="pageController.modifyCart(@item.ProductId, this)">
       Remove from Cart
</a>

Notice that you're setting the id attribute to the same value as on the Add to Cart button. As you know, you can't have two HTML elements with the id attribute set to the same value, because these two buttons are wrapped within an @if() statement, and only one is written by the server into the DOM at a time.

Add Code to pageController

Open the Views\Shopping\Index.cshtml file and add code to the removeFromCart() method. Call the $.ajax() method by setting the url property to the location of the RemoveFromCart() method you added, and set the type property to “DELETE”. Pass the id of the product to delete on the URL line, as shown in the following code snippet.

function removeFromCart(id) {
    $.ajax({
        url: "/api/ShoppingApi/RemoveFromCart/" + id,
        type: "DELETE"
    })
    .done(function (data) {
        console.log("Product Removed from Shopping Cart");
    })
    .fail(function (error) {
        console.error(error);
    });
}

Try It Out

Run the application and click on the Shop menu. Perform a search to display products on the Shopping page. Click on one of the Add to Cart links to add a product to the shopping cart. You should notice that the link changes to Remove from Cart immediately. Click on the “0 Items in Cart” link in the menu bar and you should see an item in the cart. Click on the back button on your browser and click the Remove from Cart link on the item you just added. Click on the “0 Items in Cart” link and you should see that there are no longer any items in the shopping cart.

After modifying the code in the previous section to add and remove items from the shopping cart using Ajax, you noticed that the “0 Items in Cart” link in the menu bar isn't updating with the current number of items in the cart. That's because this link is generated by data from the server side. Because you're bypassing server-side processing with Ajax calls, you need to update this link yourself.

Open the Views\Shared\_Layout.cshtml file and locate the “Items in Cart” link. Add an id attribute to the <a> tag and assign it the value of “itemsInCart”, as shown in the following code snippet.

<a id="itemsInCart"
   class="text-light"
   asp-action="Index"
   asp-controller="Cart">
        @ViewData["ItemsInCart"] Items in Cart
</a>

Create a new method to increment or decrement the “Items in Cart” link. Open the wwwroot\js\site.js file and add a new method named modifyItemsInCartText() to the mainController closure, as shown in Listing 4. An argument is passed to this method to specify whether you're adding or removing an item from the shopping cart. This tells the method to either increment the number or decrement the number of items in the text displayed on the menu.

Listing 4: Add a modifyItemsInCartText() method to the mainController closure

function modifyItemsInCartText(isAdding) {
    // Get text from <a> tag
    let value = $("#itemsInCart").text();
    let count = 0;
    let pos = 0;

    // Find the space in the text
    pos = value.indexOf(" ");

    // Get the total # of items
    count = parseInt(value.substring(0, pos));

    // Increment or Decrement the total # of items
    if (isAdding) {
        count++;
    }
    else {
        count--;
    }

    // Create the text with the new count
    value = count.toString() + " " + value.substring(pos);

    // Put text back into the cart
    $("#itemsInCart").text(value);
}

The modifyItemsInCartText() method extracts the text portion from the <a> tag holding the “0 Items in Cart”. It calculates the position of the first space in the text, which allows you to parse the numeric portion, turn that into an integer, and place it into the variable named count. If the value passed into the isAdding parameter is true, then count is incremented by one. If the value passed is false, then count is decremented by one. The new numeric value is then placed where the old numeric value was in the string and this new string is inserted back into the <a> tag. Expose the modifyItemsInCartText()method from the return object on the mainController closure, as shown in the following code.

return {
    "pleaseWait": pleaseWait,
    "disableAllClicks": disableAllClicks,
    "setSearchValues": setSearchValues,
    "isSearchFilledIn": isSearchFilledIn,
    "setSearchArea": setSearchArea,
    "modifyItemsInCartText": modifyItemsInCartText
}

Call this function after making the Ajax call to either add or remove an item from the cart. Open the Views\Shopping\Index.cshtml file and locate the done() method in the addToCart() method. Add the line shown just before the console.log() statement.

$.post(settings).done(function (data) {
    mainController.modifyItemsInCartText(true);
    console.log("Product Added to Shopping Cart");
})
// REST OF THE CODE HERE

Locate the done() method in the removeFromCart() method and add the line of code just before the console.log() statement, as shown in the following code snippet.

$.ajax({
    url: "/api/ShoppingApi/RemoveFromCart/" + id,
    type: "DELETE"
})
.done(function (data) {
    mainController.modifyItemsInCartText(false);
    console.log("Product Removed from Shopping Cart");
})
// REST OF THE CODE HERE

Try It Out

Run the application and click on the Shop menu. Perform a search to display products on the Shopping page. Click on one of the Add to Cart links to add a product to the shopping cart and notice the link changes to Remove from Cart immediately. You should also see the “Items in Cart” link increment. Click on the Remove from Cart link and you should see the “Items in Cart” link decrement.

The Problem: Dependent Drop-Downs Requires Multiple Post-Backs

A common user interface problem to solve is that when you choose an item from a drop-down, you then need a drop-down immediately following to be filled with information specific to that selected item. For example, run the application and select the Shop menu to get to the Shopping page. In the left-hand search area, select a Vehicle Year from the drop-down list (Figure 3). Notice that a post-back occurs and now a list of Vehicle Makes are filled into the corresponding drop-down. Once you choose a make, another post-back occurs and a list of vehicle models is filled into the last drop-down. Notice the flashing of the page that occurs each time you change the year or make caused by the post-back.

Figure 3: Multiple post-backs occur when you select a different value from any of these drop-downs.
Figure 3: Multiple post-backs occur when you select a different value from any of these drop-downs.

The Solution: Connect All Drop-Downs to Web API Services

To eliminate this flashing, create Web API calls to return makes and models. After selecting a year from the Vehicle Year drop-down, an Ajax call is made to retrieve all makes for that year in a JSON format. Use jQuery to build a new set of <option> objects for the Vehicle Make drop-down. The same process can be done for the Vehicle Model drop-down as well.

Open the ControllersApi\ShoppingApiController.cs file and add a new method named GetMakes() to get all makes of vehicles for a specific year, as shown in Listing 5. This method accepts the year of the vehicle to search for. The GetMakes() method on the ShoppingViewModel class is called to set the Makes property with the collection of vehicle makes that are valid for that year. The set of vehicle makes is returned from this Web API method.

Listing 5: The GetMakes() method returns all vehicles makes for a specific year

[HttpGet("{year}", Name = "GetMakes")]
public IActionResult GetMakes(int year)
{
    IActionResult ret;

    // Create view model
    ShoppingViewModel vm = new(_repo,
        _vehicleRepo, UserSession.Cart);

    // Get vehicle makes for the year
    vm.GetMakes(year);

    // Return all Makes
    ret = StatusCode(StatusCodes.Status200OK, vm.Makes);

    return ret;
}

Next, add another new method named GetModels() to the ShoppingApiController class to retrieve all models for a specific year and make as shown in Listing 6. In this method, both a year and a vehicle make are passed in. The GetModels() method on the ShoppingViewModel class is called to populate the Models property with all vehicle models for that specific year and make. The collection of vehicle models is returned from this Web API method.

Listing 6: The GetModels() method returns all vehicle models for a specific year and model

[HttpGet("{year}/{make}", Name = "GetModels")]
public IActionResult GetModels(int year, string make)
{
    IActionResult ret;

    // Create view model
    ShoppingViewModel vm = new(_repo,
        _vehicleRepo, UserSession.Cart);

    // Get vehicle models for the year/make
    vm.GetModels(year, make);

    // Return all Models
    ret = StatusCode(StatusCodes.Status200OK, vm.Models);

    return ret;
}

Modify Shopping Cart Page

It's now time to add a couple of methods to your shopping cart page to call these new Web API methods you added to the ShoppingApiController class. Open the Views\Shopping\Index.cshtml file and add a method to the pageController named getMakes(), as shown in Listing 7.

Listing 7: The getMakes() method retrieves vehicle makes and builds a drop-down

function getMakes(ctl) {
    // Get year selected
    let year = $(ctl).val();

    // Search for element just one time
    let elem = $("#SearchEntity_Make");

    // Clear makes drop-down
    elem.empty();

    // Clear models drop-down
    $("#SearchEntity_Model").empty();

    $.get("/api/ShoppingApi/GetMakes/" +
        year, function (data) {

        // Load the makes into drop-down
        $(data).each(function () {
            elem.append(`<option>${this}</option>`);
        });
    })
    .fail(function (error) {
        console.error(error);
    });
}

The getMakes() method retrieves the year selected by the user. It then clears the drop-down that holds all vehicle makes and the one that holds all vehicle models. Next, a call is made to the GetMakes() Web API method using the $.get() shorthand method. If the call is successful, use the jQuery each() method on the data returned to iterate over the collection of vehicle makes returned. For each make, build an <option> element with the vehicle make within the <option> and append that to the drop-down.

Add another method to the pageController named getModels(), as shown in Listing 8. The getModels() method retrieves both the year and make selected by the user. Clear the models drop-down list in preparation for loading the new list. Call the GetModels() method using the $.get() shorthand method. If the call is successful, use the jQuery each() method on the data returned to iterate over the collection of vehicle models returned. For each model, build an <option> element with the vehicle model within the <option> and append that to the drop-down.

Listing 8: The getModels() method retrieves vehicle models and builds a drop-down

function getModels(ctl) {
    // Get currently selected year
    let year = $("#SearchEntity_Year").val();

    // Get model selected
    let model = $(ctl).val();

    // Search for element just one time
    let elem = $("#SearchEntity_Model");

    // Clear models drop-down
    elem.empty();

    $.get("/api/ShoppingApi/GetModels/" +
        year + "/" + model, function (data) {

        // Load the makes into drop-down
        $(data).each(function () {
            elem.append(`<option>${this}</option>`);
        });
    })
    .fail(function (error) {
        console.error(error);
    });
}

Because you added two new private methods to the pageController closure, you need to expose these two methods by modifying the return object, as shown in the following code snippet.

return {
    "setSearchArea": setSearchArea,
    "modifyCart": modifyCart,
    "getMakes": getMakes,
    "getModels": getModels
}

Now that you have the new methods written and exposed from your pageController closure, hook them up to the appropriate onchange events of the drop-downs for the year and make within the search area on the page. Locate the <select> element for the SearchEntity.Year property and modify the onchange event to look like the following code snippet.

<select class="form-control"
        onchange="pageController.getMakes(this);"
        asp-for="SearchEntity.Year"
        asp-items="@(new SelectList(Model.Years))">
</select>

Next, locate the <select> element for the SearchEntity.Make property and modify the onchange event to look like the following code snippet.

<select class="form-control"
        onchange="pageController.getModels(this);"
        asp-for="SearchEntity.Make"
        asp-items="@(new SelectList(Model.Makes))">
</select>

Try It Out

Run the application and click on the Shop menu. Expand the “Search by Year/Make/Model” search area and select a year from the drop-down. The vehicle makes are now filled into the drop-down, but the page didn't flash because there's no longer a post-back. If you select a vehicle make from the drop-down, you should see the vehicle models filled in, but again, the page didn't flash because there was no post-back.

The Problem: Allow a User to Either Select an Existing Category or Add a New One

Click on the Admin > Products menu, then click the Add button to allow you to enter a new product (Figure 4). Notice that the Category field is a text box. This is fine if you want to add a new Category, but what if you want the user to be able to select from the existing categories already assigned to products? You could switch this to a drop-down list, but then the user could only select an existing category and wouldn't be able to add a new one on the fly. What would be ideal is to use a text box, but also have a drop-down component that shows them the existing categories as they type in a few letters into the text box.

Figure 4: jQuery has validation capabilities as well as an auto-complete that can make your UI more responsive and avoid post-backs
Figure 4: jQuery has validation capabilities as well as an auto-complete that can make your UI more responsive and avoid post-backs

The Solution: Use jQuery UI Auto-Complete

To solve this problem, you need to bring in the jQuery UI library and use the auto-complete functionality. Once added to your project, connect a jQuery auto-complete to the Category text box so after the user starts to type, a list of existing categories can be displayed directly under the text box.

Modify the Product Repository Class

First, you need to make some changes to the back-end to support searching for categories by finding where a category starts with the text the user types in. Open the RepositoryClasses\ProductRepository.cs file in the PaulsAutoParts.DataLayer project and add a new method named SearchCategories() to this class. This method takes the characters entered by the user and queries the database to retrieve only those categories that start with those characters, as shown in the following code snippet.

public List<string> SearchCategories(string searchValue)
{
    return _DbContext.Products
                     .Select(p => p.Category).Distinct()
                     .Where(p => p.StartsWith(searchValue))
                     .OrderBy(p => p).ToList();
}

This LINQ query roughly translates to the following SQL query.

SELECT DISTINCT Category
FROM Product
WHERE Category LIKE 'C%'

Modify the Shopping View Model Class

Instead of calling the repository methods directly from controller classes, it's best to let your view model class call these methods. Open the ShoppingViewModel.cs file in the PaulsAutoParts.ViewModeLayer project. Add a new method to this class named SearchCategories() that makes the call to the repository class method you just created.

public List<string> SearchCategories(string searchValue)
{
    return ((ProductRepository)Repository).SearchCategories(searchValue);
}

Add New Web API to Controller

It's now time to create the Web API method for you to call via Ajax to search for the categories based on each character the user types into the text box. Open the ControllersApi\ShoppingApiController.cs file and add a new method named SearchCategories() to this controller class, as shown in Listing 9. This method accepts the character(s) typed into the Category text box; if it's blank, it returns all categories, and otherwise passes the search value to the SearchCategories() method you just created.

Listing 9: The SearchCategories() method performs a search for categories based on user input

[HttpGet("{searchValue}", Name = "SearchCategories")]
public IActionResult SearchCategories(
    string searchValue)
{
    IActionResult ret;

    ShoppingViewModel vm = new(_repo,
        _vehicleRepo, UserSession.Cart);

    if (string.IsNullOrEmpty(searchValue)) {
        // Get all product categories
        vm.GetCategories();

        // Return all categories
        ret = StatusCode(StatusCodes.Status200OK, vm.Categories);
    }
    else {
    // Search for categories
    ret = StatusCode(StatusCodes.Status200OK,
        vm.SearchCategories(searchValue));
    }

    return ret;
}

Add jQuery UI Code to Product Page

Open the Views\Product\ProductIndex.cshtml file and at the top of the page, just below the setting of the page title, add a new section to include the jquery-ui.css file. This is needed for styling the auto-complete drop-down. Please note that for the limits of showing this article on a fixed-width page/screen, I had to break the href attribute on to two lines. When you put this into your cshtml file, be sure to put it all on one line.

@section HeadStyles {
<link rel="stylesheet"
      href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css";>
}

Now go to the bottom of the file and in the @section Scripts and just before your opening <script> tag, add the following <script> tag to include the jQuery UI JavaScript file.

<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js";>
</script>

Add a new method to the pageController closure named categoryAutoComplete(). The categoryAutoComplete() method is the publicly exposed method that's called from the $(document).ready() to hook up the auto-complete to the category text box using the autocomplete() method. Pass in an object to the autocomplete() method to set the source property to a method named searchCategories(), which is called to retrieve the category data to display in the drop-down under the text box. The minLength property is set to the minimum number of characters that must be typed prior to making the first call to the searchCategories() method. I've set it to one, so the user must type in at least one character in order to have the searchCategories() method called.

function categoryAutoComplete() {
    // Hook up Category auto-complete
    $("#SelectedEntity_Category").autocomplete({
        source: searchCategories, minLength: 1
    });
}

Add the searchCategories() method that's called from the source property. This method must accept a request object and a response callback function. This method uses the $.get() method to make the Web API call to the SearchCategories() method passing in the request.term property, which is the text the user entered into the category text box. If the call is successful, the data retrieved back from the Ajax call is sent back via the response callback function.

function searchCategories(request, response) {
    $.get("/api/ShoppingApi/SearchCategories/" +
        request.term, function (data) {
    response(data);
    })
    .fail(function (error) {
        console.error(error);
    });
}

Modify the return object to expose the categoryAutoComplete() method.

return {
    "setSearchValues": setSearchValues,
    "setSearchArea": mainController.setSearchArea,
    "isSearchFilledIn": mainController.isSearchFilledIn,
    "categoryAutoComplete": categoryAutoComplete
}

Finally, modify the $(document).ready() function to call the pageController.categoryAutoComplete() method to hook up jQuery UI to the Category text box.

$(document).ready(function () {
    // Setup the form submit
    mainController.formSubmit();

    // Hook up category auto-complete
    pageController.categoryAutoComplete();

    // Collapse search area or not?
    pageController.setSearchValues();

    // Initialize search area on this page
    pageController.setSearchArea();
});

Try It Out

Run the application and select the Admin > Products menu. Click on the Add button and click into the Category text box. Type the letter T in the Category input field and you should see a drop-down appear of categories that start with the letter T.

Vehicle Type Page Needs Auto-Complete for Vehicle Make Text Box

There's another page in the Web application that can benefit from the jQuery UI auto-complete functionality. On the Vehicle Type maintenance page, when the user wants to add a new vehicle, they should be able to either add a new make or select from an existing one.

Add New Method to Vehicle Type Repository

Let's start with modifying the code on the server to support searching for vehicle makes. Open the RepositoryClasses\VehicleTypeRepository.cs file and add a new method named SearchMakes(), as shown in the following code snippet.

public List<string> SearchMakes(string make)
{
    return _DbContext.VehicleTypes
                     .Select(v => v.Make).Distinct()
                     .Where(v => v.StartsWith(make))
                     .OrderBy(v => v).ToList();
}

This LINQ query roughly translates to the following SQL query:

SELECT DISTINCT Make
FROM Lookup.VehicleType
WHERE Make LIKE 'C%'

Add a New Method to Vehicle Type View Model Class

Instead of calling the repository methods directly from controller classes, it's best to let your view model class call these methods. Open the VehicleTypeViewModel.cs file in the PaulsAutoParts.ViewModeLayer project. Add a new method to this class named SearchMakes() that makes the call to the repository class method you just created.

public List<string> SearchMakes(string searchValue)
{
    return ((VehicleTypeRepository)Repository)
        .SearchMakes(searchValue);
}

Create New API Controller Class

Add a new class under the ControllersApi folder named VehicleTypeApiController.cs. Replace all the code within the new file with the code shown in Listing 10. Most of this code is boiler-plate for a Web API controller. The important piece is the SearchMakes() method that's going to be called from jQuery Ajax to perform the auto-complete.

Listing 10: Create a new Web API controller for handling vehicle type maintenance

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using PaulsAutoParts.AppClasses;
using PaulsAutoParts.Common;
using PaulsAutoParts.DataLayer;
using PaulsAutoParts.EntityLayer;
using PaulsAutoParts.ViewModelLayer;

namespace PaulsAutoParts.ControllersApi
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class VehicleTypeApiController : AppController
    {
        #region Constructor
        public VehicleTypeApiController(AppSession session,
            IRepository<VehicleType, 
                VehicleTypeSearch> repo): base(session)
        {
            _repo = repo;
        }
        #endregion

        #region Private Fields
        private readonly IRepository<VehicleType,
            VehicleTypeSearch> _repo;
        #endregion

        #region SearchMakes Method
        [HttpGet("{make}", Name = "SearchMakes")]
        public IActionResult SearchMakes(string make)
        {
            IActionResult ret;

            VehicleTypeViewModel vm = new(_repo);

            // Return all makes found
            ret = StatusCode(StatusCodes.Status200OK,
            vm.SearchMakes(make));

            return ret;
        }
        #endregion
    }
}

Add jQuery UI Code to Vehicle Type Page

Open the Views\VehicleType\VehicleTypeIndex.cshtml file and, at the top of the page, just below the setting of the page title, add a new section to include the jquery-ui.css file. This is needed for styling the auto-complete drop-down.

@section HeadStyles {
    <link rel="stylesheet"
          href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css";>
}

Now go to the bottom of the file and in the @section Scripts and just before your opening <script> tag, add the following <script> tag to include the jQuery UI JavaScript file.

<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js";>
</script>

Add a new method to the pageController closure named makesAutoComplete(). The makesAutoComplete() method is the publicly exposed method that is called from the $(document).ready() to hook up the jQuery auto-complete to the vehicle makes text box, just like you did for hooking up the category text box. Pass in an object to this method that sets the source property to a method named searchMake(), which is called to retrieve the vehicle makes to display in the drop-down under the text box. The minLength property is set to the minimum number of characters that must be typed prior to making the first call to the searchMakes() method. I've set it to one, so the user must type in at least one character in order to have the searchMakes() method called.

function makesAutoComplete() {
    // Hook up Makes auto-complete
    $("#SelectedEntity_Make").autocomplete({
        source: searchMakes, minLength: 1
    });
}

Add the searchMakes() method that is called from the source property. This method must accept a request object and a response callback function. This method uses the $.get() method to make the Web API call to the SearchMakes() method passing in the request.term property, which is the text the user entered into the vehicle makes text box. If the call is successful, the data retrieved back from the Ajax call is sent back via the response callback function.

function searchMakes(request, response) {
    $.get("/api/VehicleTypeApi/SearchMakes/" +
        request.term, function (data) {
        response(data);
    })
    .fail(function (error) {
        console.error(error);
    });
}

Modify the return object to expose the makesAutoComplete() method.

return {
    "setSearchValues": setSearchValues,
    "setSearchArea": mainController.setSearchArea,
    "isSearchFilledIn": mainController.isSearchFilledIn,
    "addValidationRules": addValidationRules,
    "makesAutoComplete": makesAutoComplete
}

Finally, modify the $(document).ready() function to call the pageController.makesAutoComplete() method to hook up jQuery UI to the vehicle makes text box.

$(document).ready(function () {
    // Add jQuery validation rules
    pageController.addValidationRules();

    // Hook up makes auto-complete
    pageController.makesAutoComplete();

    // Setup the form submit
    mainController.formSubmit();

    // Collapse search area or not?
    pageController.setSearchValues();

    // Initialize search area on this page
    pageController.setSearchArea();
});

Try It Out

Run the application and select the Admin > Vehicle Types menu. Click on the Add button and click into the Makes text box. Type the letter C and you should see a drop-down appear of makes that start with the letter C.

Search by Multiple Fields in Auto-Complete

Technically, in the last sample, you should also pass the year that the user input to the vehicle makes auto-complete. However, just to keep things simple, I wanted to just pass in a single item. Let's now hook up the auto-complete for the vehicle model input. In this one, you're going to pass in the vehicle year, make, and the letter typed into the vehicle model text box to a Web API call from the auto-complete method.

Add a New Method to the Vehicle Type Repository

Open the RepositoryClasses\VehicleTypeRepository.cs file and add a new method named SearchModels(), as shown in the following code snippet. This method makes the call to the SQL Server to retrieve all distinct vehicle models for the specified year, make, and the first letter or two of the model passed in.

public List<string> SearchModels(int year, string make, string model)
{
    return _DbContext.VehicleTypes
    .Where(v => v.Year == year &&
    v.Make == make &&
    v.Model.StartsWith(model))
    .Select(v => v.Model).Distinct()
    .OrderBy(v => v).ToList();
}

Add a New Method to the Vehicle Type View Model Class

Instead of calling repository methods directly from controller classes, it's best to let your view model class call these methods. Open the VehicleTypeViewModel.cs file in the PaulsAutoParts.ViewModeLayer project. Add a new method to this class named SearchModels() that makes the call to the repository class method you just created.

public List<string> SearchModels(
    int year, string make, string searchValue)
{
    return ((VehicleTypeRepository)Repository)
        .SearchModels(year, make, searchValue);
}

Modify API Controller

Open the ControllersApi\VehicleTypeApiController.cs file and add a new method named SearchModels() that can be called from the client-side code. This method is passed the year, make, and model to search for. It initializes the VehicleTypeViewModel class and makes the call to the SearchModels() method to retrieve the list of models that match the criteria passed to this method.

[HttpGet("{year}/{make}/{model}", Name = "SearchModels")]
public IActionResult SearchModels(int year, string make, string model)
{
    IActionResult ret;
    VehicleTypeViewModel vm = new(_repo);

    // Return all models found
    ret = StatusCode(StatusCodes.Status200OK,
        vm.SearchModels(year, make, model));
    return ret;
}

Modify the Page Controller Closure

Open the Views\VehicleType\VehicleTypeIndex.cshtml file. Add a new method to the pageController closure named modelsAutoComplete(). The modelsAutoComplete() method is the publicly exposed method called from the $(document).ready() to hook up the auto-complete to the models text box using the autocomplete() method. Pass in an object to this method to set the source property to the function to call to get the data to display in the drop-down under the text box. The minLength property is set to the minimum number of characters that must be typed prior to making the first call to the searchModels() function.

function modelsAutoComplete() {
    // Hook up Models AutoComplete
    $("#SelectedEntity_Model").autocomplete({
        source: searchModels, minLength: 1});
}

Add the searchModels() method (Listing 11) that's called from the source property. This method must accept a request object and a response callback function. This method uses the $.get() method to make the Web API call to the SearchModels() method passing in the year, make, and the request.term property, which is the text the user entered into the vehicle model text box. If the call is successful, the data retrieved back from the Ajax call is sent back via the response callback function.

Listing 11: The searchModels() method passes three values to the SearchModels() Web API

function searchModels(request, response) {
    let year = $("#SelectedEntity_Year").val();
    let make = $("#SelectedEntity_Make").val();

    if (make) {
        $.get("/api/VehicleTypeApi/SearchModels/" + year + "/" +
        make + "/" + request.term, function (data) {
            response(data);
        })
        .fail(function (error) {
            console.error(error);
        });
    }
    else {
        searchModels(request, response);
    }
}

Modify the return object to expose the modelsAutoComplete() method from the closure.

return {
    "setSearchValues": setSearchValues,
    "setSearchArea": mainController.setSearchArea,
    "isSearchFilledIn": mainController.isSearchFilledIn,
    "addValidationRules": addValidationRules,
    "makesAutoComplete": makesAutoComplete,
    "modelsAutoComplete": modelsAutoComplete
}

Modify the $(document).ready() function to make the call to the pageController.modelsAutoComplete() method, as shown in Listing 12.

Listing 12: Hook up the auto-complete functionality by calling the appropriate method in the pageController closure

$(document).ready(function () {
    // Add jQuery validation rules
    pageController.addValidationRules();

    // Hook up makes auto-complete
    pageController.makesAutoComplete();

    // Hook up models auto-complete
    pageController.modelsAutoComplete();

    // Setup the form submit
    mainController.formSubmit();

    // Collapse search area or not?
    pageController.setSearchValues();

    // Initialize search area on this page
    pageController.setSearchArea();
});

Try It Out

Run the application and click on the Admin > Vehicle Types menu. Click on the Add button and put the value 2000 into the Year text box. Type/Select the value Chevrolet in the Makes text box. Type the letter C in the Models text box and you should see a few models appear.

Summary

In this article, you once again added some functionality to improve the user experience of your website. Calling Web API methods from jQuery Ajax can greatly speed up the performance of your Web pages. Instead of having to perform a complete post-back and redraw the entire Web page, you can retrieve a small amount of data and update just a small portion of the Web page. Eliminating post-backs is probably one of the best ways to improve the user experience of your Web pages. Another technique you learned in this article was to take advantage of the jQuery UI auto-complete functionality.