Blazor — Twitch Episode 1 Recap

ASP.NET Team Blog
25 September 2023

Tuesday, September 19th, Amanda and I streamed our first Twitch episode. For all of you who didn’t make it in time, head over to the DevExpress Official channel to replay it.

In this blogpost I will explain what I have created and where to find the code on GitHub so you can check it out or recreate the project yourself.

The File / New Experience

As with any other project template or scaffolding tool, it’s important to understand what has been created and how it works to take full advantage from the generated code.

From the start, there are a couple of things that are interesting to know. First is the ~/Program.cs file which holds the following code:

var builder = WebApplication.CreateBuilder(args);

// ... code omitted for clarity ...

builder.Services.AddDevExpressBlazor(options => {
    options.BootstrapVersion = DevExpress.Blazor.BootstrapVersion.v5;
    options.SizeMode = DevExpress.Blazor.SizeMode.Medium;
});

// ... code omitted for clarity ...

This makes sure the DevExpress Blazor controls will work and we can specify some global options like control size mode and Bootstrap version.

Another interesting file to look at is the ~/Shared/MainLayout.razor file.

It contains two DevExpress controls - DxLayoutBreakpoint and the DxGridLayout. With these two controls it is easy to get the hamburger menu functionality to work.

The DxLayoutBreakpoint control is a non-visible component which has a number of CSS breakpoint constraints and a bindable IsActive property. This allows us to show or hide certain elements depending on the current screen dimensions.

<DxLayoutBreakpoint MaxWidth="1200"
                    @bind-IsActive="@IsMobileLayout" />

@code {    
    private bool _isMobileLayout;
    public bool IsMobileLayout {
        get => _isMobileLayout;
        set {
            _isMobileLayout = value;
            IsSidebarExpanded = !_isMobileLayout;
        }
    }    
    
    // ... code omitted for clarity ...
}

More examples can be found on our Demo Site.

The DxGridLayout is a component that allows you to set up rows and columns in your display. Next, you’ll add controls into its Items collection, which can then be assigned to those rows and columns. Again, we can bind the visible properties to C# properties to make items appear or disappear or even move them from one grid-cell to another depending on current screen dimensions.

<DxGridLayout CssClass="page-layout">
    <Rows>
        @if(IsMobileLayout) {
            <DxGridLayoutRow Areas="header" Height="auto"></DxGridLayoutRow>
            <DxGridLayoutRow Areas="sidebar" Height="auto"></DxGridLayoutRow>
            <DxGridLayoutRow Areas="content" />
        }
        else {
            <DxGridLayoutRow Areas="header header" Height="auto" />
            <DxGridLayoutRow Areas="@(IsSidebarExpanded ? "sidebar content" : "content content")" />
        }
    </Rows>
    <Columns>
        @if(!IsMobileLayout) {
            <DxGridLayoutColumn Width="auto" />
            <DxGridLayoutColumn />
        }
    </Columns>
    <Items>
        <DxGridLayoutItem Area="header" CssClass="layout-item">
            <Template>
                <Header @bind-ToggleOn="@IsSidebarExpanded" />
            </Template>
        </DxGridLayoutItem>
        <DxGridLayoutItem Area="sidebar" CssClass="layout-item">
            <Template>
                <NavMenu StateCssClass="@NavMenuCssClass" />
            </Template>
        </DxGridLayoutItem>
        <DxGridLayoutItem Area="content" CssClass="content px-4 layout-item">
            <Template>
                @Body
            </Template>
        </DxGridLayoutItem>
    </Items>
</DxGridLayout>

More examples can be found on our Demo Site.

Theme Switching

Our controls ship with several professionally-designed themes to give your users the best UI experience. The two I have used are the Blazing Berry and Blazing Berry Dark themes.

If you take a look in the ~/Pages/_Layout.cshtml, on line 11 you will see a link tag with a reference to one of our themes:

<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.css" rel="stylesheet" asp-append-version="true" />

Changing this to

<link href="_content/DevExpress.Blazor.Themes/blazing-dark.bs5.css" rel="stylesheet" asp-append-version="true" />

will make our app use the dark theme.

Another interesting declaration in the head section is:

<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />

This allows us to change markup in the head from other lower-level components which is what we want to do with the ThemeToggler.

The DxThemeToggler Control

Since Blazor is a component-based architecture and I might want to reuse this in other projects, I will create a Razor Class Library. The cool thing with this project is that it can also contain static web assets needed by your controls, such as CSS, images, and JavaScript.

Once the project has been added to the solution, we have to setup some references. The first is in the Blazor App. It should reference the Razor Class Library.

Because we want to use DevExpress Controls in the Library, we also need to add a reference to the DevExpress.Blazor assembly. This line can simply be copied from the App project file.

<ItemGroup>
    <PackageReference Include="DevExpress.Blazor" Version="23.1.4" />  
</ItemGroup>

After a quick build (to make sure the DevExpress package is loaded in the library), we can add the DevExpress namespace in the _Imports.razor file of the library.

Now we can create a new Razor component in the Library which I will call DxThemeToggler.razor.

It will contain the following markup:

<HeadContent>
    @foreach (var item in _activeTheme.StylesheetLinkUrl)
    {
        <link href="@item" rel="stylesheet" />
    }
</HeadContent>

<DxCheckBox CheckType="CheckType.Switch"
            LabelPosition="LabelPosition.Left" Alignment="CheckBoxContentAlignment.SpaceBetween"
            @bind-Checked="@DarkMode">
    Dark mode
</DxCheckBox>

@code {
    public record ThemeItem(string Name, string[] StylesheetLinkUrl)
    {
        public static ThemeItem Create(string name)
            => new ThemeItem(name, new[] { $"_content/DevExpress.Blazor.Themes/{name}.bs5.min.css" });
    };

    private readonly static ThemeItem lightTheme = ThemeItem.Create("blazing-berry");
    private readonly static ThemeItem darkTheme = ThemeItem.Create("blazing-dark");
    private ThemeItem _activeTheme = lightTheme;
    
    private bool _darkMode = false;
    public bool DarkMode
    {
        get => _darkMode;
        set
        {
            if (_darkMode != value)
            {
                _darkMode = value;
                _activeTheme = _darkMode ? darkTheme : lightTheme;
                InvokeAsync(StateHasChanged);
            }
        }
    }
}

The only thing to do to make this work is to add the newly created DxThemeToggler control somewhere in the App. The most ideal place would be the ~/Shared/Header.razor

<nav class="navbar header-navbar p-0">
    <button class="navbar-toggler bg-primary d-block" @onclick="OnToggleClick">
        <span class="navbar-toggler-icon"></span>
    </button>
    <div class="ms-3 fw-bold title pe-4 flex-grow-1">DxBlazorChinook</div>
    <DxBlazor.UI.DxThemeToggler></DxBlazor.UI.DxThemeToggler>
</nav>
Note: I have added the flex-grow-1 class on the element above so everything is aligned properly.

Automatic Theme Switching

One of the things I wanted to investigate is whether it is possible to check the theme of your OS and sync your App with it. After a bit of searching online, I arrived on this Stack Overflow page.

The first visible answer gave these tips:

if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
    // dark mode
}

// To watch for changes:

window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
    const newColorScheme = event.matches ? "dark" : "light";
});

Cool! Exactly what I need (and a bit more). We’ll be needing a bit of JavaScript Interop for this to work.

With the creation of the Razor Class Library, the project template is generated with a sample JavaScript file and an Interop example. The example C# code uses a nice pattern to lazy-load a JavaScript file and it shows how to execute the JavaScript method in the sample JavaScript file. We can copy over a bunch of things.

First let’s create a JavaScript file dxblazor-ui.js in the ~/wwwroot folder which contains the following function:

export function isDarkMode() {
    // from StackOverflow
    return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; 
}
    

Now let’s copy (and change) some code from the ExampleJsInterop.cs into our DxThemeToggler.razor

private readonly Lazy<Task<IJSObjectReference>> moduleTask;

[Inject] IJSRuntime jsRuntime { get; set; } = default!;

public DxThemeToggler()
{
    moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>(
        "import", "./_content/DxBlazor.UI/dxblazor-ui.js").AsTask());
}

public async ValueTask<bool> IsDarkMode()
{
    var module = await moduleTask.Value;
    return await module.InvokeAsync<bool>("isDarkMode");
}

public async ValueTask DisposeAsync()
{
    if (moduleTask.IsValueCreated)
    {
        var module = await moduleTask.Value;
        await module.DisposeAsync();
    }
}
    
Note: The ExampleJsInteropt.cs uses constructor injection to get the IJSRuntime reference but since a Razor component requires a parameterless constructor
I have created a member decorated with the [Inject] attribute.

Furthermore, the DxThemeToggler.razor component should implement IAsyncDisposable and we need to use the Microsoft.JSInterop namespace.

@implements IAsyncDisposable
@using Microsoft.JSInterop

<HeadContent>
    ... code omitted for clarity ...

The last thing to add to the DxThemeToggler is an override of the OnAfterRenderAsync.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    await base.OnAfterRenderAsync(firstRender);
    if (firstRender)
    {
        DarkMode = await IsDarkMode();
    }
}

Once we run the app, you’ll see that it starts with the same light or dark theme that your OS is currently using.

Triggering on OS Theme Changes

I mentioned earlier that we found more than we were originally looking for… the second snippet from Stack Overflow. It even states: “To watch for changes”. Let’s see how that works…

If we take a look at that code, an event listener is being registered, and in our case it should change the C# DarkMode property.

So in the JavaScript file we can add the following:

export function addThemeSwitchListener(dotNetReference) {
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
        dotNetReference.invokeMethodAsync("OsThemeSwitched", event.matches);
    });
}

In the DxThemeToggler we need to execute that function once. A good place would be the OnAfterRenderAsync.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    await base.OnAfterRenderAsync(firstRender);
    if (firstRender)
    {
        DarkMode = await IsDarkMode();
        var module = await moduleTask.Value;
        await module.InvokeVoidAsync("addThemeSwitchListener", DotNetObjectReference.Create(this));
    }

}

The second thing to add is the method OsThemeSwitched. Because it is called from JavaScript, it needs to be decorated with the [JSInvokable] attribute.

[JSInvokable]
public void OsThemeSwitched(bool isDark)
{
    DarkMode = isDark;
}

Well…Let’s bootup a bunch of different browsers navigating to your app, and try to switch the theme of your OS!

The entire project is available on GitHub. (I have also created a branch called EP01-Auto-Dark-Light-Theme-toggling that holds the code we’ve been discussing in this episode)

In the next episode, I will talk about data access and laydown a basic architecture which works for me. It might be something for you as well!

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.