MVC: Two Different Methods of Producing Cascading Drop Downs
Cascading drop downs is a feature that we see in many different applications across the web. A common scenario is asking a user to enter in their State and City.
Features like these are such common place that they are almost always expected in a web-based application. The following code sample will walk you through two different ways of producing this effect if you are using .NET Core MVC as the web development framework.
This sample application uses some JSON files that contain a listing of Cities and States. It then uses an in-memory entity framework database for temporary storage of the selected States and Cities.
The Create and Edit pages have both been included in this example since the cascading effects are different before it is known what a user is going to select vs knowing what the user has selected.
The code for this sample can be found in this GitHub repository:
Method 1
Description
The first method demonstrated on the page takes advantage of server-side rendering. It will render all the possible values for all Cities and all States all at once. After the page is rendered, JavaScript reads the data from the drop downs and stores all the possible values in a JavaScript array. Then after a state is selected it will only show available values for the Cities that were made in the array.
Pros
One call to the Create / Edit page retrieves all the information necessary to perform the cascading actions. No additional calls to the server for additional lists of Cities after a State is selected.
Cons
This method can produce waste. The server must load all the possible values for every combination of every City and State before the page is rendered. The values are also then stored client side in JavaScript arrays. While this can be faster in some scenarios. Where there is greater amount of data the benefit might not be shown.
Code Highlights
When using this method all of the states and cities are rendered in to the select list by setting them as view bags for the Create and Edit pages.
// GET: SelectedCityStates/Create
public IActionResult Create()
{
SelectedCityState selectedCityStateToEdit = new SelectedCityState();
SetStateCityViewBags();
return View(selectedCityStateToEdit);
}
// GET: SelectedCityStates/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var selectedCityState = await _context.SelectedCityStates.FindAsync(id);
if (selectedCityState == null)
{
return NotFound();
}
SetStateCityViewBags();
return View(selectedCityState);
}
private void SetStateCityViewBags()
{
string statesTextFile = System.IO.File.ReadAllText("states.json");
string citiesTextFile = System.IO.File.ReadAllText("cities.json");
List<USState> usStates = JsonSerializer.Deserialize<List<USState>>(statesTextFile, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }).OrderBy(o => o.Name).ToList();
List<USCity> usCities = JsonSerializer.Deserialize<List<USCity>>(citiesTextFile, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }).OrderBy(o => o.State).ThenBy(o => o.City).ToList();
ViewBag.USStates = usStates;
ViewBag.USCities = usCities;
}
@model MVCCascadingDropDowns.Models.SelectedCityState
@using MVCCascadingDropDowns.Models;
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
@if(Model != null)
{
<input type="hidden" asp-for="Id" />
}
<div class="form-group">
<label asp-for="State" class="control-label"></label>
<select name="State" id="State" class="form-control">
@if (string.IsNullOrEmpty(Model.State))
{
<option value="" selected>Please Select</option>
}
else
{
<option value="">Please Select</option>
}
@foreach (USState usState in (List<USState>)ViewBag.USStates)
{
if (Model.State == usState.Name)
{
<option value="@usState.Name" selected>@usState.Name</option>
}
else
{
<option value="@usState.Name">@usState.Name</option>
}
}
</select>
<span asp-validation-for="State" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="City" class="control-label"></label>
<select name="City" id="City" class="form-control">
@if (string.IsNullOrEmpty(Model.City))
{
<option value="" selected>Please Select</option>
}
else
{
<option value="">Please Select</option>
}
@foreach (USCity usCity in (List<USCity>)ViewBag.USCities)
{
if (Model.State == usCity.State && Model.City == usCity.City)
{
<option value="@usCity.City" data-state="@usCity.State" selected>@usCity.State - @usCity.City</option>
}
else
{
<option value="@usCity.City" data-state="@usCity.State">@usCity.State - @usCity.City</option>
}
}
</select>
<span asp-validation-for="City" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
$(() => {
var states = [];
var cities = [];
var stateSelectList = $('#State');
var citySelectList = $('#City');
var selectedState = stateSelectList.val();
var selectedCity = citySelectList.val();
populateStatesArray();
populateCitiesArray();
filterCities();
function populateStatesArray() {
stateSelectList.find('option').each((index, element) => {
states.push(element.value);
});
}
function populateCitiesArray() {
citySelectList.find('option').each((index, element) => {
cities.push({
state: element.getAttribute('data-state'),
city: element.value
});
});
}
function filterCities() {
citySelectList.prop('options').length = 1;
if (selectedState !== '') {
for (var i = 0; i < cities.length; i++) {
if (cities[i].state == selectedState) {
var selectThisItem = (cities[i].city === selectedCity);
var opt = document.createElement('option');
opt.setAttribute('data-state', cities[i].state);
opt.setAttribute('value', cities[i].city);
opt.appendChild(document.createTextNode(cities[i].city));
if (selectThisItem) {
opt.setAttribute('selected', true);
}
citySelectList.append(opt);
}
}
}
}
stateSelectList.on('change', (e) => {
selectedState = stateSelectList.val();
filterCities();
});
});
Method 2
Description
The second method demonstrated on the page takes advantage of AJAX calls. Two empty select lists are rendered by the server, but they have no data until after the page loads. After the page loads it will retrieve a list of States. After a State is selected, an additional call to the server requesting the relevant Cities, and then the Cities drop down list is populated based on the results returned by the server.
Pros
The page loads, before the list of States is retrieved. States, and Cities are not stored in JavaScript arrays on the client, so it takes up less memory on the client / browser.
Cons
It makes your application “chatty”. There is not much of a delay between selecting a State and the list of Cities appearing, but if the complexity of the data to be cascaded isn’t very complex then it may be better off storing them client side and letting the client pull the appropriate values without having to go back out to the server to retrieve the relevant values.
Code Highlights
The controller will have two extra methods on it to get states list and get cities list. The Create and Edit Views no longer supply ViewBags
// GET: SelectedCityStatesAjax/Create
public IActionResult Create()
{
SelectedCityState selectedCityStateToEdit = new SelectedCityState();
return View(selectedCityStateToEdit);
}
// GET: SelectedCityStatesAjax/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var selectedCityState = await _context.SelectedCityStates.FindAsync(id);
if (selectedCityState == null)
{
return NotFound();
}
return View(selectedCityState);
}
public async Task<JsonResult> GetStatesList()
{
string statesTextFile = await System.IO.File.ReadAllTextAsync("states.json");
List<USState> usStates = JsonSerializer.Deserialize<List<USState>>(statesTextFile, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }).OrderBy(o => o.Name).ToList();
return Json(usStates, new JsonSerializerOptions { DictionaryKeyPolicy = JsonNamingPolicy.CamelCase });
}
public async Task<JsonResult> GetCitiesList([FromQuery]string state)
{
string citiesTextFile = await System.IO.File.ReadAllTextAsync("cities.json");
List<USCity> usCities = JsonSerializer.Deserialize<List<USCity>>(citiesTextFile, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }).Where(w => w.State == state).OrderBy(o => o.State).ThenBy(o => o.City).ToList();
return Json(usCities, new JsonSerializerOptions { DictionaryKeyPolicy = JsonNamingPolicy.CamelCase });
}
Finally, after the server has rendered the view, JavaScript takes over. The states list is populated immediately. After a State is selected then an additional ajax call is made to retrieve the Cities list. Then a listener is setup to fetch and render a new list of cities any time a new State is selected.
$(() => {
var stateSelectList = $('#State');
var citySelectList = $('#City');
populateStatesDropDown();
function populateStatesDropDown() {
$.get(
'/SelectedCityStatesAjax/GetStatesList', (states) => {
for (var s = 0; s < states.length; s++) {
var opt = document.createElement('option');
opt.setAttribute('value', states[s]['Name']);
opt.appendChild(document.createTextNode(states[s]['Name']));
if (states[s]['Name'] === stateSelectList.attr('data-initial-val')) {
opt.setAttribute('selected', true);
}
stateSelectList.append(opt);
}
if (stateSelectList.val() !== '') {
populateCitiesDropDown();
}
}
);
}
function populateCitiesDropDown() {
$.get(
`/SelectedCityStatesAjax/GetCitiesList?state=${stateSelectList.val()}`, (cities) => {
for (var c = 0; c < cities.length; c++) {
var opt = document.createElement('option');
opt.setAttribute('value', cities[c]['City']);
opt.appendChild(document.createTextNode(cities[c]['City']));
if (cities[c]['City'] === citySelectList.attr('data-initial-val')) {
opt.setAttribute('selected', true);
}
citySelectList.append(opt);
}
}
)
}
stateSelectList.on('change', (e) => {
citySelectList.prop('options').length = 1;
if (e.currentTarget.value !== '') {
populateCitiesDropDown();
}
});
});
Conclusion
Both methods are good options depending on the use case and complexity of the data. Some analysis and test may be needed to determine which is best for the situation.
About Intertech
Founded in 1991, Intertech delivers software development consulting to Fortune 500, Government, and Leading Technology institutions, along with real-world based corporate education services to current consulting customers. Whether you are a company looking to partner with a team of technology leaders who provide solutions, mentor staff and add true business value, or a developer interested in working for a company that invests in its employees, we’d like to meet you. Learn more about us.