Cascading dropdowns in Blazor C#

Blazor is a web framework introduced by Microsoft that allows developers to create web applications using C# and HTML. It allows for rich UI elements to be created without the need for any JavaScript. I thought I would demonstrate this by creating a popular form element that many may have found themselves previously writing in JavaScript. 

Cascading dropdowns are a series of dependent dropdown lists or select lists in which the values that are available in one dropdown list depend on what has been selected in a parent or previous list. When the value of the parent dropdown list changes, the options in the second dropdown list change depending on the value of the first. 

In JavaScript, this would have typically been achieved by performing an Ajax call upon the value of the parent dropdown changing. The value of the parent dropdown would be passed to the server and a list of items based on that value would be retrieved. These options would then be used to populate a child dropdown. Not a difficult feat by any means, but far from efficient and certainly arduous. This is made much simpler in Blazor.

In this example, we will be building a form whereby you first select a food category and then a food product. We’ll start by creating some necessary classes:

public class Category
{
    public int Id { get; set; }

    public string Name { get; set; }
}

public class Product
{
    public int Id { get; set; }

    public int CategoryId { get; set; }

    public string Name { get; set; }
}

public class SelectListItem
{
    public string Text { get; set; }

    public int Value { get; set; }
}

As well as the Food and Product classes, we also have a SelectListItem class. This will be the type returned when we are retrieving our dropdown lists. Next, we’ll create a SelectListService. Don’t forget to register this service in Startup.cs. Note the first two methods that retrieve categories and products: although in this example they sit in the SelectListService, in reality they may be better off in a service of their own so that they can be injected anywhere in the application and thus re-used elsewhere. 

public class SelectListService
{
    private static IEnumerable<Category> GetCategories()
    {
        return new List<Category>()
        {
            new Category { Id = 1, Name = "Cheese" },
            new Category { Id = 2, Name = "Vegetables" },
            new Category { Id = 3, Name = "Meat" }
        };
    }

    private static IEnumerable<Product> GetProducts()
    {
        return new List<Product>()
        {
            new Product { Id = 1, CategoryId = 1, Name = "Stilton" },
            new Product { Id = 2, CategoryId = 1, Name = "Cheddar" },
            new Product { Id = 3, CategoryId = 2, Name = "Carrot" },
            new Product { Id = 4, CategoryId = 2, Name = "Broccoli" },
            new Product { Id = 5, CategoryId = 3, Name = "Chicken" },
            new Product { Id = 6, CategoryId = 3, Name = "Beef" }
        };
    }
}

Next is for the methods that will serve us our dropdown lists. The method to be used for our parent select list will serve us the full list of categories. However, the method for our products, which is to be our child dropdown list, will accept a category ID as a parameter so that we can filter the products based on the category that has been selected.

public IEnumerable<SelectListItem> GetCategoriesSelectList()
{
    return GetCategories().Select(c => new SelectListItem
    {
        Text = c.Name,
        Value = c.Id
    });
}

public IEnumerable<SelectListItem> GetProductsSelectList(int categoryId)
{
    return GetProducts().Where(p => p.CategoryId == categoryId).Select(c => new SelectListItem
    {
        Text = c.Name,
        Value = c.Id
    });
}

Now that our data service is in place, we can implement our form. We’ll start by creating the viewmodel that will serve as the context of our form. Note that the properties bound to each dropdown are both integers as we’ll look to capture the category and product IDs. 

public class FoodViewModel
{
    [Range(1, double.MaxValue, ErrorMessage = "Please select a category.")]
    public int CategoryId { get; set; }

    [Range(1, double.MaxValue, ErrorMessage = "Please select a product.")]
    public int ProductId { get; set; }
}

Next, we will implement our parent dropdown, listing all of the categories:

@inject SelectListService SelectListService

<h1>Cascading Dropdowns</h1>

<p>This component demonstrates the use of cascading dropdowns.</p>

@if (categories != null)
{
    <EditForm Model="@viewModel" OnValidSubmit="@HandleValidSubmit">
        <DataAnnotationsValidator />
        <ValidationSummary />

        <div class="form-group">
            <select id="CategoryId" class="form-control">
                <option value="0">Please select a category</option>
                @foreach (var category in categories)
                {
                    <option value="@category.Value">@category.Text</option>
                }
            </select>
        </div>

        <button type="submit" class="btn btn-primary">Submit</button>
    </EditForm>
}

@code {
    private FoodViewModel viewModel = new();
    private IEnumerable<SelectListItem> categories = null;

    protected override void OnInitialized() => categories = SelectListService.GetCategoriesSelectList();

    private void HandleValidSubmit()
    {
        // Do something.
    }
}

When the value of this dropdown changes, we want to show a list of products pertaining to that category. First, we need a property for the products dropdown list. Next, we’ll implement an OnChange handler to the category dropdown list:

@inject SelectListService SelectListService

<h1>Cascading Dropdowns</h1>

<p>This component demonstrates the use of cascading dropdowns.</p>

@if (categories != null)
{
    <EditForm Model="@viewModel" OnValidSubmit="@HandleValidSubmit">
        <DataAnnotationsValidator />
        <ValidationSummary />

        <div class="form-group">
            <select id="CategoryId" @onchange="OnChangeCategory" class="form-control">
                <option value="0">Please select a category</option>
                @foreach (var category in categories)
                {
                    <option value="@category.Value">@category.Text</option>
                }
            </select>
        </div>

        @if (products == null)
        {
            // If a category has not been selected, show a disabled dropdown
            <div class="form-group">
                <select id="CategoryId" class="form-control" disabled>
                    <option value="0">Please select a product</option>
                </select>
            </div>
        }
        else
        {
            <div class="form-group">
                <InputSelect id="CategoryId" @bind-Value="viewModel.ProductId" class="form-control">
                    <option value="0">Please select a product</option>
                    @foreach (var product in products)
                    {
                        <option value="@product.Value">@product.Text</option>
                    }
                </InputSelect>
            </div>
        }

        <button type="submit" class="btn btn-primary">Submit</button>
    </EditForm>
}

@code {
    private FoodViewModel viewModel = new();
    private IEnumerable<SelectListItem> categories = null;
    private IEnumerable<SelectListItem> products = null;

    protected override void OnInitialized() => categories = SelectListService.GetCategoriesSelectList();

    private void OnChangeCategory(ChangeEventArgs e)
    {
        viewModel.CategoryId = Convert.ToInt32(e.Value);

        if (viewModel.CategoryId == 0)
            products = null;
        else
            products = SelectListService.GetProductsSelectList(viewModel.CategoryId);
    }

    private void HandleValidSubmit()
    {
        // Do something.
    }
}

We can see that when a category has been selected, the OnChangeCategory handler fetches a list of product dropdown items for that particular category and assigns it to the products property. We have also catered for if a category is not selected (if the placeholder is selected instead of a category), products is set to null. This allows us to either show a list of products based off of the selected category, or simply a disabled dropdown.

I hope this post has demonstrated how cascading dropdowns can be achieved in Blazor without any JavaScript at all. If you’re already used to writing applications in C#, Blazor is a great way of achieving sleek UI elements whilst writing in the language you’re already familiar with. Instead of performing an Ajax call, you’re fetching data as you normally would in a C# application. Instead of writing JavaScript to wipe and re-populate a dropdown on completion of an Ajax call, dropdown lists are populated in a far more simpler fashion. 

Scroll to Top