Cascading DropdDown

Environment

Product DropDownList for Blazor, ComboBox for Blazor

Description

How to make cascading dropdown or combobox components? The data from one should filter the selection of the next and enable them.

Solution

Use the ValueChanged event to update the model value and to filter the data for the next dropdown. Additionally, you can also use its Enabled parameter and tie it to the value of its "parent".

Cascading DropDowns

@* Cascading componentsare disabled based on the selection of their parents. Events on parent components
    trigger data loading for child components so they show relevant result only.
    You can also see how to get the selected model from a dropdown component.
    The same approach works for a ComboBox (just use nullable values), see the next example *@

<TelerikDropDownList Value="@CurrentOrder.CategoryId" Data="@Categories" DefaultText="Select Category"
                     TextField="CategoryName" ValueField="CategoryId"
                     ValueChanged="@( (int c) => CategorySelected(c) )">
</TelerikDropDownList>

<TelerikDropDownList Value="@CurrentOrder.ProductId" Data="@CurrentProducts" DefaultText="Select Product"
                     TextField="ProductName" ValueField="ProductId" Enabled="@( CurrentOrder.CategoryId > 0 )"
                     ValueChanged="@( (int p) => ProductSelected(p) )">
</TelerikDropDownList>

@* The last dropdown can use two-way binding, it does not need to filter subsequent data *@
<TelerikDropDownList @bind-Value="@CurrentOrder.Quantity" Data="@Quantities" DefaultText="Select Quantity"
                     Enabled="@( CurrentOrder.ProductId > 0 )">
</TelerikDropDownList>

<TelerikButton Enabled="@( CurrentOrder.Quantity > 0 )" OnClick="@SendOrder">Send Order</TelerikButton>

@if (CurrentOrder.CategoryId > 0)
{
    <h5>Order Summary</h5>
    @CurrentOrder.CategoryName
    <br />
    @CurrentOrder.ProductName
    <br />
    @CurrentOrder.Quantity
}
else if(!string.IsNullOrEmpty(orderStatusMessage))
{
    <div class="alert alert-success">@orderStatusMessage</div>
}

@code{
    // data sources
    List<Category> Categories { get; set; }
    List<Product> AllProducts { get; set; }
    List<Product> CurrentProducts { get; set; }
    List<int> Quantities { get; set; }
    // model
    Order CurrentOrder { get; set; } = new Order();

    string orderStatusMessage { get; set; } // UI related for the sample

    // generate data we will be using in this example
    protected override void OnInitialized()
    {
        base.OnInitialized();

        Categories = Enumerable.Range(1, 6).Select(x => new Category
        {
            CategoryId = x,
            CategoryName = $"Category {x}"
        }).ToList();

        AllProducts = Enumerable.Range(1, 50).Select(x => new Product
        {
            ProductId = x,
            ProductName = $"Product {x}",
            CategoryId = (int)Math.Ceiling((double)x % 7)
        }).ToList();
    }

    //ValueChanged handlers - implementation of cascading dropdowns
    void CategorySelected(int category)
    {
        if(category == 0) // the default value - the user selected the default item == deselected the current item
        {
            //reset the "form" / process
            CurrentOrder = new Order();
            return;
        }

        // cascade the selection by filtering the data for the next dropdown
        CurrentProducts = AllProducts.Where(p => p.CategoryId == category).ToList();

        // get the selected model from the data source
        Category SelectedCategory = Categories.Where(c => c.CategoryId == category).First();

        // business logic
        CurrentOrder.CategoryId = SelectedCategory.CategoryId;
        CurrentOrder.CategoryName = SelectedCategory.CategoryName;
    }

    void ProductSelected(int product)
    {
        if(product == 0) // the default value - the user selected the default item == deselected the current item
        {
            //reset the "form" / process
            CurrentOrder.ProductId = product;
            CurrentOrder.ProductName = string.Empty;
            CurrentOrder.Quantity = 0;
            return;
        }

        Random rnd = new Random();
        Quantities = Enumerable.Range(1, new Random().Next(5, 10)).ToList();

        Product SelectedProduct = AllProducts.Where(p => p.ProductId == product).First();

        CurrentOrder.ProductId = SelectedProduct.ProductId;
        CurrentOrder.ProductName = SelectedProduct.ProductName;
    }

    // sample notification of success and reseting of the process, data classes
    async Task SendOrder()
    {
        CurrentOrder = new Order();
        orderStatusMessage = "Thank you for your order!";
        await Task.Delay(2000);
        orderStatusMessage = "";
        StateHasChanged();
    }

    public class Category
    {
        public int CategoryId { get; set; }
        public string CategoryName { get; set; }
    }

    public class Product
    {
        public int CategoryId { get; set; }
        public int ProductId { get; set; }
        public string ProductName { get; set; }
    }

    public class Order
    {
        public int CategoryId { get; set; }
        public string CategoryName { get; set; }
        public int ProductId { get; set; }
        public string ProductName { get; set; }
        public int Quantity { get; set; }
    }
}
@* The same approach works for the ComboBox, just make sure to use a nullable field so you can see the Placeholder *@

<TelerikComboBox Value="@CurrentOrder.CategoryId" Data="@Categories" Placeholder="Select Category"
                    TextField="CategoryName" ValueField="CategoryId" Filterable="true"
                    ValueChanged="@( (int? c) => CategorySelected(c) )">
</TelerikComboBox>

<TelerikComboBox Value="@CurrentOrder.ProductId" Data="@CurrentProducts" Placeholder="Select Product" Filterable="true"
                    TextField="ProductName" ValueField="ProductId" Enabled="@( CurrentOrder.CategoryId > 0 )"
                    ValueChanged="@( (int? p) => ProductSelected(p) )">
</TelerikComboBox>

@* The last dropdown can use two-way binding, it does not need to filter subsequent data *@
<TelerikComboBox @bind-Value="@CurrentOrder.Quantity" Data="@Quantities" Placeholder="Select Quantity"
                    Enabled="@( CurrentOrder.ProductId > 0 )">
</TelerikComboBox>

<TelerikButton Enabled="@( CurrentOrder.Quantity > 0 )" OnClick="@SendOrder">Send Order</TelerikButton>

@if (CurrentOrder.CategoryId > 0)
{
    <h5>Order Summary</h5>
    @CurrentOrder.CategoryName
    <br />
    @CurrentOrder.ProductName
    <br />
    @CurrentOrder.Quantity
}
else if (!string.IsNullOrEmpty(orderStatusMessage))
{
    <div class="alert alert-success">@orderStatusMessage</div>
}

@code{
    // data sources
    List<Category> Categories { get; set; }
    List<Product> AllProducts { get; set; }
    List<Product> CurrentProducts { get; set; }
    List<int> Quantities { get; set; }
    // model
    Order CurrentOrder { get; set; } = new Order();

    string orderStatusMessage { get; set; } // UI related for the sample

    // generate data we will be using in this example
    protected override void OnInitialized()
    {
        base.OnInitialized();

        Categories = Enumerable.Range(1, 6).Select(x => new Category
        {
            CategoryId = x,
            CategoryName = $"Category {x}"
        }).ToList();

        AllProducts = Enumerable.Range(1, 50).Select(x => new Product
        {
            ProductId = x,
            ProductName = $"Product {x}",
            CategoryId = (int)Math.Ceiling((double)x % 7)
        }).ToList();
    }

    //ValueChanged handlers - implementation of cascading dropdowns
    void CategorySelected(int? category)
    {
        if (category == null) // the default value - the user selected the default item == deselected the current item
        {
            //reset the "form" / process
            CurrentOrder = new Order();
            return;
        }

        // cascade the selection by filtering the data for the next dropdown
        CurrentProducts = AllProducts.Where(p => p.CategoryId == category).ToList();

        // get the selected model from the data source
        Category SelectedCategory = Categories.Where(c => c.CategoryId == category).First();

        // business logic
        CurrentOrder.CategoryId = SelectedCategory.CategoryId;
        CurrentOrder.CategoryName = SelectedCategory.CategoryName;
    }

    void ProductSelected(int? product)
    {
        if (product == null) // the default value - the user selected the default item == deselected the current item
        {
            //reset the "form" / process
            CurrentOrder.ProductId = product;
            CurrentOrder.ProductName = string.Empty;
            CurrentOrder.Quantity = 0;
            return;
        }

        Random rnd = new Random();
        Quantities = Enumerable.Range(1, new Random().Next(5, 10)).ToList();

        Product SelectedProduct = AllProducts.Where(p => p.ProductId == product).First();

        CurrentOrder.ProductId = SelectedProduct.ProductId;
        CurrentOrder.ProductName = SelectedProduct.ProductName;
    }

    // sample notification of success and reseting of the process, data classes
    async Task SendOrder()
    {
        CurrentOrder = new Order();
        orderStatusMessage = "Thank you for your order!";
        await Task.Delay(2000);
        orderStatusMessage = "";
        StateHasChanged();
    }

    public class Category
    {
        public int CategoryId { get; set; }
        public string CategoryName { get; set; }
    }

    public class Product
    {
        public int CategoryId { get; set; }
        public int ProductId { get; set; }
        public string ProductName { get; set; }
    }

    public class Order
    {
        public int? CategoryId { get; set; }
        public string CategoryName { get; set; }
        public int? ProductId { get; set; }
        public string ProductName { get; set; }
        public int Quantity { get; set; }
    }
}
@using System.Collections.ObjectModel

<TelerikMultiSelect Value="@CurrentOrder.Categories" Data="@Categories" Placeholder="Select Categories"
                    TextField="CategoryName" ValueField="CategoryId" Filterable="true"
                    ValueChanged="@( (List<int> c) => CategorySelected(c) )">
</TelerikMultiSelect>

<TelerikMultiSelect Value="@CurrentOrder.Products" Data="@CurrentProducts" Placeholder="Select Products" Filterable="true"
                    TextField="ProductName" ValueField="ProductId" Enabled="@( CurrentOrder.Categories.Count > 0 )"
                    ValueChanged="@( (List<int> p) => ProductSelected(p) )">
</TelerikMultiSelect>

@* This sample has only two dropdowns as even dummy data becomes rather long for a multiselect scenario, even for a demo
    The last item could use @bind-Value instead of a handler, this just showcases the main concept. *@

<TelerikButton Enabled="@( CurrentOrder.Products.Count > 0 )" OnClick="@SendOrder">Send Order</TelerikButton>

@if (CurrentOrder.Products.Count > 0)
{
    <h5>Order Summary</h5>
    <ul>
        @foreach (var item in CurrentOrder.ChosenProducts)
        {
            <li>@item.ProductName from category @item.CategoryId</li>
        }
    </ul>
}
else if (!string.IsNullOrEmpty(orderStatusMessage))
{
    <div class="alert alert-success">@orderStatusMessage</div>
}

@code{
    // data sources
    List<Category> Categories { get; set; }
    List<Product> AllProducts { get; set; }
    ObservableCollection<Product> CurrentProducts { get; set; } = new ObservableCollection<Product>();
    // model
    Order CurrentOrder { get; set; } = new Order();

    string orderStatusMessage { get; set; } // UI related for the sample

    // generate data we will be using in this example
    protected override void OnInitialized()
    {
        base.OnInitialized();

        Categories = Enumerable.Range(1, 6).Select(x => new Category
        {
            CategoryId = x,
            CategoryName = $"Category {x}"
        }).ToList();

        AllProducts = Enumerable.Range(1, 50).Select(x => new Product
        {
            ProductId = x,
            ProductName = $"Product {x}",
            CategoryId = (int)Math.Ceiling((double)x % 7)
        }).ToList();
    }

    //ValueChanged handlers - implementation of cascading dropdowns
    void CategorySelected(List<int> categories)
    {
        if (categories.Count == 0) // the user deselected all
        {
            //reset the "form" / process
            CurrentOrder = new Order();
            return;
        }

        // cascade the selection by filtering the data for the next dropdown
        CurrentProducts.Clear();
        foreach (var item in categories)
        {
            var productForCategory = AllProducts.Where(p => p.CategoryId == item);
            foreach (var p in productForCategory)
            {
                CurrentProducts.Add(p);
            }
        }
        CurrentProducts.OrderBy(p => p.ProductId);


        // get the selected models from the data source and use them
        CurrentOrder.Categories.Clear();
        CurrentOrder.ChosenCategories.Clear();
        foreach (var item in categories)
        {
            Category SelectedCategory = Categories.Where(c => c.CategoryId == item).First();
            CurrentOrder.Categories.Add(item);
            // business logic
            CurrentOrder.ChosenCategories.Add(SelectedCategory);
        }
    }

    void ProductSelected(List<int> products)
    {
        if (products.Count == 0) // the user deselected all
        {
            //reset the "form" / process
            CurrentOrder.Products = new List<int>();
            CurrentOrder.ChosenProducts = new List<Product>();
            return;
        }


        // get the selected models from the data source and use them
        CurrentOrder.Products.Clear();
        CurrentOrder.ChosenProducts.Clear();
        foreach (var item in products)
        {
            Product SelectedProduct = AllProducts.Where(p => p.ProductId == item).First();
            CurrentOrder.Products.Add(item);
            // business logic
            CurrentOrder.ChosenProducts.Add(SelectedProduct);
        }
    }

    // sample notification of success and resetting of the process, data classes
    async Task SendOrder()
    {
        CurrentOrder = new Order();
        orderStatusMessage = "Thank you for your order!";
        await Task.Delay(2000);
        orderStatusMessage = "";
        StateHasChanged();
    }

    public class Category
    {
        public int CategoryId { get; set; }
        public string CategoryName { get; set; }
    }

    public class Product
    {
        public int CategoryId { get; set; }
        public int ProductId { get; set; }
        public string ProductName { get; set; }
    }

    public class Order
    {
        public List<int> Categories { get; set; } = new List<int>();
        public List<int> Products { get; set; } = new List<int>();
        public List<Category> ChosenCategories { get; set; } = new List<Category>();
        public List<Product> ChosenProducts { get; set; } = new List<Product>();
    }
}
In this article
Not finding the help you need? Improve this article