Cascading Dropdowns With Blazor

4.83 (6 votes)

Blazor is an experimental framework introduced by Steve Sanderson of Knockout.js fame (among other things) on the ASP.NET team. The premise of the framework is simple, but potentially game-changing for ASP.NET developers: it enables you to write your client side code in C#. What this means is that rather than having to chase after the latest Javascript-based hotness - Aurelia, React, Angular etc, only to find that they are dependent on learning a whole new load of frameworks, or that they are no longer flavour of the week, you just use the .NET skills that you already have to move your processing to the browser.

As I said, Blazor is experimental, but hopefully it will become an official part of the ASP.NET offer in due course. It relies on a technology called Web Assembly which is available in all modern browsers. You don't actually need to know anything about Web Assembly to use Blazor.

I have been watching Blazor since it was announced that it was being adopted by the ASP.NET team and have been playing with samples on and off since then. I have dabbled a tiny bit with Angular and React, and just like React, Blazor is based on components being the building blocks of UI. Unlike React, you don't use JSX or strings to build your components - you use Razor, just like in MVC or Razor Pages. What could possibly be simpler?

To demonstrate how this works currently - it's changing all the time - I thought I would have a go at implementing that old favourite among UI patterns - Cascading Dropdowns. If you want to play along, you should follow the setup instructions here to get Blazor working in your environment. Once you have done that, you should choose the Blazor (ASP.NET Core hosted) option which will generate a solution consisting of 3 projects: a <solution_name>.Server project that includes a Web API controller; a <solution_name>.Client project that holds the Blazor application and component files; and a <solution_name>.Shared project that holds class files for entities used in both of the other projects.

The Data

This example uses Authors and Books to populate dropdowns. The user will initially be presented with a list of authors, and when one is selected, the dependent dropdown will be populated with a list of books filtered by the selected author. So two classes are required to represent these entities. They are added to the Shared project:

public class Author
{
    public int AuthorId { get; set; }
    public string Name { get; set; }
    public ICollection<Book> Books { get; set; }
}

public class Book
{
    public int BookId { get; set; }
    public string Title { get; set; }
    public Author Author { get; set; }
}

Next we need a way of generating and exposing suitable data.  This is best achieved by adding a Web API controller named BookController to the Controllers folder in the Server project with the following code:

using Blazor.Shared;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;

namespace Blazor.Server.Controllers
{
    [Produces("application/json")]
    [Route("api/book")]
    public class BookController : Controller
    {
        private static readonly List<Author> authors = new List<Author>{
            new Author{
                AuthorId = 1, Name = "Tom Clancy", Books = new List<Book>
                {
                    new Book{BookId = 1, Title = "Sum of all Fears"},
                    new Book{BookId = 2, Title = "Rainbow Six"},
                    new Book{BookId = 3, Title = "Hunt for Red October"}
                }
            },
            new Author{
                AuthorId = 2, Name = "Stephen King", Books = new List<Book>
                {
                    new Book{BookId = 4, Title = "Carrie"},
                    new Book{BookId = 5, Title = "The Stand"},
                    new Book{BookId = 6, Title = "The Black House"},
                    new Book{BookId = 7, Title = "It"}
                }
            },
            new Author{
                AuthorId = 3, Name = "Robert Ludlum", Books = new List<Book>
                {
                    new Book{BookId = 8, Title = "The Bourne Ultimatum"},
                    new Book{BookId = 9, Title = "The Holcroft Covenant"},
                    new Book{BookId = 10, Title = "The Rhineman Exchange"}
                }
            }
        };

        [HttpGet]
        public IEnumerable<Author> Get()
        {
            return authors;
        }
    }
}

The data is hardcoded but it could just as easily come from a database.

The Blazor Component

The UI for displaying the dropdowns will be implemented as a Blazor component. This will be created in the Client project, which looks very similar in structure to a Razor Pages application:

Blazor Client

Components are created as Razor files and added to the Pages folder. Tooling is still being worked on, so the workaround for adding a component at the moment is to add a Razor View. I named mine Books.cshtml. I'll show the finished code for the component first and then explain it in sections:

@using Blazor.Shared
@page "/books"
@inject HttpClient http

<h1>Books</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (authors == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <select id="authors" onchange="@AuthorSelectionChanged">
        <option></option>
        @foreach (var author in authors)
        {
            <option value="@author.AuthorId">@author.Name</option>
        }
    </select>
}
@if (books != null)
{
    <select id="books">
        <option></option>
        @foreach (var book in books)
        {
            <option value="@book.BookId">@book.Title</option>
        }
    </select>
}


@functions {
    
    Author[] authors;
    Book[] books;

    protected override async Task OnInitAsync()
    {
        authors = await http.GetJsonAsync<Author[]>("/api/book");
    }


    void AuthorSelectionChanged(UIChangeEventArgs e)
    {
        if (int.TryParse(e.Value.ToString(), out int id))
        {
            books = authors.First(a => a.AuthorId == id).Books.ToArray();
        }
        else
        {
            books = null;
        }
    }
}

Before explaining the component, it needs to be wired up to the example application. This is achieved by adding another link to the NavMenu component in the Shared folder in the Client project:

<li class="nav-item px-3">
    <NavLink class="nav-link" href="/books">
        <span class="oi oi-book" aria-hidden="true"></span> Books
    </NavLink>
</li>

At the top of the component, a using directive is added to bring the Shared project into scope so that the entities declared in it can be referenced. Next, a route is defined using an @page directive. The syntax for this is very similar to Razor Pages. However, the @page directive is required for a Razor Page, whereas it is only needed in a Blazor Component if the component is to take part in routing. The @inject directive is then used to provide an instance of HttpClient from the dependency injection container which will be used to call the Web API controller.

The middle section is largely plain HTML with a bit of C# embedded using Razor syntax. If you are familiar with Razor, there is nothing daunting here at all. If the authors collection is not null, the first dropdown is displayed, populated with data. If the books collection is not null, the second dropdown is displayed, populated with the books. The only new Blazor thing in this section is the  onchange event handler in the first select element. This is a Blazor event hander, not a standard Javascript event attribute. The method that it points to, AuthorSelectionChanged, is declared in the @functions block which is examined next.

The @functions block is a block of C# code. It is where private fields and methods are placed. It works in the same way as in Razor Pages and ASP.NET Web Pages in the past. In this example, two private fields are declared - an array of Authors and an array of Books. The OnInitAsync() method (and it's synchronous counterpart OnInit() method) are provided by the BlazorComponent class, which this component derives from. They can be overridden in derived classes for processing that takes place after the component has been initialised - a bit like the old OnInit event handler in Web Forms or the document.ready function in jQuery. In this example, an asynchronous call is made to the Web API controller using the injected HttpClient instance to obtain the collection of authors. The resulting reponse is assigned to the authors field, which results in the dropdown of authors being populated and made visible:

dropdown

Then the event handler for the onclick event on the authors dropdown is defined. The method accepts a UIChangeEventArgs object as a parameter, which contains information about the change event that was fired. Specifically, it includes a Value property, which holds the value of the selected option. This is parsed into an int and then used to filter the books according to the selected author. The filtered books are assigned to the books dropdown which is then displayed:

dropdown 2

So what does this look like in the browser? Here's a peek at the Network tab in Chrome:

network

There are actual .NET dll files there. They are used by a special version of the NET runtime being developed by the Mono team to work with Web Assembly in the browser. The total download size in this example is 1.8Mb, but the team working on Blazor will be looking to optimise this kind of thing before it ever gets to a stage where the framework can be used in production.

Summary

Blazor is extremely promising. The team stress that it is an unsupported experimental web framework and that it should not be relied on for production use. There is a lot of work to be done but they seem to be committed to doing it, certainly for the time being. Personally, I'd love to see Blazor released.

Date Posted:
Last Updated:
Posted by:
Total Views to date: 829