Cascading Dropdown in Blazor

In my last post (CRUD using Blazor and Entity Framework Core), we discussed using Entity Framework Core with Blazor and implement CRUD functionalities.  We also discussed the usage of Bootstrap, Validations using Data Annotations and Creation of Dynamic content using RenderFragment. In this post, we will see how to implement a cascading dropdown in Blazor.

I will use the sample application we have created in the previous post and extend it with the new functionalities we are going to talk about in this post.  You will be familiar with the following topics after you read this post.

  • Data Binding
  • Cascading Dropdown
  • Show hide elements

Okay, Let’s get started.

Prerequisites

  • Visual Studio 2019
  • .NET Core 3.0
  • Blazor Templates

Data access service

In order to implement cascading dropdowns, I have created two tables named Categories and Products. A category can have more than one product. Using Entity Framework Core code-first approach, I have created the tables and populated them with sample data. I also have a model (Order) to store the selected ids of the dropdowns.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorApp.Data
{
    public class Category
    {
        [Key]
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Product
    {
        [Key]
        public int Id { get; set; }
        public string Name { get; set; }

        [ForeignKey("CategoryId")]
        public virtual Category Category { get; set; }

        public int CategoryId { get; set; }
    }

    public class Order
    {
        [Required(ErrorMessage = "Category is required")]
        public string CategoryId { get; set; }

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

        [Required(ErrorMessage = "Shipping City is required")]
        public string ShipCity { get; set; }
    }
}

I am going to create a service (CascadingDropdownService) that fetches the categories and the products belong to a category. This service implements from ICascadingDropdownService.

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BlazorApp.Data;
using Microsoft.EntityFrameworkCore;

namespace BlazorApp.Services
{
    public interface ICascadingDropdownService
    {
        Task<List<Category>> GetCategories();
        List<Product> GetProducts(int id);
    }
    public class CascadingDropdownService : ICascadingDropdownService
    {
        private readonly ApplicationDbContext _context;

        public CascadingDropdownService(ApplicationDbContext context)
        {
            _context = context;
        }
       
        public async Task<List<Category>> GetCategories()
        {
            return await _context.Categories.ToListAsync();
        }

        public List<Product> GetProducts(int id)
        {
            var products = _context.Products.Where(x => x.CategoryId.Equals(id)).ToList();
            return products;
        }
       
    }
}

Okay, the data access part is now completed.  Make sure, you have added the following code at the end of ConfigureServices method in startup.cs.  This is to inject CascadingDropdownService.

services.AddTransient<ICascadingDropdownService, CascadingDropdownService>();

Create the razor page

We have created the tables and the service to retrieve data from the tables. Let’s create a razor page and start to implement the cascading dropdown functionality. I am going to add a dropdown list on the page and populate it with categories in OnInitializedAsync. Following is the code of CascadingDropdown.razor

@page "/cascadingdropdown"

@using BlazorApp.Data
@using BlazorApp.Services
@inject ICascadingDropdownService service

<h1>Cascading Dropdown</h1>

<p>This component demonstrates Cascading dropdown</p>

@if (categories == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <div class="col-6">
        <EditForm Model="@OrderObject">
            <div class="form-group">
                <label for="category">Category</label>
                <InputSelect id="category" class="form-control" @bind-Value="Category">
                    <option value="">Select</option>
                    @foreach (var category in categories)
                    {
                        <option value="@category.Id">
                            @category.Name
                        </option>
                    }
                </InputSelect>
            </div>
            <button type="submit" class="btn btn-primary">Submit</button>
        </EditForm>
    </div>
}


@code {
    List<Category> categories;

    [Parameter]
    public Order OrderObject { get; set; }

    protected override async Task OnInitializedAsync()
    {
        categories = await service.GetCategories();
        OrderObject = new Order();
    }

    private string category;
    public string Category
    {
        get => category;
        set
        {
            category = value;
        }
    }
}

I have a form element that contains the Category dropdown. I bind the Category property to the dropdown. The application produces the following screen with the code we have now.

Dropdown with Categories loaded

Implementation of Cascading dropdown

Okay, what are the next steps?

We have to add a dropdown for Products. On the change of the Category dropdown, retrieve the products for the selected category. Those products need to be loaded in the product dropdown.

The following code contains GetProducts function to retrieve the products associated with the selected category. This function is called inside the set accessor of the Category property. I have also added a property for the Product.

@code {
    List<Category> categories;
    List<Product> products;


    [Parameter]
    public Order OrderObject { get; set; }


    protected override async Task OnInitializedAsync()
    {
        categories = await service.GetCategories();
        OrderObject = new Order();
    }

    private string category;
    public string Category
    {
        get => category;
        set
        {
            category = value;
            Product = default;
            GetProducts();
        }
    }

    void GetProducts()
    {
        products = null;
        if (!string.IsNullOrEmpty(category))
        {
            var selectedCategory = Convert.ToInt16(category);
            products = service.GetProducts(selectedCategory);
        }
    }

    private string product;
    public string Product
    {
        get => product;
        set
        {
            product = value;
            OrderObject.ShipCity = default;
        }
    }
}

The following code is added for the Product dropdown. This will be populated when there is a change in the Category dropdown as we are calling the GetProducts method inside the set accessor

<div class="form-group">
    <label for="product">Product</label>
    <InputSelect id="product" class="form-control" @bind-Value="Product">
        <option value="">Select</option>
        @if (products != null)
            {
            @foreach (var product in products)
                {
                <option value="@product.Id">
                    @product.Name
                </option>
                }
            }
    </InputSelect>
</div>

Now you can see the cascading dropdown list working.

Cascading Dropdown with Category and Products

Show Hide dropdown using Data binding

Now we have the cascading dropdown implemented. Let’s show and hide another dropdown based on the selection of the Product. We bind the Product property to the dropdown already. We can use the property in a condition to show the City dropdown.

@if (!string.IsNullOrEmpty(Product))
{
    <div class="form-group">
        <label for="city">Ship City</label>
        <InputSelect id="Summary" class="form-control" @bind-Value="OrderObject.ShipCity">
            <option value="">Select</option>
            @foreach (var city in Cities)
            {
                <option value="@city">
                    @city
                </option>
            }
        </InputSelect>
        <ValidationMessage For="@(() => OrderObject.ShipCity)" />
    </div>
}

You will see the following screen with all the three dropdowns now.

Cascading Dropdowns
@page "/cascadingdropdown"

@using BlazorApp.Data
@using BlazorApp.Services
@inject ICascadingDropdownService service

<h1>Cascading Dropdown</h1>

<p>This component demonstrates Cascading dropdown</p>

@if (categories == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <div class="col-6">
        <EditForm Model="@OrderObject">
            <DataAnnotationsValidator />
            <div class="form-group">
                <label for="category">Category</label>
                <InputSelect id="category" class="form-control" @bind-Value="Category">
                    <option value="">Select</option>
                    @foreach (var category in categories)
                    {
                        <option value="@category.Id">
                            @category.Name
                        </option>
                    }
                </InputSelect>
                <ValidationMessage For="@(() => OrderObject.CategoryId)" />
            </div>

            <div class="form-group">
                <label for="product">Product</label>
                <InputSelect id="product" class="form-control" @bind-Value="Product">
                    <option value="">Select</option>
                    @if (products != null)
                    {
                        @foreach (var product in products)
                            {
                            <option value="@product.Id">
                                @product.Name
                            </option>
                            }
                    }
                </InputSelect>
                <ValidationMessage For="@(() => OrderObject.ProductId)" />
            </div>
            @if (!string.IsNullOrEmpty(Product))
            {
                <div class="form-group">
                    <label for="city">Ship City</label>
                    <InputSelect id="Summary" class="form-control" @bind-Value="OrderObject.ShipCity">
                        <option value="">Select</option>
                        @foreach (var city in Cities)
                        {
                            <option value="@city">
                                @city
                            </option>
                        }
                    </InputSelect>
                    <ValidationMessage For="@(() => OrderObject.ShipCity)" />
                </div>
            }
            <button type="submit" class="btn btn-primary">Submit</button>
        </EditForm>
    </div>
}

@code {
    List<Category> categories;
    List<Product> products;

    [Parameter]
    public Order OrderObject { get; set; }

    List<string> Cities = new List<string>() { "Chennai", "Kolkata", "Mumbai", "New Delhi" };

    protected override async Task OnInitializedAsync()
    {
        categories = await service.GetCategories();
        OrderObject = new Order();
    }

    private string category;
    public string Category
    {
        get => category;
        set
        {
            category = OrderObject.CategoryId = value;
            Product = default;
            GetProducts();
        }
    }

    void GetProducts()
    {
        products = null;
        if (!string.IsNullOrEmpty(category))
        {
            var selectedCategory = Convert.ToInt16(category);
            products = service.GetProducts(selectedCategory);
        }
    }

    private string product;
    public string Product
    {
        get => product;
        set
        {
            product = OrderObject.ProductId = value;
            Console.WriteLine(Product);
            OrderObject.ShipCity = default;
        }
    }

    private void HandleValidSubmit()
    {
        Console.WriteLine("OnValidSubmit");
    }
}

Summary

This post tried to explain about implementing Cascading dropdowns in Blazor using Entity Framework Core and Data binding capabilities of Blazor.  We also discussed the show and hide content functionality using Data binding.

The full implementation of this post will be available in Github

Further reading

The following are some of the links you can refer to if you want to learn more about Blazor.

Leave a Reply

Close Menu
Share via
Copy link
Powered by Social Snap