In this post we will see how to implement a Cascading DropDownList in Blazor. When we talk about a Cascading DropDownList (or dependent DropDownList), we mean when the options of a DropDownList are filtered by another DropDownList.
For example, if we have the Country and State DropDownLists, then the States to be displayed will be filtered according to the selected Country.
In this video I show how to implement this step by step:
In this post I’ll summarize the content of the video:
Note: This code was done with ASP.NET Core 3.1.
Repository: https://github.com/gavilanch/BlazorCascadingDropDownList
For our example, what we did was to create the Country and State entities, and we added a StateId property in a Person class, because we want to cascade the DropDown in this entity:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Country | |
{ | |
public int Id { get; set; } | |
public string Name { get; set; } | |
public List<State> States { get; set; } | |
} | |
public class State | |
{ | |
public int Id { get; set; } | |
public string Name { get; set; } | |
public int CountryId { get; set; } | |
public Country Country { get; set; } | |
} | |
public class Person | |
{ | |
public int Id { get; set; } | |
[Required] | |
public string Name { get; set; } | |
public string Biography { get; set; } | |
[Range(1, int.MaxValue, ErrorMessage = "You must select a state")] | |
public int StateId { get; set; } | |
public State State { get; set; } | |
} |
Then, I create the tables corresponding to these entities in my database. We can also use Data Seeding to fill these entities with test data.
The next step is to place the DropDownLists in a form. However, at the time of writing this entry, the InputSelect component of Blazor does not support ints (integers) as a value, therefore, we have to create a component that does this. We can create it as a class, since we are only going to extend the InputSelect functionality to handle ints (I think this code was written by Steve Sanderson):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class InputSelectNumber<T> : InputSelect<T> | |
{ | |
protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage) | |
{ | |
if (typeof(T) == typeof(int)) | |
{ | |
if (int.TryParse(value, out var resultInt)) | |
{ | |
result = (T)(object)resultInt; | |
validationErrorMessage = null; | |
return true; | |
} | |
else | |
{ | |
result = default; | |
validationErrorMessage = "The chosen value is not a valid number."; | |
return false; | |
} | |
} | |
else | |
{ | |
return base.TryParseValueFromString(value, out result, out validationErrorMessage); | |
} | |
} | |
} |
With this we can use the InputSelect with numbers. In this case we use “countryId” as a component field, because our EditForm model does not bring a Country property (we only put Status in the Person class). We also use ValueChanged to execute a method when the value of the Country DropDownList is changed. This way we will execute the State filter:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="form-group"> | |
<label>Country:</label> | |
<div> | |
<InputSelectNumber class="form-control" | |
ValueChanged="@((int value) => CountryHasChanged(value))" | |
ValueExpression="@(() => countryId)" | |
Value="@countryId"> | |
<option value="0">–Select a country–</option> | |
@foreach (var item in Countries) | |
{ | |
@if (item.Id == countryId) | |
{ | |
<option selected value="@item.Id">@item.Name</option> | |
} | |
else | |
{ | |
<option value="@item.Id">@item.Name</option> | |
} | |
} | |
</InputSelectNumber> | |
</div> | |
</div> |
In the case of the State DropDownList, we do almost the same, except that since we do not need to use ValueChanged, we can simplify the code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="form-group"> | |
<label>State:</label> | |
<div> | |
<InputSelectNumber class="form-control" | |
@bind-Value="Person.StateId" | |
> | |
<option value="0">–Select a state–</option> | |
@foreach (var item in States) | |
{ | |
@if (item.Id == Person.StateId) | |
{ | |
<option selected value="@item.Id">@item.Name</option> | |
} | |
else | |
{ | |
<option value="@item.Id">@item.Name</option> | |
} | |
} | |
</InputSelectNumber> | |
<ValidationMessage For="@(() => Person.StateId)" /> | |
</div> | |
</div> |
In the C # code of the component we implement the CountryHasChanged method, where we call the LoadStates method, which is responsible for carrying out the filter:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@code { | |
[Parameter] public Person Person { get; set; } | |
[Parameter] public string ButtonText { get; set; } = "Save Person"; | |
[Parameter] public EventCallback OnValidSubmit { get; set; } | |
private List<Country> Countries = new List<Country>(); | |
private List<State> States = new List<State>(); | |
private int countryId = 0; | |
protected override async Task OnInitializedAsync() | |
{ | |
if (Person.State != null) | |
{ | |
countryId = Person.State.CountryId; | |
await LoadStates(countryId); | |
} | |
Countries = await http.GetJsonAsync<List<Country>>("api/countries"); | |
} | |
private async Task CountryHasChanged(int value) | |
{ | |
Person.StateId = 0; | |
countryId = value; | |
if (value == 0) | |
{ | |
States.Clear(); | |
} | |
else | |
{ | |
await LoadStates(value); | |
} | |
} | |
private async Task LoadStates(int countryId) | |
{ | |
States = await http.GetJsonAsync<List<State>>($"api/countries/{countryId}/states"); | |
} | |
} |
Finally, in the country controller we look for the list of countries and the states filtered by country:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ApiController] | |
[Route("api/[controller]")] | |
public class CountriesController: ControllerBase | |
{ | |
private readonly ApplicationDbContext context; | |
public CountriesController(ApplicationDbContext context) | |
{ | |
this.context = context; | |
} | |
[HttpGet] | |
public async Task<ActionResult<List<Country>>> Get() | |
{ | |
return await context.Countries.OrderBy(x => x.Name).ToListAsync(); | |
} | |
[HttpGet("{countryId}/states")] | |
public async Task<List<State>> GetStates(int countryId) | |
{ | |
return await context.States.Where(x => x.CountryId == countryId) | |
.OrderBy(x => x.Name).ToListAsync(); | |
} | |
} |
These are the steps to implement a cascade DropDownList.
Summary
- With cascading DropDownLists we can filter the values of a DropDownList according to the values of another DropDownList
- We use the ValueChanged attribute to execute a functionality when the value of a component changes
- We create the InputSelectNumber component to make the InputSelect component handle integers.
Course
If you want to learn step by step how to build Web Applications from Scratch using Blazor, check out my full course: https://www.udemy.com/course/programming-in-blazor-aspnet-core/?referralCode=8EFA9D9FF38E3065DF0C