Cascading Dropdown List in ASP.NET Core MVC

How to Implement Cascading Dropdown List in ASP.NET Core MVC

In this article, I will explain How to Implement a Cascading Dropdown List in ASP.NET Core MVC using jQuery Ajax with an Example. Please read our previous article discussing How to Perform CRUD Operation on a Single Page in ASP.NET Core MVC using jQuery Ajax.

What is a Cascading Drop-Down List?

Cascading drop-down lists (also known as dependent drop-down lists) are a user interface pattern in which the options in one drop-down are dynamically filtered or populated based on the user’s selection from a previous drop-down. A classic example is selecting a Country, which then limits the choices of States, which in turn limits the options of Cities. The following are some of the real-time use cases of Cascading drop-downs.

What is a Cascading Drop-Down List?

How Do Cascading Drop-Down Lists Work in Web Applications?
  • Initial Selection: The user makes a selection in the first dropdown (e.g., Country).
  • Filtering/Populating Next List: Immediately after that selection, the application fetches and displays only the relevant options in the second dropdown (e.g., the States within that selected Country).
  • Further Dependency: If there is a third dropdown (like City), it is, in turn, limited by the selected State, and so on.

Under the hood, there is typically an AJAX call that retrieves the filtered data from the server. This ensures the user only sees valid, context-specific options at each step.

Cascading Dropdown List in ASP.NET Core MVC

Let us create an Employee Registration Application using cascading dropdowns for Country, State, and City in ASP.NET Core MVC using jQuery AJAX. We will develop the following pages for our application.

  • The Employee List Page is where you see all employees and choose to either create new ones or edit existing ones.
  • The Employee Registration Page (Create) is for adding brand-new employees to the system.
  • The Employee Edit Page is where you can modify the fields of an already registered employee (e.g., update their department or location).
Employee List Page:

This Page displays all the existing employees in a tabular format and the option to add a new employee. Upon loading, this page retrieves all employees (including location info) from the database and shows them in a tabular format. The Edit link directs us to a page to update the selected employee’s details.

How to Implement Cascading Dropdown List in ASP.NET Core MVC

Employee Registration Page:

This page provides a form to register (create) a new employee record. When you select a Country, the page automatically fetches the corresponding States from the server (via AJAX) and populates the State dropdown. Once you pick a State, the City dropdown is similarly updated. Clicking the Register button will post the form data to the server, which adds a new record to the database if validation succeeds. If any validation errors occur, they are displayed next to the respective fields.

How to Implement a Cascading Dropdown List in ASP.NET Core MVC using jQuery Ajax with an Example

Employee Edit Page:

This Page allows users to modify the details of an existing employee. The fields are pre-filled with the employee’s current information from the database. Changing Country or State dynamically modifies the dropdown options for State and City, respectively (via AJAX). Clicking the Save Changes button updates the existing employee record in the database and redirects back to the Employee List Page on success.

How to Implement a Cascading Dropdown List in ASP.NET Core MVC using jQuery Ajax

Set Up the ASP.NET Core MVC Project

First, create a new ASP.NET Core MVC project named CascadingDemo and add the required packages. As we are going to interact with the SQL Server database using the Entity Framework Core Code First Approach, we need the packages from the NuGet Package Manager:

  • EF Core DB Provider: Microsoft.EntityFrameworkCore.SqlServer
  • EF Core Tools: Microsoft.EntityFrameworkCore.Tools

Please execute the following commands in the Visual Studio Package Manager console to install the EF Core Packages.

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
Creating the Models

Models represent the entities or domain objects in our application. They typically reflect how data is structured in the database. Please ensure the Models folder is in the project root directory, where we will create all our models.

Country Model:

Create a class file named Country.cs within the Models folder and then copy and paste the following code. The Country model represents a country record in the database. It contains an ICollection<State> navigation property representing the 1-to-many relationship with the State.

namespace CascadingDemo.Models
{
    //Represents the master data for countries.
    public class Country
    {
        public int CountryId { get; set; }
        public string CountryName { get; set; }
        // Navigation property for States
        public ICollection<State> States { get; set; }
    }
}
State Model:

Create a class file named State.cs within the Models folder and then copy and paste the following code. The State model represents a state record in the database. It has a CountryId, which indicates which country it belongs to. It also contains an ICollection<City> navigation property representing the 1-to-many relationship with the City.

namespace CascadingDemo.Models
{
    // Represents states which are linked to a Country via CountryId.
    public class State
    {
        public int StateId { get; set; }
        public string StateName { get; set; }
        public int CountryId { get; set; }
        // Navigation properties
        public Country Country { get; set; }
        public ICollection<City> Cities { get; set; }
    }
}
City Model:

Create a class file named City.cs within the Models folder and then copy and paste the following code. The City model represents a city record in the database. It has a StateId which indicates which state it belongs to.

namespace CascadingDemo.Models
{
    //Represents cities which are linked to a State via StateId.
    public class City
    {
        public int CityId { get; set; }
        public string CityName { get; set; }
        public int StateId { get; set; }
        // Navigation property
        public State State { get; set; }
    }
}
Employee Model

Create a class file named Employee.cs within the Models folder, and then copy and paste the following code. The Employee model represents an employee record in the database. It contains basic information (Name, Email, Phone, Department) and location info (CountryId, StateId, CityId), which links it to the Country, State, and City tables.

using System.ComponentModel.DataAnnotations.Schema;
namespace CascadingDemo.Models
{
    public class Employee
    {
        public int EmployeeId { get; set; }
        public string FullName { get; set; }
        public string Email { get; set; }
        public string Phone { get; set; }
        public string Department { get; set; }
        public int CountryId { get; set; }
        public int StateId { get; set; }
        public int CityId { get; set; }

        [ForeignKey("CountryId")]
        public Country Country { get; set; }

        [ForeignKey("StateId")]
        public State State { get; set; }

        [ForeignKey("CityId")]
        public City City { get; set; }
    }
}
Creating the DbContext with Seed Data

The DbContext class inherits from DbContext and serves as the bridge between our application and the database. It:

  • Defines DbSet properties for each model (Countries, States, Cities, Employees) so that Entity Framework Core can manage these entities.
  • Uses the OnModelCreating method to configure relationships between entities (for example, restricting cascade deletes between employees and location data) and to seed initial data into the master tables (Countries, States, and Cities).

First, create a folder named Data in the project root directory. Then, create a class file named EmployeeDBContext within the Data folder and copy and paste the following code.

using CascadingDemo.Models;
using Microsoft.EntityFrameworkCore;

namespace CascadingDemo.Data
{
    public class EmployeeDBContext : DbContext
    {
        public EmployeeDBContext(DbContextOptions<EmployeeDBContext> options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Configure Employee->Country relationship to restrict delete
            modelBuilder.Entity<Employee>()
                .HasOne(e => e.Country)
                .WithMany() // or .WithMany(c => c.Employees) if you have that navigation property
                .HasForeignKey(e => e.CountryId)
                .OnDelete(DeleteBehavior.Restrict); // Prevent cascade delete

            // Configure Employee->State relationship to restrict delete
            modelBuilder.Entity<Employee>()
                .HasOne(e => e.State)
                .WithMany()
                .HasForeignKey(e => e.StateId)
                .OnDelete(DeleteBehavior.Restrict);

            // Configure Employee->City relationship to restrict delete
            modelBuilder.Entity<Employee>()
                .HasOne(e => e.City)
                .WithMany()
                .HasForeignKey(e => e.CityId)
                .OnDelete(DeleteBehavior.Restrict);

            // Seed Countries: India, USA, UK
            modelBuilder.Entity<Country>().HasData(
                new Country { CountryId = 1, CountryName = "India" },
                new Country { CountryId = 2, CountryName = "USA" },
                new Country { CountryId = 3, CountryName = "UK" }
            );

            // Seed States with more initial data
            modelBuilder.Entity<State>().HasData(
                // India
                new State { StateId = 1, StateName = "Maharashtra", CountryId = 1 },
                new State { StateId = 2, StateName = "Gujarat", CountryId = 1 },
                new State { StateId = 3, StateName = "Delhi", CountryId = 1 },
                new State { StateId = 4, StateName = "Karnataka", CountryId = 1 },
                // USA
                new State { StateId = 5, StateName = "California", CountryId = 2 },
                new State { StateId = 6, StateName = "Texas", CountryId = 2 },
                new State { StateId = 7, StateName = "New York", CountryId = 2 },
                // UK
                new State { StateId = 8, StateName = "England", CountryId = 3 },
                new State { StateId = 9, StateName = "Scotland", CountryId = 3 }
            );

            // Seed Cities with more initial data
            modelBuilder.Entity<City>().HasData(
                // Maharashtra
                new City { CityId = 1, CityName = "Mumbai", StateId = 1 },
                new City { CityId = 2, CityName = "Pune", StateId = 1 },
                // Gujarat
                new City { CityId = 3, CityName = "Ahmedabad", StateId = 2 },
                new City { CityId = 4, CityName = "Surat", StateId = 2 },
                // Delhi
                new City { CityId = 5, CityName = "New Delhi", StateId = 3 },
                // Karnataka
                new City { CityId = 6, CityName = "Bangalore", StateId = 4 },
                new City { CityId = 7, CityName = "Mysore", StateId = 4 },
                // California
                new City { CityId = 8, CityName = "Los Angeles", StateId = 5 },
                new City { CityId = 9, CityName = "San Francisco", StateId = 5 },
                // Texas
                new City { CityId = 10, CityName = "Houston", StateId = 6 },
                new City { CityId = 11, CityName = "Dallas", StateId = 6 },
                // New York
                new City { CityId = 12, CityName = "New York City", StateId = 7 },
                new City { CityId = 13, CityName = "Buffalo", StateId = 7 },
                // England
                new City { CityId = 14, CityName = "London", StateId = 8 },
                new City { CityId = 15, CityName = "Manchester", StateId = 8 },
                // Scotland
                new City { CityId = 16, CityName = "Edinburgh", StateId = 9 },
                new City { CityId = 17, CityName = "Glasgow", StateId = 9 }
            );
        }

        // DbSets for all models
        public DbSet<Country> Countries { get; set; }
        public DbSet<State> States { get; set; }
        public DbSet<City> Cities { get; set; }
        public DbSet<Employee> Employees { get; set; }
    }
}
Modifying appsettings.json file:

Please modify the appsettings.json file as follows. This file holds configuration data such as the connection string (used to connect to the SQL Server database) and logging settings. The connection string under “ConnectionStrings” tells Entity Framework Core where and how to connect to the database.

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=EmployeesDB;Trusted_Connection=True;TrustServerCertificate=True;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
Modify the Program Class:

The Program class is the entry point of the ASP.NET Core application. It:

  • Configures essential services, including MVC (Controllers with Views) and the Entity Framework Core DbContext, using the connection string from the configuration.
  • Sets up the middleware pipeline (e.g., HTTPS redirection, static files, routing, and authorization).
  • Defines the default route, which by default directs to the Employees controller’s Index action.

Please modify the Program class as follows. We have registered the required services and middleware components to the request processing pipeline.

using CascadingDemo.Data;
using Microsoft.EntityFrameworkCore;

namespace CascadingDemo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddControllersWithViews();

            // This registers the EFCodeDBContext with the SQL Server connection read from appsettings.json
            builder.Services.AddDbContext<EmployeeDBContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
            );

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Employees}/{action=Index}/{id?}");

            app.Run();
        }
    }
}
Generate the Migration and Update the Database:

Open the Package Manager Console and execute the Add-Migration and Update-Database commands, as shown in the image below.

Cascading Dropdown List in ASP.NET Core MVC using jQuery Ajax

This should generate the EmployeesDB with the Employees, Countries, States, and Cities database tables in the SQL Server database, as shown in the below image:

Cascading Dropdown List in ASP.NET Core MVC

View Models

In an ASP.NET Core MVC Application, View Models shape or transform data specifically for the views. They can include validation attributes, select lists, or combine data from multiple entities to supply exactly what the view needs. First, create a folder named ViewModels in the project root directory, where we will create all our View Models.

Employee Create View Model

Create a class file named EmployeeCreateViewModel.cs within the ViewModels folder, and then copy and paste the following code. This view model is designed for the employee registration (create) page. It contains only the properties needed for creating an employee and validation attributes. It also includes a collection of countries (as a dropdown list) so the user can select a country. The States and Cities are loaded via AJAX based on the selected country.

using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace CascadingDemo.ViewModels
{
    public class EmployeeCreateViewModel
    {
        [Display(Name = "Full Name")]
        [Required(ErrorMessage = "Full Name is required.")]
        [StringLength(100, ErrorMessage = "Full Name cannot exceed 100 characters.")]
        public string FullName { get; set; }

        [Required(ErrorMessage = "Email is required.")]
        [EmailAddress(ErrorMessage = "Invalid Email Address.")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Phone is required.")]
        [Phone(ErrorMessage = "Invalid Phone number.")]
        public string Phone { get; set; }

        [Required(ErrorMessage = "Department is required.")]
        public string Department { get; set; }

        // Use a [Range] to ensure a valid selection (nonzero)
        [Range(1, int.MaxValue, ErrorMessage = "Please select a valid Country.")]
        [Display(Name = "Country")]
        [Required(ErrorMessage = "Country is required.")]
        public int? CountryId { get; set; }

        [Range(1, int.MaxValue, ErrorMessage = "Please select a valid State.")]
        [Display(Name = "State")]
        [Required(ErrorMessage = "Stat is required.")]
        public int? StateId { get; set; }

        [Range(1, int.MaxValue, ErrorMessage = "Please select a valid City.")]
        [Display(Name = "City")]
        [Required(ErrorMessage = "City is required.")]
        public int? CityId { get; set; }

        // Dropdown list for Countries; States and Cities will be loaded via AJAX
        public IEnumerable<SelectListItem>? Countries { get; set; }
    }
}
Employee Edit View Model

Create a class file named EmployeeEditViewModel.cs within the ViewModels folder, and then copy and paste the following code. This View Model is inherited from EmployeeCreateViewModel and adds an EmployeeId property. It also includes dropdown lists for states and cities to prepopulate the form with the current location details when editing an existing employee.

using Microsoft.AspNetCore.Mvc.Rendering;
namespace CascadingDemo.ViewModels
{
    public class EmployeeEditViewModel : EmployeeCreateViewModel
    {
        public int EmployeeId { get; set; }
        public IEnumerable<SelectListItem>? States { get; set; }
        public IEnumerable<SelectListItem>? Cities { get; set; }
    }
}
Creating Controllers:

A controller in ASP.NET Core MVC handles incoming HTTP requests, interacts with the data (via the DbContext or other services), and returns a response (usually a View). That means it handles user requests, processes data using models, and returns appropriate views or data responses.

Creating the Employee Controller

Create a new Empty MVC Controller named EmployeesController within the Controllers folder, then copy and paste the following code. This controller manages all employee-related operations.

using CascadingDemo.Data;
using CascadingDemo.Models;
using CascadingDemo.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;

namespace CascadingDemo.Controllers
{
    public class EmployeesController : Controller
    {
        private readonly EmployeeDBContext _context;

        public EmployeesController(EmployeeDBContext context)
        {
            _context = context;
        }

        // GET: Employees/Create
        public IActionResult Create()
        {
            var viewModel = new EmployeeCreateViewModel
            {
                Countries = new SelectList(_context.Countries.AsNoTracking().ToList(), "CountryId", "CountryName")
            };

            return View(viewModel);
        }

        // POST: Employees/Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(EmployeeCreateViewModel viewModel)
        {
            if (ModelState.IsValid)
            {
                // Map the view model to the domain model
                var employee = new Employee
                {
                    FullName = viewModel.FullName,
                    Email = viewModel.Email,
                    Phone = viewModel.Phone,
                    Department = viewModel.Department,
                    CountryId = Convert.ToInt32(viewModel.CountryId),
                    StateId = Convert.ToInt32(viewModel.StateId),
                    CityId = Convert.ToInt32(viewModel.CityId)
                };

                _context.Employees.Add(employee);
                await _context.SaveChangesAsync();
                return RedirectToAction(nameof(Index));
            }

            // Repopulate the Countries dropdown on error
            viewModel.Countries = new SelectList(_context.Countries.AsNoTracking().ToList(), "CountryId", "CountryName", viewModel.CountryId);
            return View(viewModel);
        }

        // GET: Employees/Edit/5
        public async Task<IActionResult> Edit(int id)
        {
            var employee = await _context.Employees.AsNoTracking().FirstOrDefaultAsync(e => e.EmployeeId == id);
            if (employee == null)
            {
                return NotFound();
            }

            // Map domain model to view model and preload dropdowns
            var viewModel = new EmployeeEditViewModel
            {
                EmployeeId = employee.EmployeeId,
                FullName = employee.FullName,
                Email = employee.Email,
                Phone = employee.Phone,
                Department = employee.Department,
                CountryId = employee.CountryId,
                StateId = employee.StateId,
                CityId = employee.CityId,
                Countries = new SelectList(_context.Countries.AsNoTracking().ToList(), "CountryId", "CountryName", employee.CountryId)
                // States and Cities will be loaded via AJAX in the view for prepopulation
            };

            return View(viewModel);
        }

        // POST: Employees/Edit/5
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(EmployeeEditViewModel viewModel)
        {
            if (ModelState.IsValid)
            {
                var employee = await _context.Employees.FindAsync(id);
                if (employee == null)
                {
                    return NotFound();
                }

                // Map view model values to the domain model
                employee.FullName = viewModel.FullName;
                employee.Email = viewModel.Email;
                employee.Phone = viewModel.Phone;
                employee.Department = viewModel.Department;
                employee.CountryId = Convert.ToInt32(viewModel.CountryId);
                employee.StateId = Convert.ToInt32(viewModel.StateId);
                employee.CityId = Convert.ToInt32(viewModel.CityId);
                await _context.SaveChangesAsync();
                return RedirectToAction(nameof(Index));
            }

            // Repopulate Countries on error
            viewModel.Countries = new SelectList(_context.Countries.AsNoTracking().ToList(), "CountryId", "CountryName", viewModel.CountryId);
            return View(viewModel);
        }

        // GET: Employees/Index
        public async Task<IActionResult> Index()
        {
            var employees = await _context.Employees
                .Include(e => e.Country)
                .Include(e => e.State)
                .Include(e => e.City)
                .AsNoTracking()
                .ToListAsync();
            return View(employees);
        }

        // JSON endpoint: Get States for a Country
        [HttpGet]
        public IActionResult GetStates(int countryId)
        {
            var states = _context.States.AsNoTracking().Where(s => s.CountryId == countryId).ToList();
            return Json(new SelectList(states, "StateId", "StateName"));
        }

        // JSON endpoint: Get Cities for a State
        [HttpGet]
        public IActionResult GetCities(int stateId)
        {
            var cities = _context.Cities.AsNoTracking().Where(c => c.StateId == stateId).ToList();
            return Json(new SelectList(cities, "CityId", "CityName"));
        }
    }
}
Index Action:
  • Retrieves a list of employees, including their linked Country, State, and City information, and passes them to the Index view to display the list of employees.
Create Actions (GET and POST):
  • GET: Initializes an empty EmployeeCreateViewModel by preloading the Countries dropdown and rendering the Create view.
  • POST: Handles form submission, validates the input, maps the view model to the Employee domain model, and saves a new employee record. In case of errors, repopulate the Countries dropdown and return the same view to display validation errors.
Edit Actions (GET and POST):
  • GET: It fetches the existing Employee record from the database using the ID and then maps the employee data into EmployeeEditViewModel, including the selected CountryId, StateId, and CityId. It preloads the Countries dropdown and passes everything to the Edit view.
  • POST: Receives updated data from the Edit form. If valid, update the existing employee record in the database and redirect it to the Index if successful. If invalid, repopulates the dropdown lists and returns the same Edit view.
JSON Endpoints (GetStates and GetCities):
  • These endpoints return filtered lists of States or Cities as JSON data based on the selected Country or State. They are used by the jQuery AJAX code in the views to implement cascading dropdown behavior.
Creating Views with Bootstrap & jQuery AJAX

Views are responsible for rendering the UI (HTML) to the end user. They typically receive a View Model or a Model from the Controller action. Let us create the required views using Bootstrap and jQuery AJAX to populate the dropdowns.

Employee Index View (Index.cshtml)

Create a view named Index.cshtml within the Views/Employees folder and copy and paste the following code. This View displays a list of employees in a table format with their ID, Name, Email, Phone, Department, and associated Country, State, and City. It also provides a link to the Create page to add new employees and an Edit button to modify existing employees.

@model IEnumerable<CascadingDemo.Models.Employee>
@{
    ViewData["Title"] = "Employee List";
}

<div class="container mt-4">
    <h2>Employee List</h2>
    <a href="@Url.Action("Create")" class="btn btn-primary mb-3">Register New Employee</a>
    <table class="table table-bordered table-striped">
        <thead class="table-dark">
            <tr>
                <th>Employee Id</th>
                <th>Full Name</th>
                <th>Email</th>
                <th>Phone</th>
                <th>Department</th>
                <th>Country</th>
                <th>State</th>
                <th>City</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var emp in Model)
            {
                <tr>
                    <td>@emp.EmployeeId</td>
                    <td>@emp.FullName</td>
                    <td>@emp.Email</td>
                    <td>@emp.Phone</td>
                    <td>@emp.Department</td>
                    <td>@emp.Country?.CountryName</td>
                    <td>@emp.State?.StateName</td>
                    <td>@emp.City?.CityName</td>
                    <td>
                        <a href="@Url.Action("Edit", new { id = emp.EmployeeId })" class="btn btn-warning btn-sm">Edit</a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
</div>
Employee Create View (Create.cshtml)

Create a view named Create.cshtml within the Views/Employees folder and copy and paste the following code. This view displays a form where we can enter a new Employee’s details and select a Country, State, and City from cascading dropdowns. It uses jQuery AJAX calls to retrieve the corresponding states and cities based on the selected country or state. The View also uses ASP.NET Core tag helpers (asp-for, asp-action, asp-items) for data binding.

@model CascadingDemo.ViewModels.EmployeeCreateViewModel
@{
ViewData["Title"] = "Register Employee";
}
<div class="container mt-5">
<!-- Card for better UI -->
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h3 class="m-0">Register Employee</h3>
</div>
<div class="card-body">
<!-- Begin Form -->
<form asp-action="Create" method="post">
<div class="row">
<!-- Full Name -->
<div class="col-md-6 mb-3">
<label asp-for="FullName" class="form-label"></label>
<input asp-for="FullName" class="form-control" />
<span asp-validation-for="FullName" class="text-danger"></span>
</div>
<!-- Email -->
<div class="col-md-6 mb-3">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>
<div class="row">
<!-- Phone -->
<div class="col-md-6 mb-3">
<label asp-for="Phone" class="form-label"></label>
<input asp-for="Phone" class="form-control" />
<span asp-validation-for="Phone" class="text-danger"></span>
</div>
<!-- Department -->
<div class="col-md-6 mb-3">
<label asp-for="Department" class="form-label"></label>
<input asp-for="Department" class="form-control" />
<span asp-validation-for="Department" class="text-danger"></span>
</div>
</div>
<!-- Divider / Subtitle -->
<hr />
<h5 class="mb-3">Location Details</h5>
<div class="row">
<!-- Country -->
<div class="col-md-4 mb-3">
<label asp-for="CountryId" class="form-label">Country</label>
<select asp-for="CountryId"
class="form-select"
asp-items="Model.Countries"
id="CountryId">
<option value="">Select Country</option>
</select>
<span asp-validation-for="CountryId" class="text-danger"></span>
</div>
<!-- State -->
<div class="col-md-4 mb-3">
<label asp-for="StateId" class="form-label">State</label>
<select asp-for="StateId"
class="form-select"
id="StateId">
<option value="">Select State</option>
</select>
<span asp-validation-for="StateId" class="text-danger"></span>
</div>
<!-- City -->
<div class="col-md-4 mb-3">
<label asp-for="CityId" class="form-label">City</label>
<select asp-for="CityId"
class="form-select"
id="CityId">
<option value="">Select City</option>
</select>
<span asp-validation-for="CityId" class="text-danger"></span>
</div>
</div>
<!-- Action buttons -->
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-success me-2">
<i class="bi bi-person-plus-fill"></i> Register
</button>
<a class="btn btn-secondary" href="@Url.Action("Index", "Employees")">
<i class="bi bi-arrow-left"></i> Back to List
</a>
</div>
</form>
<!-- End Form -->
</div>
</div>
</div>
<!-- Include jQuery library for AJAX functionality -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function () {
// Retrieve preselected values from the model, if any (useful when re-rendering after validation errors)
var selectedCountry = $('#CountryId').val();
var selectedState = '@Model.StateId';
var selectedCity = '@Model.CityId';
// If a country is already selected, load corresponding States and Cities
if (selectedCountry) {
// AJAX call to get the list of States for the selected Country
$.getJSON('@Url.Action("GetStates")', { countryId: selectedCountry }, function (states) {
//Get the State Dropdown which needs to be populated
var statesSelect = $('#StateId');
// Clear any existing options
statesSelect.empty();
// Add a default option prompting user selection
statesSelect.append('<option value="">Select State</option>');
// Populate the dropdown with retrieved states
// The index parameter represents the current iteration number (or position) in the array being iterated over.
$.each(states, function (index, state) {
// 'index' holds the current iteration number (0, 1, 2, ...)
// 'state' is the current state object
// In this code, index is not used directly, but it's available if needed.
var option = $('<option/>', {
value: state.value,
text: state.text
});
// If the state matches the preselected state, mark it as selected
if (state.value == selectedState) {
option.prop('selected', true);
}
statesSelect.append(option);
});
// If a state was preselected, load its corresponding Cities
if (selectedState) {
// AJAX call to get the list of Cities for the selected State
$.getJSON('@Url.Action("GetCities")', { stateId: selectedState }, function (cities) {
//Get the City Dropdown which needs to be populated
var citiesSelect = $('#CityId');
// Clear any existing options
citiesSelect.empty();
// Add a default option prompting user selection
citiesSelect.append('<option value="">Select City</option>');
// Populate the dropdown with retrieved cities
// The index parameter represents the current iteration number (or position) in the array being iterated over.
$.each(cities, function (index, city) {
var option = $('<option/>', {
value: city.value,
text: city.text
});
// If the city matches the preselected city, mark it as selected
if (city.value == selectedCity) {
option.prop('selected', true);
}
citiesSelect.append(option);
});
});
}
});
}
// Event handler: When the Country dropdown changes
$('#CountryId').change(function () {
//Get the Selected Country Dropdown Value
var countryId = $(this).val();
// AJAX call to load States based on the selected Country
$.getJSON('@Url.Action("GetStates")', { countryId: countryId }, function (states) {
//Get the State Dropdown which needs to be populated
var statesSelect = $('#StateId');
// Clear the States dropdown
statesSelect.empty();
// Add a default option prompting user selection
statesSelect.append('<option value="">Select State</option>');
// Populate with new state options
// The index parameter represents the current iteration number (or position) in the array being iterated over.
$.each(states, function (index, state) {
statesSelect.append($('<option/>', {
value: state.value,
text: state.text
}));
});
// Clear the Cities dropdown as the State selection has changed
$('#CityId').empty().append('<option value="">Select City</option>');
});
});
// Event handler: When the State dropdown changes
$('#StateId').change(function () {
//Get the Selected State Dropdown Value
var stateId = $(this).val();
// AJAX call to load Cities based on the selected State
$.getJSON('@Url.Action("GetCities")', { stateId: stateId }, function (cities) {
//Get the City Dropdown which needs to be populated
var citiesSelect = $('#CityId');
// Clear the Cities dropdown
citiesSelect.empty();
// Add a default option prompting user selection
citiesSelect.append('<option value="">Select City</option>');
// Populate with new city options
// The index parameter represents the current iteration number (or position) in the array being iterated over.
$.each(cities, function (index, city) {
citiesSelect.append($('<option/>', {
value: city.value,
text: city.text
}));
});
});
});
});
</script>
Employee Edit View (Edit.cshtml)

Create a view named Edit.cshtml within the Views/Employees folder and copy and paste the following code. This View is similar to the Create.cshtml view but is pre-populated with the existing Employee data for editing. It includes a hidden field for the EmployeeId so the controller knows which record to update. It also uses AJAX to load the state and city dropdowns based on the employee’s saved country and state.

@model CascadingDemo.ViewModels.EmployeeEditViewModel
@{
ViewData["Title"] = "Edit Employee";
}
<div class="container mt-5">
<!-- Card for better UI -->
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h3 class="m-0">Edit Employee</h3>
</div>
<div class="card-body">
<form asp-action="Edit" method="post">
<!-- Hidden field for EmployeeId -->
<input type="hidden" asp-for="EmployeeId" />
<!-- Personal Info -->
<div class="row">
<!-- Full Name -->
<div class="col-md-6 mb-3">
<label asp-for="FullName" class="form-label"></label>
<input asp-for="FullName" class="form-control" />
<span asp-validation-for="FullName" class="text-danger"></span>
</div>
<!-- Email -->
<div class="col-md-6 mb-3">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>
<div class="row">
<!-- Phone -->
<div class="col-md-6 mb-3">
<label asp-for="Phone" class="form-label"></label>
<input asp-for="Phone" class="form-control" />
<span asp-validation-for="Phone" class="text-danger"></span>
</div>
<!-- Department -->
<div class="col-md-6 mb-3">
<label asp-for="Department" class="form-label"></label>
<input asp-for="Department" class="form-control" />
<span asp-validation-for="Department" class="text-danger"></span>
</div>
</div>
<!-- Divider -->
<hr />
<h5 class="mb-3">Location Details</h5>
<div class="row">
<!-- Country -->
<div class="col-md-4 mb-3">
<label asp-for="CountryId" class="form-label">Country</label>
<select asp-for="CountryId"
class="form-select"
asp-items="Model.Countries"
id="CountryId">
<option value="">Select Country</option>
</select>
<span asp-validation-for="CountryId" class="text-danger"></span>
</div>
<!-- State -->
<div class="col-md-4 mb-3">
<label asp-for="StateId" class="form-label">State</label>
<select asp-for="StateId"
class="form-select"
id="StateId">
<option value="">Select State</option>
</select>
<span asp-validation-for="StateId" class="text-danger"></span>
</div>
<!-- City -->
<div class="col-md-4 mb-3">
<label asp-for="CityId" class="form-label">City</label>
<select asp-for="CityId"
class="form-select"
id="CityId">
<option value="">Select City</option>
</select>
<span asp-validation-for="CityId" class="text-danger"></span>
</div>
</div>
<!-- Action buttons -->
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-success me-2">
<i class="bi bi-save"></i> Save Changes
</button>
<a class="btn btn-secondary" href="@Url.Action("Index", "Employees")">
<i class="bi bi-arrow-left"></i> Back to List
</a>
</div>
</form>
</div>
</div>
</div>
<!-- jQuery and AJAX -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function () {
// Store the preselected values from the model
var selectedState = '@Model.StateId';
var selectedCity = '@Model.CityId';
// Pre-load the State dropdown based on the selected Country
var selectedCountry = $('#CountryId').val();
if (selectedCountry) {
$.getJSON('@Url.Action("GetStates")', { countryId: selectedCountry }, function (states) {
var statesSelect = $('#StateId');
statesSelect.empty();
statesSelect.append('<option value="">Select State</option>');
$.each(states, function (index, state) {
var option = $('<option/>', {
value: state.value,
text: state.text
});
if (state.value == selectedState) {
option.prop('selected', true);
}
statesSelect.append(option);
});
});
}
// Pre-load the City dropdown based on the selected State
if (selectedState) {
$.getJSON('@Url.Action("GetCities")', { stateId: selectedState }, function (cities) {
var citiesSelect = $('#CityId');
citiesSelect.empty();
citiesSelect.append('<option value="">Select City</option>');
$.each(cities, function (index, city) {
var option = $('<option/>', {
value: city.value,
text: city.text
});
if (city.value == selectedCity) {
option.prop('selected', true);
}
citiesSelect.append(option);
});
});
}
// When Country changes, update the States and reset Cities
$('#CountryId').change(function () {
var countryId = $(this).val();
$.getJSON('@Url.Action("GetStates")', { countryId: countryId }, function (states) {
var statesSelect = $('#StateId');
statesSelect.empty();
statesSelect.append('<option value="">Select State</option>');
$.each(states, function (index, state) {
statesSelect.append($('<option/>', {
value: state.value,
text: state.text
}));
});
// Reset City dropdown
$('#CityId').empty().append('<option value="">Select City</option>');
});
});
// When State changes, update Cities
$('#StateId').change(function () {
var stateId = $(this).val();
$.getJSON('@Url.Action("GetCities")', { stateId: stateId }, function (cities) {
var citiesSelect = $('#CityId');
citiesSelect.empty();
citiesSelect.append('<option value="">Select City</option>');
$.each(cities, function (index, city) {
citiesSelect.append($('<option/>', {
value: city.value,
text: city.text
}));
});
});
});
});
</script>
Modifying the Layout Page:

Please modify the Layout Page as follows. The Layout Page (_Layout.cshtml) defines the overall HTML structure: header, footer, and navigation. It also includes common resources (like Bootstrap, jQuery, and validation scripts) for the application. It contains the @RenderBody() placeholder where individual views get injected. It is shared across multiple views to provide a consistent look and feel.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - CascadingDemo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/CascadingDemo.styles.css" asp-append-version="true" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">CascadingDemo</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2025 - CascadingDemo - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

Now, launch the application. You can now register a new employee using the cascading dropdown lists. Once an employee is created, the admin is redirected to the Index page listing all employees, and the Edit page is available to update existing records.

When Should We Use Cascading Drop-Down Lists?
  • Hierarchical or Related Data: Whenever you have a natural hierarchy—like Country →, State →, City, or Category → Subcategory, and you only want to show the relevant child items.
  • Large Data Sets: If displaying all options at once is overwhelming or performance-heavy (e.g., you have hundreds of states and thousands of cities in a global application), cascading dropdowns keep the interface simpler and more manageable.
  • Data Validation and Accuracy: By restricting choices based on prior selections, we reduce user mistakes and enforce valid combinations. For example, users won’t mistakenly select Texas under India.
  • Improved User Experience: It is more user-friendly. The form feels responsive and helps guide the user by showing only the valid or relevant choices step by step.

Cascading drop-down lists are essential for any web application that requires structured and dependent data selection. They streamline user workflows, enhance data accuracy, and improve overall usability by presenting only context-relevant options. Using technologies like jQuery AJAX with ASP.NET Core MVC allows for efficient, dynamic, and interactive dropdown implementations.

In the next article, I will discuss the ASP.NET Core Razor Pages Application with an Example. In this article, I explain How to Implement a Cascading Dropdown List in ASP.NET Core MVC using jQuery AJAX with an Example. I hope you enjoy this article: How to Implement Cascading Dropdown List in ASP.NET Core MVC using jQuery AJAX.

Leave a Reply

Your email address will not be published. Required fields are marked *