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.
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.
Modify the Add to Cart Link
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.
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);
}
Modify the Remove from Cart Link
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.
The Problem: The “n Items in Cart” Link isn't Updated
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.
The Solution: Add Client-Side Code to Update Link
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.
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.
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.