Chapter 4. Customizing The User Login Experience

In this chapter, you’re going to build on your understanding of how to authenticate a user in the context of a Blazor WebAssembly application and customize the authentication experience. You’ll see a familiar web client startup configuration pattern and continue to explore a few other areas of the app, such as the registration of client-side services. From there, I’ll take your knowledge of JavaScript interoperability further with a compelling example, using browser native Speech Synthesis. You’ll learn how the app’s header functions and you’ll see a pattern for implementing modal dialogs, as a shared infrastructure within a small base component hierarchy. As part of this, you’ll learn how to write and handle custom events.

A Bit More On Blazor Authentication

When you use the app, your identity is used to uniquely identify you as a user of the app. This is true in most app scenarios, including the defaults for both Blazor hosting models when authentication is configured. A single user can log in from multiple clients to use the Learning Blazor application. When a user is authenticated, meaning that the user has entered their credentials or been redirected through an authentication workflow. These workflows define a series of sequential steps that must be followed precisely, and successfully to yield an authenticated user. Here are the basic steps:

  1. Get an authorization code: Run the /authorize endpoint providing the requested scope, where the user interacts with the framework-provided UI.

  2. Get an access token: When successful, from the authorization code you’ll get a token from the /token endpoint.

  3. Use the token: Use the access token to make requests to the various HTTP Web APIs.

  4. Refresh the token: Tokens expire. They’ll need to refresh the tokens automatically when required.

This can also be visualized, consider the following diagram that shows the Authentication user flow.

Authentication user flow.
Figure 4-1. Authentication user flow.

I’m not going to share how to create an AAD B2C tenant, that’s beyond the scope of this book. Besides, there are plenty of good resources for that sort of thing. For more information, see Microsoft Docs: Create an Azure Active Directory B2C tenant. Just know that a tenant exists, and it contains two app registrations. There’s a WebAssembly Client app configured as a SPA, and an API app configured as a server. It’s rather feature-rich, with the ability to customize the clients’ HTML workflow. As an admin, I configured what user scopes exist and what claims are returned/requested.

During the authentication process, the possible states are represented in authentication states listing later in this chapter.

The user is represented as a series of key/value pairs, called “claims”. The keys are named and fairly well standardized. The values are stored, maintained, and retrieved from the trusted third-party entity — also known as “Authentication Providers”, think Google, GitHub, Facebook, Microsoft, and Twitter.

Client-side Custom Authorization Message Handler Implementation

The Learning Blazor app defines a custom implementation of the AuthorizationMessageHandler. In a Blazor WebAssembly app, you can attach tokens to outgoing requests using the framework-provided AuthorizationMessageHandler type. Let’s take a look at the ApiAccessAuthorizationMessageHandler.cs C# file for its implementation:

namespace Learning.Blazor.Handlers;

public sealed class ApiAccessAuthorizationMessageHandler 1
    : AuthorizationMessageHandler
{
    2
    public ApiAccessAuthorizationMessageHandler(
        IAccessTokenProvider provider,
        NavigationManager navigation,      3
        IOptions<WebApiOptions> options) : base(provider, navigation) =>
        ConfigureHandler(
            authorizedUrls: new[]
            {
                options.Value.WebApiServerUrl,
                options.Value.PwnedWebApiServerUrl,
                "https://learningblazor.b2clogin.com"
            },
            scopes: new[] { AzureAuthenticationTenant.ScopeUrl }); 4
}
1

The ApiAccessAuthorizationMessageHandler is a sealed class.

2

Its constructor takes an IAccessTokenProvider, NavigationManager, and IOptions<WebApiOptions> parameters.

3

The base constructor takes a IAccessTokenProvider and a NavigationManager.

4

The ConfigureHandler method is called by the constructor, setting the authorizedUrls and scopes properties.

The framework exposes an AuthorizationMessageHandler. It can be registered as an HttpClient instance HTTP message handler, ensuring that access tokens are appended to outgoing HTTP requests.

The implementation will need the configured IOptions<WebApiOptions> abstraction. This code is requesting the dependency injection service provider to resolve a strongly-typed configuration object.

Subclasses should use the base class’s ConfigureHandler method to configure themselves. The authorizedUrls array is assigned given the Web API and Pwned Web API servers’ URLs. This implementation essentially takes a few configured URLs and sets them as the allow-listed URLs. It also configures an app-specific scope URL, which is set as the handlers scopes argument to the ConfigureHandler function. This handler can then be added to an IHttpClientBuilder instance using the AddHttpMessageHandler<ApiAccessAuthorizationMessageHandler> fluent API call, where you map and configure an HttpClient for dependency injection. This is shown later “The Web.Client ConfigureServices Functionality”. All of the HTTP requests made from the configured HttpClient instance will append the appropriate Authorization header with the short-lived access token.

With C# 10’s constant interpolated strings, the tenant host and public app identifier are formatted along with the API requesting scope. This is a const value defined in a class named AzureAuthenticationTenant, as shown in the following AzureAuthenticationTenant.cs C# file:

namespace Learning.Blazor;

static class AzureAuthenticationTenant
{
    const string TenantHost =
        "https://learningblazor.onmicrosoft.com";

    const string TenantPublicAppId =
        "ee8868e7-73ad-41f1-88b4-dc698429c8d4";

    /// <summary>
    /// Gets a formatted string value
    /// that represents the scope URL:
    /// <c>{tenant-host}/{app-id}/User.ApiAccess</c>.
    /// </summary>
    internal const string ScopeUrl =
        $"{TenantHost}/{TenantPublicAppId}/User.ApiAccess";
}

The class is defined as static, as I do not intend to let developers create an instance of my object. The object exposes a single const string value named ScopeUrl. The first const string is the TenantHost. The second const string is the public application identifier (App Id), or TenantPublicAppId. The ScopeUrl value is formatted as the host and App Id, with an ending segment representing the scope specifier "User.ApiAccess".

This is just a utilitarian static class, and it’s a welcome alternative to having a hardcoded URL in the source. This approach is preferable with each segment of the fully qualified URL specified as a name identifier. These named values are to represent the Learning Blazor Azure business-to-consumer (B2C) user scope. This configuration is handled in the section “The Web.Client ConfigureServices Functionality”. Next, we’ll cover the customization of the client authorization user experience (UX).

Customizing The Client’s Authorization Experience

The client-side configuration will handle setting up the client’s front-end Blazor code to depend on specific services, clients, and authenticated endpoints. The user experiences an authentication flow, and while parts of that flow are configurable from Azure Active Directory (AAD) business-to-consumer (B2C), we’re also able to manage what the user experiences leading up to and returning from various states of the authentication flow. This is possible with the "/authentication/{action}" page’s route template, and this belongs to the Authentication.razor markup:

@page "/authentication/{action}"
@inherits LocalizableComponentBase<Authentication>

<div class="is-size-3">
    <RemoteAuthenticatorView 1
        Action=@Action
        LogOut=@LocalizedLogOutFragment
        LogOutSucceeded=@LocalizedLoggedOutFragment
        LogOutFailed=@LocalizedLogOutFailedFragment
        LogInFailed=@LocalizedLogInFailedFragment>

        <LoggingIn> 2
            <LoadingIndicator Message=@Localizer["CheckingLoginState"]
                              HideLogo="true" />
        </LoggingIn>
        <CompletingLogOut>
            <LoadingIndicator Message=@Localizer["ProcessingLogoutCallback"]
                              HideLogo="true" />
        </CompletingLogOut>
        <CompletingLoggingIn>
            <LoadingIndicator Message=@Localizer["CompletingLogin"]
                              HideLogo="true" />
        </CompletingLoggingIn>

    </RemoteAuthenticatorView>
</div>
1

The Authentication page renders a RemoteAuthenticatorView component.

2

Several component templates exist to render different fragments of the authentication flow.

Like most of the app’s components, the Authentication page is a component that also @inherits LocalizableComponentBase. This component is considered a page since it defines an @page "/authentication/{action}" directive. This will be the component that is rendered when the client-side routing handles a navigation event in response to the browser’s URL requests /authentication/{action} route where the {action} corresponds to the state of the remote authentication flow.

The component markup wraps the framework-provided RemoteAuthenticatorView component with a single div and class attribute to control the overall layout.

The RemoteAuthenticatorView component itself is where the customization capability comes from. This component exposes templated render fragment parameters. It is with this capability that you can provide a custom experience for the following authentication flow states:

  • LogOut: The UI to display while the log out event is being handled.

  • LogOutSucceeded: The UI to display while the log out succeeded event is being handled.

  • LogOutFailed: The UI to display while the log out failed event is being handled.

  • LogInFailed: The UI to display while the log in failed event is being handled.

  • LoggingIn: The UI to display while the logging in event is being handled.

  • CompletingLogOut: The UI to display while the completing log out event is being handled.

  • CompletingLoggingIn: The UI to display while the completing logging in event is being handled.

Since these are all framework-provided RenderFragment types, we can customize what is rendered. We can assign to the RemoteAuthenticatorView component’s parameter properties inline, or using multiple templated-parameter syntaxes. The LoggingIn, CompletingLogOut, and CompletingLoggingIn parameters are assigned to using the markup syntax, where other components can be referenced directly.

These three parameters are assigned given the custom LoadingIndicator component. The LoadingIndicator component conditionally renders the Blazor logo along with the loading indicator message and animated/styled spinning icon. All states of the authentication flow hide the Blazor logo but they could choose to render it by setting the LoadingIndicator.HideLogo parameter to false. Each passes a localized text message to the loading indicator message. These three states are transitional, so when I was designing this approach I determined it best to use messaging that aligns with that expectation.

That’s not to say that you couldn’t just as easily, use humorous nonsense instead. The authentication flow state is only interesting when you’re learning about it the first few times, beyond that we’re all nerds here now — so let’s get creative! We could replace these states with random facts — who doesn’t love hearing something interesting? I’ll leave that to you, send me a pull request and I might just create a community-supported messaging list. The point is that it is entirely customizable. The following list contains the initial states that I’ve configured for the app:

  • LoggingIn relies on the "CheckingLoginState" localized message with the following value: "Reading about the amazing Ada Lovelace (world's first computer programmer)."

  • CompletingLogOut relies on the "ProcessingLogoutCallback" localized message: "Things aren't always as they seem."

  • CompletingLogin relies on the "CompletingLogin" localized message: "Plugging in the random wires lying around."

The Authentication page component’s shadow uses a slightly different technique to satisfy the RenderFragment delegate. Recall that a framework-provided RenderFragment is a void returning delegate type, and it defines a RenderTreeBuilder parameter. With that in mind, consider the Authentication.razor.cs C# file:

using Microsoft.AspNetCore.Components.Rendering; 1

namespace Learning.Blazor.Pages
{
    public sealed partial class Authentication 2
    {
        [Parameter] public string? Action { get; set; } = null!;

        private void LocalizedLogOutFragment( 3
            RenderTreeBuilder builder) =>
            ParagraphElementWithLocalizedContent(
                builder, Localizer, "ProcessingLogout");

        private void LocalizedLoggedOutFragment(
            RenderTreeBuilder builder) =>
            ParagraphElementWithLocalizedContent(
                builder, Localizer, "YouAreLoggedOut");

        private RenderFragment LocalizedLogInFailedFragment( 4
            string errorMessage) =>
            ParagraphElementWithLocalizedErrorContent(
                errorMessage, Localizer, "ErrorLoggingInFormat");

        private RenderFragment LocalizedLogOutFailedFragment(
            string errorMessage) =>
            ParagraphElementWithLocalizedErrorContent(
                errorMessage, Localizer, "ErrorLoggingOutFormat");

        private static void ParagraphElementWithLocalizedContent( 5
            RenderTreeBuilder builder,
            CoalescingStringLocalizer<Authentication> localizer,
            string resourceKey)
        {
            builder.OpenElement(0, "p");
            builder.AddContent(1, localizer[resourceKey]);
            builder.CloseElement();
        }

        6
        private static RenderFragment ParagraphElementWithLocalizedErrorContent(
            string errorMessage,
            CoalescingStringLocalizer<Authentication> localizer,
            string resourceKey) =>
            builder =>
            {
                builder.OpenElement(0, "p");
                builder.AddContent(1, localizer[resourceKey, errorMessage]);
                builder.CloseElement();
            };
    }
1

The component uses the Rendering namespace to consume RenderTreeBuilder and RenderFragment types.

2

Authentication page has several states.

3

Each method either satisfies the RenderFragment delegate signature or returns a RenderFragment type.

4

A localized message is rendered when the authentication flow state has failed to log in.

5

The ParagraphElementWithLocalizedContent method create a p element with a localized message.

6

ParagraphElementWithLocalizedErrorContent method differs by accepting a formattable error message.

The RenderFragment, RenderFragment<T>, and RenderTreeBuilder types were first discussed in the framework-provided RenderFragment type section of chapter two and are part of Microsoft.AspNetCore.Components.Rendering namespace, while the Authentication page component is in Learning.Blazor.Pages.

The Authentication page component is opaque in that it defines a string property named Action, and binds it to the framework-provided RemoteAuthenticatorView.Action property of the same name. This component is also a partial class, serving as the markup’s shadow with code-behind.

The LocalizedLogOutFragment method is private, however; the partial class markup component has access to it. This method is assigned to the rendering responsibility when the client browser has finished handling the log out authentication flow. Its parameter is the RenderTreeBuilder builder instance. The builder is immediately passed to the ParagraphElementWithLocalizedContent method, along with the Localizer, and a const string value of "ProcessingLogout". This pattern is repeated for the LocalizedLoggedOutFragment method delegating to the same helper function, changing only the third parameter to "YouAreLoggedOut". These two methods are void returning and RenderTreeBuilder parameter-accepting. This means that they match the RenderFragment delegate expected signature.

For education, I’ll show a few more ways to customize using a slightly different approach. Notice that the LocalizedLogInFailedFragment is not void returning, nor are they RenderTreeBuilder parameter-accepting. Instead, this method returns a RenderFragment and accepts a string. This is possible as there are two RenderFragment delegates:

  • delegate void RenderFragment(RenderTreeBuilder builder);

  • delegate RenderFragment RenderFragment<TValue>(TValue value);

The ParagraphElementWithLocalizedContent method uses the RenderTreeBuilder builder, CoalescingStringLocalizer<Authentication> localizer, and string resourceKey parameters. Using the builder an opening <p> HTML element is built. Content is added given the value of the localizer[resourceKey] evaluation. Finally, the closing </p> HTML element is built. This method is being used by the log out and logged out authentication flow events:

  • "ProcessingLogout" renders the “If you’re not changing the world, you’re standing still.” message.

  • "YouAreLoggedOut" renders the “Bye for now!” message.

The ParagraphElementWithLocalizedErrorContent method is very similar to the ParagraphElementWithLocalizedContent method in that it defines identical parameters, but return different things. In this case the generic RenderFragment<string> delegate type is inferred, even though the RenderFragment delegate type is explicitly returned. This method is being used by the log in failed and log out failed authentication flow events:

  • When login fails, display a formatted message of: "There was an error trying to log you in: '{0}'".

  • When logout fails, display a formatted message of: "There was an error trying to log you out: '{0}'".

The {0} values within the message formats are used as placeholders for the raw and untranslated error messages.

The Web.Client ConfigureServices Functionality

You should recall the common nomenclature of the top-level WebAssembly app entry point, a C# top-level program. This was initially shown in Link to Come and covered the ConfigureServices extension method. We didn’t discuss the specifics of the client-side service registration. A majority of that work happens in the WebAssemblyHostBuilderExtensions.cs C# file:

namespace Learning.Blazor.Extensions;

internal static class WebAssemblyHostBuilderExtensions
{
    internal static WebAssemblyHostBuilder ConfigureServices(
        this WebAssemblyHostBuilder builder)
    {
        var (services, configuration) = 1
            (builder.Services, builder.Configuration);

        services.AddMemoryCache();
        services.AddScoped<ApiAccessAuthorizationMessageHandler>();
        services.Configure<WebApiOptions>(
            configuration.GetSection(nameof(WebApiOptions)));

        static WebApiOptions? GetWebApiOptions(
            IServiceProvider serviceProvider) =>
            serviceProvider.GetService<IOptions<WebApiOptions>>()
                ?.Value;

        var addHttpClient = 2
            static IHttpClientBuilder (
                IServiceCollection services, string httpClientName,
                Func<WebApiOptions?, string?> webApiOptionsUrlFactory) =>
                services.AddHttpClient(
                    httpClientName, (serviceProvider, client) =>
            {
                var options = GetWebApiOptions(serviceProvider);
                var apiUrl = webApiOptionsUrlFactory(options);
                if (apiUrl is { Length: > 0 })
                    client.BaseAddress = new Uri(apiUrl);

                var cultureService =
                    serviceProvider.GetRequiredService<CultureService>();

                client.DefaultRequestHeaders.AcceptLanguage.ParseAdd(
                    cultureService.CurrentCulture.TwoLetterISOLanguageName);
            })
            .AddHttpMessageHandler<ApiAccessAuthorizationMessageHandler>();

        _ = addHttpClient(
            services, HttpClientNames.ServerApi,
            options => options?.WebApiServerUrl);
        _ = addHttpClient(
            services, HttpClientNames.PwnedServerApi,
            options => options?.PwnedWebApiServerUrl);
        _ = addHttpClient(
            services, HttpClientNames.WebFunctionsApi,
            options => options?.WebFunctionsUrl ??
                builder.HostEnvironment.BaseAddress);

        services.AddScoped<WeatherFunctionsClientService>();
        services.AddScoped( 3
            sp => sp.GetRequiredService<IHttpClientFactory>()
                .CreateClient(HttpClientNames.ServerApi));
        services.AddLocalization();
        services.AddMsalAuthentication(
            options =>
            {
                configuration.Bind(
                    "AzureAdB2C", options.ProviderOptions.Authentication);
                options.ProviderOptions.LoginMode = "redirect";
                var add = options.ProviderOptions.DefaultAccessTokenScopes.Add;

                add("openid");
                add("offline_access");
                add(AzureAuthenticationTenant.ScopeUrl);
            });
        services.AddOptions();
        services.AddAuthorizationCore();
        services.AddSingleton<SharedHubConnection>();
        services.AddSingleton<AppInMemoryState>();
        services.AddSingleton<CultureService>();
        services.AddSingleton(typeof(CoalescingStringLocalizer<>));
        services.AddScoped
            <IWeatherStringFormatterService, WeatherStringFormatterService>();
        services.AddScoped<GeoLocationService>();
        services.AddHttpClient<GeoLocationService>(client =>
        {
            var apiHost = "https://api.bigdatacloud.net"; 4
            var reverseGeocodeClientRoute = "data/reverse-geocode-client";
            client.BaseAddress =
                new Uri($"{apiHost}/{reverseGeocodeClientRoute}");
            client.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip");
        });
        services.AddJokeServices();
        services.AddLocalStorageServices();
        services.AddSpeechRecognitionServices();

        return builder;
    }
}
1

The (IServiceCollection services, IConfiguration configuration) tuple is being used to capture the services and configuration as locals.

2

A static local function addHttpClient is defined.

3

The IHttpClientFactory is being added as a singleton.

4

The geolocation API has its HttpClient configured.

The file-scoped namespace is Learning.Blazor.Extensions which shares all extension’s functionality for the client code. The extensions class is internal and like all extensions classes, it is required to be static. The ConfigureServices method is named this way as it might seem familiar to ASP.NET Core developers who were accustomed to startup conventions, but it doesn’t have to be named this way. To allow for method chaining, this extension method returns the WebAssemblyHostBuilder object that it extends.

Declare and assign the services and configuration objects from the builder. Then it’s off to the races, we add the scoped aforementioned ApiAccessAuthorizationMessageHandler as a service. The WebApiOptions instance is configured, essentially binding them from the resolved configuration instance’s WebApiOptions object. There is a static local function named GetWebApiOptions that returns a questionable WebApiOptions object given an IServiceProvider instance.

To avoid duplicating code, the addHttpClient is a static local function that encapsulates the adding and configuring of an HTTP client. It returns an IHttpClientBuilder instance given the services, an httpClientName, and a function that acts as a factory. The function is named webApiOptionsUrlFactory and it returns a nullable string given the configured options object. The lambda expression delegates to the AddHttpClient extension method on the IServiceCollection type. This configures the HTTP client base address, from the configured URL. It also sets the "Accept-Language" default request header to the currently configured CultureService instance’s ISO 639-1 two-letter code. There are two calls to this addHttpClient expression, setting up the Web API server endpoint and the “Have I Been Pwned” server endpoint.

A few additional services are added, and the Microsoft Authentication Library (MSAL) services are configured and bound to the "AzureAdB2C" section of the configuration instance. The LoginMode is assigned to "redirect", which causes the app to redirect the user to AAD B2C to complete sign-in. Another example of the improvements to lambda expressions is how we declare and assign a variable named add, which delegates to the default access token scopes Add to the collection method. It expects a string and is void returning. The add variable is then invoked three times, adding the "openid", "offline_access" and ScopeUrl scopes. Many of the remaining services are then registered.

An HttpClient is added and configured, which will be used when DI resolves the GeoLocationService. The big data cloud, API host, and route are used as the base address for the client. The additional dependencies are then registered, which include the Joke Services and Local Storage packages. An IJSInProcessRuntime is registered as a single instance, resolved by a cast from the IJSRuntime. This is only possible with Blazor WebAssembly. This is discussed in much more detail in Chapter 7. Finally, the builder is returned completing the fluent ConfigureServices API.

This single extension method is the code that is responsible for configuring the dependency injection of the client-side app. You will have noticed that the HTTP message handler was configured for the HttpClient instances that will forward the bearer tokens on behalf of the client from the ApiAccessAuthorizationMessageHandler. This is important, as not all API endpoints require an authenticated user but those that do will only be accessible when correctly configured this way.

Native Speech Synthesis

You’ve seen how to register all the client-side services for dependency injection, and how to consume registered services in components. In the previous chapter, you saw how the home page renders its tiled content. If you recall, each tile had some markup that included the AdditiveSpeechComponent. While I showed you how to consume this component, I didn’t yet expand upon how it works. Any component that attaches to the AdditiveSpeechComponent will be able to use a native speech synthesis service. Clicking the audio button will trigger the speech synthesis service to speak the text of the tile as shown in Figure 4-2.

Home page tiles
Figure 4-2. Home page tiles.

The AdditiveSpeechComponent exposes a single Message parameter. The consuming components reference this component and assign a message. Consider the AdditiveSpeechComponent.razor markup file:

@inherits LocalizableComponentBase<AdditiveSpeechComponent>

<div class="is-top-right-overlay">
    <button class="button is-rounded is-theme-aware-button p-4 @_dynamicCSS"
        disabled=@_isSpeaking @onclick=OnSpeakButtonClickAsync>
        <span class="icon is-small">
            <i class="fas fa-volume-up"></i>
        </span>
    </button>
</div>

The AdditiveSpeechComponent inherits the LocalizableComponentBase to use three common services which are injected into the base class. The AppInMemoryState, CultureService, and IJSRuntime services are common enough to warrant this inheritance.

The markup is a div element with a descriptive class attribute, which overlays the element in the top-right hand corner of the consuming component. The div element is a parent to a rounded and theme-aware button with a bit of dynamic CSS. The button itself is disabled when the _isSpeaking bit evaluates as true. This is the first component markup we’re covering that shows Blazor event handling. When the user clicks on the button, the OnSpeakButtonClickAsync event handler is called.

You can specify event handlers for all valid Document Object Model (DOM) events. The syntax follows a very specific pattern @on{EventName}={EventHandler}. This syntax is applied as an element attribute, where:

  • The {EventName} is the DOM event name.

  • The {EventHandler} is the name of the method that will handle the event.

For example, @onclick=OnSpeakButtonClickAsync assigns the OnSpeakButtonClickAsync event handler to the click event of the element, in other words when the click is fired, it calls OnSpeakButtonClickAsync.

The OnSpeakButtonClickAsync method is defined in the component shadow, and it is Task returning. This means that in addition to synchronous event handlers, asynchronous event handlers are fully supported. With Blazor event handlers, changes to the UI are automatically triggered, thus you will not have to manually call StateHasChanged to signal re-rendering. The AdditiveSpeechComponent.razor.cs C# file looks like this:

namespace Learning.Blazor.Components
{
    public partial class AdditiveSpeechComponent
    {
        1
        private bool _isSpeaking = false;
        private string _dynamicCSS
        {
            get
            {
                return string.Join(" ", GetStyles()).Trim();

                IEnumerable<string> GetStyles()
                {
                    if (string.IsNullOrWhiteSpace(Message))
                        yield return "is-hidden";

                    if (_isSpeaking)
                        yield return "is-flashing";
                };
            }
        }

        [Parameter]
        public string? Message { get; set; } = null!;

        async Task OnSpeakButtonClickAsync() 2
        {
            if (Message is null or { Length: 0 })
            {
                return;
            }

            var (voice, voiceSpeed) = AppState.ClientVoicePreference;
            var bcp47Tag = Culture.CurrentCulture.Name;

            _isSpeaking = true;

            await JavaScript.SpeakMessageAsync(
                this,
                nameof(OnSpokenAsync),
                Message,
                voice,
                voiceSpeed,
                bcp47Tag);
        }

        [JSInvokable]
        public Task OnSpokenAsync(double elapsedTimeInMilliseconds) => 3
            InvokeAsync(() =>
            {
                _isSpeaking = false;

                Logger.LogInformation(
                    "Spoke utterance in {ElapsedTime} milliseconds",
                    elapsedTimeInMilliseconds);

                StateHasChanged();
            });
    }
}
1

The AdditiveSpeechComponent maintains several bits of component state.

2

The OnSpeakButtonClickAsync method conditionally speaks a message.

3

The OnSpokenAsync method is called after the message has been spoken.

The class has an _isSpeaking field, that defaults to false. This value is used to determine how to render the <button>. The _dynamicCSS property only has a get accessor, which makes it read-only. It determines the styles applied to the <button>. The Message property is a Parameter, which is what allows it to be assigned from consuming components.

The event handler that was assigned to handle the button’s click event is the OnSpeakButtonClickAsync method. When there is a meaningful value from the Message, this handler gets the voice and voiceSpeed from the in-memory app state service, as well as the Best Current Practices (BCP 47) language tag value from the current culture. The _isSpeaking bit is set to true, and a call to JavaScript.SpeakMessageAsync is awaited given this component, the name of the OnSpokenAsync callback, the Message, voice, voiceSpeed, and bcp47Tag. This pattern might start looking a bit familiar, as much or as little as your app needs to rely on native functionality from the browser it can use JavaScript interop.

The OnSpokenAsync method is declared as JSInvokable. Since this callback happens asynchronously and at an undetermined time, the component couldn’t know when to re-render, so you must tell it to with StateHasChanged.

Tip

Anytime you define a method that is JSInvokable that alters the state of the component, you must call StateHasChanged to signal a re-render.

The OnSpokenAsync handler is expressed as InvokeAsync, which executes the given work item on the renders synchronization context. It sets _isSpeaking to false, logs the total amount of time the message was spoken and then it notifies the component that its state has changed.

The markup is minimal and the code behind is clean but very powerful. Let’s lean into the JSRuntimeExtensions.cs C# file to see what the SpeakMessageAsync looks like:

namespace Learning.Blazor.Extensions;

internal static partial class JSRuntimeExtensions
{
    internal static ValueTask SpeakMessageAsync<T>(
        this IJSRuntime jsRuntime,
        T dotnetObj,
        string callbackMethodName,
        string message,
        string defaultVoice,
        double voiceSpeed,
        string lang) where T : class =>
        jsRuntime.InvokeVoidAsync(
            "app.speak",
            DotNetObjectReference.Create(dotnetObj),
            callbackMethodName, message, defaultVoice, voiceSpeed, lang);
}

Extending the IJSRuntime functionality with meaningful names makes me happy. I find joy in these small victories, but it does make for a more enjoyable experience when reading the code. Being able to read it as JavaScript.SpeakMessageAsync is very self-descriptive. This extension method delegates to the IJSRuntime.InvokeVoidAsync method, calling "app.speak" given the DotNetObjectReference, the callback method name, a message, voice, voice speed, and language. I could have called InvokeVoidAsync directly from the component, but I instead prefer the descriptive method name of the extension method. This is the pattern that I recommend, as it helps to encapsulate the logic and it’s easier to consume from multiple call points. The JavaScript code that this extension method relies on is part of the wwwroot/js/app.js file:

const cancelPendingSpeech = () => { 1
    if (window.speechSynthesis
    && window.speechSynthesis.pending === true) {
        window.speechSynthesis.cancel();
    }
};

const speak = (dotnetObj, callbackMethodName, message, 2
               defaultVoice, voiceSpeed, lang) => {
    const utterance = new SpeechSynthesisUtterance(message);
    utterance.onend = e => {
        if (dotnetObj) {
            dotnetObj.invokeMethodAsync(callbackMethodName, e.elapsedTime)
        }
    };

    const voices = window.speechSynthesis.getVoices(); 3
    try {
        utterance.voice =
            !!defaultVoice && defaultVoice !== 'Auto'
                ? voices.find(v => v.name === defaultVoice)
                : voices.find(v => !!lang &&
                    v.lang.startsWith(lang)) || voices[0];
    } catch { }
    utterance.volume = 1;
    utterance.rate = voiceSpeed || 1;

    window.speechSynthesis.speak(utterance); 4
};

window.app = Object.assign({}, window.app, {
    speak,
    // omitted for brevity...
});

// Prevent the client from speaking when the user closes the tab or window.
window.addEventListener('beforeunload', _ => { 5
    cancelPendingSpeech();
});
1

As a safety net to avoid the browser from speaking when the user closes the tab or window, the cancelPendingSpeech method is defined.

2

The speak function creates and prepares an utterance instance for usage.

3

The utterance.voice property is set to the voices array, filtered by the defaultVoice and lang parameters.

4

The utterance is passed to the speechSynthesis.speak method.

5

The beforeunload event handler is defined to cancel any pending speech.

The cancelPendingSpeech function checks if the window.speechSynthesis object is truthy (in this case meaning it’s not null or undefined). If there are any pending utterances in the queue, a call to window.speechSynthesis.cancel() is made — removing all utterances from the queue.

The "app.speak" method is defined as the function named speak. It has six parameters, which feels like too many. You could choose to parameterize this with a single top-level object if you’d like, but that would require a new model and additional serialization. I’d probably limit a parameter list to no more than six, but as with everything in programming, there are tradeoffs. The speak method body starts by instantiating a SpeechSynthesisUtterance given the message. This object exposes an end/onend event that is fired when the utterance has finished being spoken. An inline event handler is assigned, which relies on the given dotnetObj instance and the callbackMethodName. When the utterance is done being spoken, the event fires and calls back onto the calling components given method.

An attempt to assign the desired voice to speak the utterance is made. This can be problematic, and error-prone — as such its attempt is fragile and protected with a try / catch. If it works, great, if not, it’s not a big deal as the browser will select the default voice. The volume is to 1 and the speed at which the utterance is spoken is set as well.

With an utterance instance prepared, a call to window.speechSynthesis.speak(utterance) is made. This will enqueue the utterance into the native speech synthesis queue. When the utterance reaches the end of the queue, it is spoken. The "app.speak" name comes from how the speak function const is added to either a new instance of app or the existing one.

If a long utterance is being spoken, and the user closes the app’s browser tab or window, but leaves the browser open — the utterance will continue to be spoken. To avoid this behavior, we’ll call cancelPendingSpeech when the window is unloaded.

The AdditiveSpeechComponent could be bundled into a separate Razor component project, and distributed to consuming apps. That approach is very beneficial as it exposes functionality and shares it with consumers. All of the functionality of this component is encapsulated and could benefit from being shared via NuGet. At the time of writing, the component remained as part of the Web.Client project but that’s not to say that this couldn’t easily evolve in complexity, or add new functionality. Once on NuGet it could be used by other .NET developers who consume open-source projects.

The Learning Blazor sample app demonstrates how to create Razor projects and consume them from the Blazor web client. The Web.Client project depends on the Web.TwitterComponents Razor class library. The Web.TwitterComponents project encapsulates a few Twitter-specific components. The Web.Client consumes these components and exposes them to the Blazor web client.

Sharing And Consuming Custom Components

To consume a component, you reference it from a consuming component’s markup. Blazor provides many components out-of-the-box, from layouts to navigation, from standard form controls to error boundaries, from page titles to head outlets, and so on. See, ASP.NET Core built-in Razor components for a listing of the available components.

When the built-in components are not enough, you can turn to custom components. There are many other vendor-provided components. Additionally, there is a massive open-source community that builds component libraries as well. Chances are you’ll find what you need as a developer when building Blazor apps from all the vendor-provided component libraries out there, consider the following list of vendor resources:

There is a community-curated list on GitHub known as Awesome Blazor which is another great resource. Sometimes, you may require functionality that isn’t available from the framework, from vendors, or even from the community at large. When this happens, you can write your component libraries.

Since Blazor is built atop Razor, all of the components are Razor components. They’re easily identifiable by their .razor file extension.

Chrome: The overloaded term

With graphical user interface (GUI) apps, there is an old term that’s been overloaded through the years. The term “chrome” refers to an element of the user interface that displays the various commands or capabilities available to the user. For example, the chrome of the Learning Blazor sample app is the top bar. This contains the app’s top-level navigation, theme display icon, and the buttons for various popup modal components such as the notification toggle, task list toggle, and the log in/out button. This was shown in The header and footer components figures from chapter two. When I refer to chrome, I’m not talking about the web browser here. We’ve already discussed navigation and routing a bit, so let’s focus on modal modularity.

Modal Modularity And Blazor Component Hierarchies

Most apps need to interact with the user and prompt them for input. The app’s navigation is a user experience and one said example of user input — is the user clicks a link to a route they want to visit, then the app takes an action. Sometimes we’ll need to prompt the user to use the keyboard, instead of the mouse. The questions we ask users vary primarily by domain, for example; “What’s your email address?” or “What’s your message to send?” Answers vary by control type, meaning free-form text line or text area, or a checkbox, or select list, or a button. All of this is fully supported with Blazor, you can subscribe to native HTML element events and handle them in Razor C# component logic. There are native forms of integration and modal/input binding validation, templating, and component hierarchies.

One such control is a custom control, named ModalComponent. This component is going to be used throughout the app for various use cases. It’s will have an inherited component to exemplify component subclass patterns, which are common in C#, but were under-utilized as a programming pattern for JavaScript SPAs. Consider the ModalComponent.razor markup file:

<div class="modal has-text-left @_isActiveClass"> 1
    <div class="modal-background" @onclick=@CancelAsync></div>
    <div class="modal-card">
        <header class="modal-card-head">
            <p class="modal-card-title">
                @TitleContent 2
            </p>
            <button class="delete" aria-label="close" @onclick=@CancelAsync></button>
        </header>

        <section class="modal-card-body">
            @BodyContent 3
        </section>

        <footer class="modal-card-foot is-justify-content-flex-end">
            <div>
                @ButtonContent 4
            </div>
        </footer>
    </div>
</div>
1

The outermost element is a div with the modal class.

2

The title is represented as a header element with the modal-card-title class.

3

The body is a section with the modal-card-body class.

4

The footer is styled with the modal-card-foot class.

The HTML is a modal styled div with an _isActiveClass value bound to the modal’s class attribute. Meaning that the state of the modal, whether it is active (shown) or not is dependent on a component variable. It has a background style that applies an overlay, making this element popup as a modal dialog displayed to the user. The background div element itself handles user clicks by calling CancelAsync and covers the entire page.

The HTML is semantically accurate; representing an industry-standardized three-part header/body/footer layout. The first template placeholder is the @TitleContent. This is a required RenderFragment which allows for the consuming component to provide custom title markup. The header also contains a button which will call CancelAsync when clicked.

The BodyContent is styled appropriately as a modal’s body, which is a section HTML element and semantically positioned beneath the header and above the footer.

The modal footer contains the required ButtonContent markup. Collectively, this modal represents a common dialog component where consumers can plug in their customized markup and corresponding prompts.

The component shadow defines the component’s parameter properties, events, component state, and functionality. Consider the ModalComponent.razor.cs C# file:

namespace Learning.Blazor.Components; 1

public partial class ModalComponent
{
    private string _isActiveClass => IsActive ? "is-active" : "";

    [Parameter]
    public EventCallback<DismissalReason> Dismissed { get; set; } 2

    [Parameter]
    public bool IsActive { get; set; }

    [Parameter, EditorRequired]
    public RenderFragment TitleContent { get; set; } = null!;

    [Parameter, EditorRequired]
    public RenderFragment BodyContent { get; set; } = null!;

    [Parameter, EditorRequired]
    public RenderFragment ButtonContent { get; set; } = null!;

    /// <summary>
    /// Gets the reason that the <see cref="ModalComponent"/> was dismissed.
    /// </summary>
    public DismissalReason Reason { get; private set; }

    /// <summary>
    /// Sets the <see cref="ModalComponent"/> instance's
    /// <see cref="IsActive"/> value to <c>true</c> and
    /// <see cref="Reason"/> value as <c>default</c>.
    /// It then signals for a change of state, this rerender will
    /// show the modal.
    /// </summary>
    public Task ShowAsync() => 3
        InvokeAsync(() => (IsActive, Reason) = (true, default));

    /// <summary>
    /// Sets the <see cref="ModalComponent"/> instance's
    /// <see cref="IsActive"/> value to <c>false</c> and
    /// <see cref="Reason"/> value as given <paramref name="reason"/>
    /// value. It then signals for a change of state,
    /// this rerender will cause the modal to be dismissed.
    /// </summary>
    public Task DismissAsync(DismissalReason reason) =>
        InvokeAsync(async () =>
        {
            (IsActive, Reason) = (false, reason);
            if (Dismissed.HasDelegate)
            {
                await Dismissed.InvokeAsync(Reason);
            }
        });

    /// <summary>
    /// Dismisses the shown modal, the <see cref="Reason"/>
    /// will be set to <see cref="DismissalReason.Confirmed"/>.
    /// </summary>
    public Task ConfirmAsync() => DismissAsync(DismissalReason.Confirmed);

    /// <summary>
    /// Dismisses the shown modal, the <see cref="Reason"/>
    /// will be set to <see cref="DismissalReason.Cancelled"/>.
    /// </summary>
    public Task CancelAsync() => DismissAsync(DismissalReason.Cancelled);

    /// <summary>
    /// Dismisses the shown modal, the <see cref="Reason"/>
    /// will be set to <see cref="DismissalReason.Verified"/>.
    /// </summary>
    public Task VerifyAsync() => DismissAsync(DismissalReason.Verified);
}

public enum DismissalReason 4
{
    Unknown, Confirmed, Cancelled, Verified
};
1

The ModalComponent class is part of the Learning.Blazor.Components namespace.

2

Several properties together, represent examples of required component parameters, events, templates, and component state values.

3

As for the functionality and modularity, the modal component can be shown, and just as easily dismissed.

4

The enum DismissalReason type is defined within the same file-scoped namespace.

Tip

In Blazor when you define a property that is used as a Parameter and you want that parameter to be required, you can use the framework-provided EditorRequired attribute. This specifies that the component parameter is required to be provided by the user when authoring it in the editor. If a value for this parameter is not provided, editors or build tools may provide warnings indicating the user to specify a value.

The ModalComponent class defines several properties:

  • _isActiveClass: A private string which serves as a computed property, which evaluates the IsActive property and returns "is-active" when true. This was bound to the modal’s markup, where the div’s class attribute had some static classes and a dynamically bound value.

  • Dismissed: A component parameter, which is of type EventCallback<DismissalReason>. An event callback accepts delegate assignments from consumers, where events flow from this component to interested recipients.

  • IsActive: A bool value, which represents the current state of whether the modal is actively being displayed to the user. This parameter is not required, and is typically set implicitly from calls to DismissAsync.

  • Three RenderFragment template placeholder objects, for the header title, body content, and footer controls which are buttons; TitleContent, BodyContent, and ButtonContent.

  • Reason: The reason for the dismissal of the modal, is either “unknown”, “confirmed”, “canceled”, or “verified”.

The ModalComponent exposes modularity as the functionality is templated, and consumers have hooks into the component. Consumers can call any of these public Task returning asynchronous operational methods:

  • ShowAsync: Immediately shows the modal to the user. This method is expressed as a call to InvokeAsync given a lambda expression which sets the values of IsActive to true and assigns default to the Reason (or DismissalReason.Unknown). Calling StateHasChanged is unnecessary at this point. Asynchronous operational support will automatically re-render the UI components implicitly as needed.

  • DismissAsync: Given a dismissal reason, immediately dismisses the modal. The IsActive state is set to false which will effectively hide the component from the user.

  • ConfirmAsync: Sets the dismissal reason as Confirmed, delegates to DismissAsync.

  • CancelAsync: Sets the dismissal reason as Cancelled, delegates to DismissAsync.

  • VerifyAsync: Sets the dismissal reason as Verified, delegates to DismissAsync.

The enum DismissalReason type defines four states, Unknown (which is the default), Confirmed, Cancelled (can occur implicitly from the user clicking outside the modal), and Verified. While I will usually place every type definition in its file, I choose to keep the enum DismissalReason within the same file. To me, these are logically cohesive and belong together.

Exploring Blazor Event Binding

The ModalComponent is consumed by the VerificationModalComponent, let’s take a look at how this is achieved in the VerificationModalComponent.razor markup file:

@inherits LocalizableComponentBase<VerificationModalComponent>

<ModalComponent @ref="_modal" Dismissed=@OnDismissed> 1
    <TitleContent> 2
        <span class="icon pr-2">
            <i class="fas fa-robot"></i>
        </span>
        <span>@Localizer["AreYouHuman"]</span>
    </TitleContent>
    <BodyContent> 3
        <form>
            <div class="field">
                <label class="label">@_math.HumanizeQuestion()</label>
                <div class="field-body">
                    <div class="field">
                        <p class="control is-expanded has-icons-left">
                            @{
                                var inputValidityClass =
                                    _answeredCorrectly is false
                                        ? " invalid"
                                        : "";

                                var inputClasses = $"input{inputValidityClass}";
                            }
                            <input @bind="_attemptedAnswer" 4
                                class=@inputClasses
                                type="text"
                                placeholder="@Localizer["AnswerFormat",
                                    _math.GetQuestion()]" />
                            <span class="icon is-small is-left">
                                <i class="fas fa-info-circle"></i>
                            </span>
                        </p>
                    </div>
                </div>
            </div>
        </form>
    </BodyContent>
    <ButtonContent> 5
        <button class="button is-info is-large is-pulled-left" @onclick=Refresh>
            <span class="icon">
                <i class="fas fa-redo"></i>
            </span>
            <span>@Localizer["Refresh"]</span>
        </button>
        <button class="button is-success is-large" @onclick=AttemptToVerify>
            <span class="icon">
                <i class="fas fa-check"></i>
            </span>
            <span>@Localizer["Verify"]</span>
        </button>
    </ButtonContent>
</ModalComponent>
1

The _modal reference wires the OnDismissed event handler.

2

The TitleContent renders a localized prompt message and a robot icon.

3

The BodyContent renders a form with a single input field.

4

The _attemptedAnswer property is bound to the input field’s value attribute.

5

The buttons are rendered in the ButtonContent template.

The VerificationModalComponent markup relies on the ModalComponent, and it captures a reference to the modal using the @ref="_modal" syntax. Blazor will automatically assign the _modal field from the instance value of the referenced component markup. Internal to the VerificationModalComponent the dependent ModalComponent.Dismissed event is handled by the OnDismissed handler. In other words, the ModalComponent.Dismissed is a required parameter and it’s an event that the component will fire. The VerificationModalComponent.OnDismissed event handler is assigned to handle it. This is custom event binding, where the consuming component handles the dependent component’s exposed parameterized event.

The verification modal’s title content (TitleContent) prompts the user with an “Are you human?” message.

The BodyContent markup contains a native HTML form element. Within this markup is a simple label and corresponding text input element. The label splats a question into the markup from the evaluated _math.GetQuestion() invocation (more on the _math object in a bit). The attempted answer input element has dynamic CSS classes bound to it, based on whether the question was correctly answered.

The input element has its value bound to the _attemptedAnswer variable. It also has a placeholder bound from a localized answer format given the math question, which will serve as a clue to the user about what’s expected.

The ButtonContent markup has two buttons, one for refreshing the question (via the Refresh method), and the other for attempting to verify the answer (via the AttemptToVerify method). This is an example of native event binding, where the button elements have their click events bound to the corresponding event handlers.

The ModalComponent itself is a base modal, while the VerificationModalComponent uses the base modal and employs a very specific verification prompt. The VerificationModalComponent will render as shown in Figure 4-3.

An example rendering of the VerificationModalComponent.
Figure 4-3. An example rendering of the VerificationModalComponent.

The component shadow for the VerificationModalComponent resides in the VerificationModalComponent.cs file:

namespace Learning.Blazor.Components
{
    public sealed partial class VerificationModalComponent
    {
        1
        private AreYouHumanMath _math = AreYouHumanMath.CreateNew();
        private ModalComponent _modal = null!;
        private bool? _answeredCorrectly = null!;
        private string? _attemptedAnswer = null!;
        private object? _state = null;

        [Parameter, EditorRequired] 2
        public EventCallback<(bool IsVerified, object? State)>
            OnVerificationAttempted
            {
                get;
                set;
            }

        public Task PromptAsync(object? state = null) 3
        {
            _state = state;
            return _modal.ShowAsync();
        }

        private void Refresh() => 4
            (_math, _attemptedAnswer) = (AreYouHumanMath.CreateNew(), null);

        private async Task OnDismissed(DismissalReason reason) 5
        {
            if (OnVerificationAttempted.HasDelegate)
            {
                await OnVerificationAttempted.InvokeAsync(
                    (reason is DismissalReason.Verified, _state));
            }
        }

        private async Task AttemptToVerify() 6
        {
            if (int.TryParse(_attemptedAnswer, out var attemptedAnswer))
            {
                _answeredCorrectly = _math.IsCorrect(attemptedAnswer);
                if (_answeredCorrectly is true)
                {
                    await _modal.DismissAsync(DismissalReason.Verified);
                }
            }
            else
            {
                _answeredCorrectly = false;
            }
        }
    }
}
1

The VerificationModalComponent wraps the ModalComponent to add a verification layer.

2

An event callback exposes whether the verification attempt was successful.

3

The prompt method delegates to the ModalComponent.ShowAsync method.

4

The Refresh method resets the _math and _attemptedAnswer fields.

5

The OnDismissed event handler is invoked when the modal is dismissed.

6

The AttemptToVerify method dismisses the modal if the answer is correct.

The VerificationModalComponent class defines the following fields:

  • _math: The math object is of type AreYouHumanMath and is assigned from the AreYouHumanMath.CreateNew() factory method. This is a custom type that helps to represent a simple mathematical problem that a human could likely figure out in their head.

  • _modal: The field representing the ModalComponent instance from the corresponding markup. Methods will be called on this instance, such as ShowAsync to display the modal to the user.

  • _answeredCorrectly: The three-state bool is used to determine if the user answered the question correctly.

  • _attemptedAnswer: The nullable string bound to the input element, used to store the user-entered value.

  • _state: A state object that represents an opaque value, stored on behalf of the consumer. When the consuming component calls PromptAsync, if they pass state it’s assigned to the _state variable then given back to the caller when the OnVerificationAttempted event callback is invoked.

The OnVerificationAttempted is a required parameter. The callback signature passes a tuple object, where its first value represents whether the verification attempt was successful. This is true when the user correctly entered the correct answer, otherwise false. The second value is an optional state object.

The PromptAsync method is used to display the modal dialog and accepts an optional state object.

The Refresh method is bound to the refresh button and is called to re-randomize the question being asked. The AreYouHumanMath.CreateNew() factory method is reassigned to the _math field and the _attemptedAnswer is set to null.

The OnDismissed method is the handler for the ModalComponent.Dismissed event callback. When the base modal is dismissed, it will have DismissalReason. With the reason and when the OnVerificationAttempted has a delegate, it’s invoked passing whether it’s verified and any state that was held on to when prompted.

The AttemptToVerify method is bound to the verify button. When called it will attempt to parse the _attemptedAnswer as an int and ask the _math object if the answer is correct. When true, the _modal is dismissed as Verified. This will indirectly call Dismissed.

I bet you’re wondering what the AreYouHumanMath object looks like, it sure was fun writing this cute little object. Take a look at the AreYouHumanMath.cs C# file:

namespace Learning.Blazor.Models;

public readonly record struct AreYouHumanMath( 1
    byte LeftOperand,
    byte RightOperand,
    MathOperator Operator = MathOperator.Addition)
{
    private static readonly Random s_random = Random.Shared;

    /// <summary>
    /// Determines if the given <paramref name="guess"/> value is correct.
    /// </summary>
    /// <param name="guess">The value being evaluated for correctness.</param>
    /// <returns>
    /// <c>true</c> when the given <paramref name="guess"/> is correct,
    /// otherwise <c>false</c>.
    /// </returns>
    /// <exception cref="ArgumentException">
    /// An <see cref="ArgumentException"/> is thrown when
    /// the current <see cref="Operator"/> value is not defined.
    /// </exception>
    public bool IsCorrect(int guess) => guess == Operator switch 2
    {
        MathOperator.Addition => LeftOperand + RightOperand,
        MathOperator.Subtraction => LeftOperand - RightOperand,
        MathOperator.Multiplication => LeftOperand * RightOperand,

        _ => throw new ArgumentException(
            $"The operator is not supported: {Operator}")
    };

    /// <summary>
    /// The string representation of the <see cref="AreYouHumanMath"/> instance.
    /// <code language="cs">
    /// <![CDATA[
    /// var math = new AreYouHumanMath(7, 3);
    /// math.ToString(); // "7 + 3 ="
    /// ]]>
    /// </code>
    /// </summary>
    /// <exception cref="ArgumentException">
    /// An <see cref="ArgumentException"/> is thrown when
    /// the current <see cref="Operator"/> value is not defined.
    /// </exception>
    public override string ToString() 3
    {
        var operatorStr = Operator switch
        {
            MathOperator.Addition => "+",
            MathOperator.Subtraction => "-",
            MathOperator.Multiplication => "*",

            _ => throw new ArgumentException(
                $"The operator is not supported: {Operator}")
        };

        return $"{LeftOperand} {operatorStr} {RightOperand} =";
    }

    public string GetQuestion() => $"{this} ?";

    public static AreYouHumanMath CreateNew( 4
        MathOperator? mathOperator = null)
    {
        var mathOp =
            mathOperator.GetValueOrDefault(RandomOperator());

        var (left, right) = mathOp switch
        {
            MathOperator.Addition => (Next(), Next()),
            MathOperator.Subtraction => (Next(120), Next(120)),
            _ => (Next(30), Next(30)),
        };

        (left, right) = (Math.Max(left, right), Math.Min(left, right));

        return new AreYouHumanMath(
            (byte)left,
            (byte)right,
            mathOp);

        static MathOperator RandomOperator()
        {
            var values = Enum.GetValues<MathOperator>();
            return values[s_random.Next(values.Length)];
        };

        static int Next(byte? maxValue = null) =>
            s_random.Next(1, maxValue ?? byte.MaxValue);
    }
}

public enum MathOperator { Addition, Subtraction, Multiplication }; 5
1

The AreYouHumanMath is a positional record that defines a simple math problem.

2

The ability to test whether a guess is the correct answer is expressed by the IsCorrect method.

3

The ToString method is used to display the math problem.

4

The CreateNew method is used to create a new random math problem.

5

The MathOperator enum defines whether a problem is either addition, subtraction, or multiplication.

The AreYouHumanMath object is a readonly record struct. As such, it’s immutable but allows for with expressions which creates a clone. It’s a positional record, meaning it can only be instantiated using the required parameter constructor. A left and right operand value is required, but the math operator is optional and defaults to addition.

The Random.Shared was introduced with .NET 6, and is used to assign the static readonly Random instance.

The IsCorrect method accepts a guess. This method will return true only when the given guess equals the evaluated math operation of the left and right operand values. For example, new AreYouHumanMath(7, 3).IsCorrect(10) would evaluate as true because seven plus three equals ten. This method is expressed as a switch expression on the Operator. Each operator case arm is expressed as the corresponding math operation.

The ToString and GetQuestion methods return the mathematical representation of the applied operator and two operands. For example, new AreYouHumanMath(7, 3).ToString() would evaluate as "7 + 3 =", whereas new AreYouHumanMath(7, 3).GetQuestion() would be "7 + 3 = ?".

The CreateNew method relies heavily on the Random class to help ensure that each time it’s invoked a new question is asked. When the optional mathOperator is provided it’s used, otherwise, a random one is determined. With an operator the operands are randomly determined, the maximum number is the left operand and the minimum is the right.

As for the enum MathOperator, I intentionally decided to avoid division. With the use of random numbers, it would have been a bit more complex, with concerns of dividing by 0 and precision. Instead, I was hoping for math that you could more than likely do in your head.

The VerificationModalComponent is used as a spam blocker on the Contact.razor page that we’ll discuss in detail. We’ll look at that pattern in Chapter 8. The ModalComponent is also used by the AudioDescriptionComponent and the LanguageSelectionComponent. These two components are immediately to the right of the ThemeIndicatorComponent discussed in “Native Theme Awareness”.

Summary

You learned a lot more about how extensive and configurable Blazor app development is. You have a much better understanding of how to authenticate a user in the context of a Blazor WebAssembly application. I showed you a familiar web client startup configuration pattern where all the client-side services are registered. We customized the authorization user experience. We explored the implementation of browser native speech synthesis. Finally, we read all the markup and C# code for the chrome within the app’s header, and modal dialog hierarchical capabilities. We now have a much better understanding of Blazor event management, firing, and consuming.

In the next chapter, I’m going to show you a pattern for localizing the app in forty different languages. I’ll show you how we use an entirely free GitHub Action combined with Azure Cognitive Services to machine translate resource files on our behalf. You’ll learn exactly how to implement localization, using the framework-provided IStringLocalizer<T> type along with static resource files. You’ll learn various formatting details as well.

Get Learning Blazor now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.