Show cascading dropdown lists in ASP.NET Core
Many years ago I wrote a few articles on displaying cascading dropdown lists in ASP.NET (available here and here). Since this is a very common requirement while building web UIs it's worthwhile to see how cascading dropdown lists can be displayed in ASP.NET Core MVC also. That's what we will do in this article.
Before we delve into the code level details, take a look at the following figure that shows a classic example of cascading dropdown lists -- countries, states, and cities.
As you can see, there are three dropdown lists. The first dropdown list shows a list of countries when the page is loaded in the browser. This dropdown list is populated from server side code. Initially the state and city dropdown lists and the submit button are disabled because we want to ensure that the end user selects all the values before submitting the form.
Upon selecting a country, the state dropdown list loads all the states belonging to selected country and is enabled so that the end user can pick a state.
When the end user selects a state, the city dropdown is filled with all the cities for that state and is enabled (along with the submit button) so that the form can be submitted.
Once the form is submitted we simply display a message with the selected country, state, and city.
Now that you know how the application is going to work, let's create an ASP.NET Core MVC project and add the necessary pieces.
Once the project is created, add Models, Views, and Controllers folder as you normally do. Then add HomeController in the Controllers folder and Index.cshtml view in the Views -- Home folder.
Since we want to fetch country, state, and city data from a SQL Server database, add the NuGet package for SQL Server data provider for EF Core.
Then add three tables namely Countries, States, and Cities to a SQL Server database as per the following schema.
Then add the following three entity classes in the Models folder.
public class Country
{
public int CountryId { get; set; }
public string CountryName { get; set; }
}
public class State
{
public int StateId { get; set; }
public int CountryId { get; set; }
public string StateName { get; set; }
}
public class City
{
public int CityId { get; set; }
public int StateId { get; set; }
public string CityName { get; set; }
}
Also add a DbContext class as shown below:
public class AppDbContext:DbContext
{
public DbSet<Country> Countries { get; set; }
public DbSet<State> States { get; set; }
public DbSet<City> Cities { get; set; }
public AppDbContext(DbContextOptions
<AppDbContext> options):base(options)
{
}
}
Register the AppDbContext class with the DI container so that we can inject it into the controller.
builder.Services.AddDbContext<AppDbContext>
(o => o.UseSqlServer
(builder.Configuration.GetConnectionString("AppDb")));
The above code assumes that you have stored a database connection string named AppDb in the ConnectionStrings section of appsettings.json. Make sure to add this connection string as per your SQL Server setup. Also make sure to add some sample data to the Countries, States, and Cities tables before you move ahead.
"ConnectionStrings": {
"AppDb": "data source=.;
initial catalog=Northwind;
integrated security=true;
Encrypt=False"
}
The country dropdown list is populated using server side code when the page is loaded in the browser. The Index() action that supplies the country data to the dropdown list is shown below:
public IActionResult Index()
{
List<Country> data = db.Countries.ToList();
var countries = (from i in data
select new SelectListItem() {
Text=i.CountryName,
Value=i.CountryId.ToString()
}).ToList();
ViewData["countries"] = countries;
return View();
}
We get all the countries using the Countries DbSet and the ToList() method. In order to bind them with a dropdown list, we need to convert them into SelectListItem objects. So, we write a LINQ query that does the conversion. The List of SelectListItem objects is stored in ViewData so that we can access it from the Index view.
The markup of the Index view is shown below:
<h1>Cascading Dropdown Lists Demo</h1>
<form asp-controller="Home"
asp-action="Index" id="form1" name="form1">
<h2>Select a country :</h2>
<select id="country" name="country"
asp-items='@ViewData["countries"]
as List<SelectListItem>'
required>
<option value="">Please select</option>
</select>
<h2>Select a state :</h2>
<select id="state" name="state" required>
<option value="">Please select</option>
</select>
<h2>Select a city :</h2>
<select id="city" name="city" required>
<option value="">Please select</option>
</select>
<br/><br />
<button id="submit" type="submit">
Submit</button>
</form>
<h2>@ViewData["message"]</h2>
As you can see, the form contains three dropdown lists namely country, state, and city. The country dropdown list is filled with the list of countries available in the ViewData["countries"]. This is done using the asp-items property.
The state and city dropdown lists are initially empty because they need to be filled depending on the selection made in the country and state dropdown lists respectively.
The submit button POSTs the form to the Index() POST action of the HomeController. The ViewData["message"] is assigned a value in the Index() POST action and is displayed just below the form.
The Index() POST action is shown below:
[HttpPost]
public IActionResult Index
(int country, int state, int city)
{
var ctry = db.Countries.Find(country);
var ste = db.States.Find(state);
var cty = db.Cities.Find(city);
ViewData["message"] = "You selected : " +
ctry.CountryName + " -- " +
ste.StateName + " -- " +
cty.CityName;
List<Country> data = db.Countries.ToList();
var countries =
(from i in data
select new SelectListItem()
{
Text = i.CountryName,
Value = i.CountryId.ToString()
}).ToList();
ViewData["countries"] = countries;
return View();
}
The Index() action receives the countryId, stateId, and cityId as selected by the end user. Notice that the parameter names -- country, state, and city -- match the name attributes of the respective select elements.
Since we want to display the country, state, and city name (not their IDs) we get their names from our sample data. And then set the ViewData["message"] value accordingly. We also refill the ViewData["countries"] so that the user can pick a country again.
At this stage, if you run the application only country dropdown list is populated. But the state and city dropdown lists don't show any value. To fill the state and city dropdown lists we need to add a dash of JavaScript code. Let's do that.
Add a <script> block at the top of the Index view and write the following skeleton code in it.
document.addEventListener("DOMContentLoaded",
async function () {
var country = document.
getElementById("country");
var state = document.
getElementById("state");
var city = document.
getElementById("city");
var submit = document.
getElementById("submit");
state.disabled = true;
city.disabled = true;
submit.disabled = true;
country.addEventListener('change',
async function () {
});
state.addEventListener('change',
async function () {
});
});
Here, we grab references to the country, state, and city dropdown lists. We also grab a reference to the submit button. Initially, state, city, and submit are disabled.
Then we wire JavaScript change event handler of the country and state dropdown lists. This is where the magic happens -- we fetch() data from the server depending on the selection in the country and state dropdown lists respectively.
Let's add code to the change event handler of the country dropdown list.
country.addEventListener('change',
async function () {
if(country.value == "") {
state.disabled = true;
city.disabled = true;
submit.disabled=true;
return;
}
const request = new Request
("/Home/GetStates/" + country.value);
const options = {
method: "GET"
};
const response = await fetch(request, options);
if (!response.ok) {
message.innerHTML = `${response.status}
- ${response.statusText}`;
return;
}
const json = await response.json();
for (var i = state.options.length - 1;
i > 0; i--) {
state.removeChild(state.options[i]);
}
json.forEach(function(data){
const option =
document.createElement('option');
option.value = data.stateId;
option.innerHTML = data.stateName;
state.appendChild(option);
});
state.disabled = false;
});
The event handler begins by disabling the state, city, and submit if the user picks the "Please select" option. Then the code makes a fetch() request to the GetStates() method of the HomeController and passes the selected country ID to it. We will add the GetStates() method shortly.
The return value of GetStates() is a JSON array containing the State objects. We read the response using json() method. Then we remove all the previously filled options from the state dropdown list. A forEach loop iterates through the JSON array of State objects and loads the <option> elements to the state dropdown list. The state dropdown list is enabled by setting its disabled property to false.
Now, open the HomeController and add the GetStates() method as shown below:
public IActionResult GetStates(int id)
{
List<State> data = db.States
.Where(i=>i.CountryId==id)
.ToList();
return Ok(data);
}
The GetStates() method returns a list of states based on the CountryId passed to it.
On the similar lines we will add the change event handler for the state dropdown list.
state.addEventListener('change',
async function () {
if (state.value == "") {
city.disabled = true;
submit.disabled = true;
return;
}
const request = new Request
("/Home/GetCities/" + state.value);
const options = {
method: "GET"
};
const response = await fetch(request, options);
if (!response.ok) {
message.innerHTML = `${response.status}
- ${response.statusText}`;
return;
}
const json = await response.json();
for (var i = city.options.length - 1;
i > 0; i--) {
city.removeChild(city.options[i]);
}
json.forEach(function(data){
const option =
document.createElement('option');
option.value = data.cityId;
option.innerHTML = data.cityName;
city.appendChild(option);
});
city.disabled = false;
submit.disabled = false;
});
Here, we invoke the GetCities() method of the HomeController using fetch() and fill the city dropdown list with the returned JSON data.
Now, open the HomeController and add the GetCities() method as shown below:
public IActionResult GetCities(int id)
{
List<City> data = db.Cities
.Where(i => i.StateId == id)
.ToList();
return Ok(data);
}
The GetCities() method returns a list of cities based on the StateId passed to it.
This completes the application. Run the app and test whether the cascading dropdown lists are shown as expected.
That's it for now! Keep coding!!