Blazor Dynamic Layout Pages and CSS

Functionality that provides a consistent look and feel across an entire web site is nothing new. In fact, I enjoyed stumbling across this 2008 gem by Scott Mitchell, a founder of the classic 4GuysFromRolla.com ASP site:

Creating a Site-Wide Layout – circa 2008

I mean, this example showcases a Silverlight Beta, a featured advertisement, and multi-language globalization functionality recently covered in this blog post.

Introduction

So, I was looking to experiment with approaches that would work to dynamically swap out Blazor’s layout page and CSS, based upon data-driven input. This input could come from query string parameters, a database, or even a cookie. For the purposes of this article, it really does not matter where the data originates, so long as we can use it to drive changes in the look-and-feel of the site.

The goals are:

  1. Provide multiple versions of layout pages and multiple versions of CSS files.
  2. Change page appearance based upon a data-driven, component-based, approach.
  3. Position the application to persist a user-selected configuration.

Allowing users to choose their own layouts and CSS is similar to, but different from, theming functionality such as that provided by MudBlazor and other component frameworks. Changing CSS can accomplish the same functionality themes do such as toggling between, say, a “light” mode and a “dark” mode. Blazor pages, on the other hand, are currently designed to have static layouts defined. We are going to demonstrate a way to overcome that limitation in this blog post.

This technique can be useful for allowing users to have their own display preferences, changing content based upon the user’s device type, or for multi-tenant scenarios that allow each tenant to choose from a pre-defined set of templates for their own site’s appearance.

Spoiler alert – the code accompanying this article is available in this public “Blazor Dynamic Layout Pages” GitHub repo.

The “Problem” with Blazor Layouts

A Blazor layout is simply a Razor component that shares markup with other components that reference it. It can be thought of as a “super component” and is well-described in Microsoft’s documentation here. Our “problem” is that a page’s layout cannot be changed at runtime because it is controlled by a compile-time constant that gets converted into a class-level attribute. This happens either by providing a type to the DefaultLayout property of a RouteView component or by specifying a @layout directive in your razor page component.

App.razor in a Blazor Wasm project
@layout directive in a Razor page

I put “problem” in quotes above because it is conceivable that, through the power of nested components, we could devise a strategy pattern or other method for layout/template selection. That, of course, would not be nearly as much fun and challenging for this mission.

Let’s Do This

To meet our first goal of providing multiple versions of layout pages and multiple versions of CSS files, we first must actually have said files. The steps that follow will get you going quickly and a screenshot is provided at the end so you can verify your solution and project structures match the example.

1. Create a new Blazor WebAssembly project as described here, or, from the CLI:

dotnet new blazorwasm -o blazor-dynamic-layoutcss

2. Open the new project, named blazor-dynamic-layoutcss above, build and run it just to verify everything is working out-of-the-box.

To make our example a little more “real world” and reusable, we are going to isolate our key functionality into a separate component library project that will be referenced by our Blazor WebAssembly project.

3. Add a second project named LayoutSwapper to your solution. The project you create should use the Razor Component Library project template.

It is important that you do not select the regular Class library project template. We want to create a reusable component library for our Blazor project to make it a little more of a real-world example.

Delete these unnecessary files from the newly-created LayoutSwapper project, but leave the wwwroot directory intact:

  • wwwroot\background.png
  • wwwroot\exampleJsInterop.js
  • Component1.razor
  • ExampleJsInterop.cs

4. Add two more NuGet packages to the new LayoutSwapper Razor class library project, Microsoft.AspNetCore.Components and Microsoft.AspNetCore.WebUtilities. Adding these upfront saves us some compilation errors when we add additional files in just a moment.

Note, the Microsoft.AspNetCore.Components.Web NuGet package should already be a project dependency if you created the LayoutSwapper project using the Razor class library project template.

5. Underneath the wwwroot folder in the LayoutSwapper class library project, create a new folder named css.

6. At the root of the LayoutSwapper class library project, create two new folders named Components and Pages.

At this point, your basic solution structure should look like the image on the right.

With the basic solution structure and references to necessary NuGet packages in place, we can now add a few CSS files and layout pages that we will later use for testing.




7. Inside the css folder create two files, sample1.css and sample2.css.

sample1.css:

html, body {
    background-color: palevioletred;
}

sample2.css:

html, body {
    background-color: darkslategrey;
}
Make sure both files have their build action property set to Content.

We can now add the classes we will need to dynamically select a layout page based upon external data.

8. Inside the Components folder, add a new class named DynamicLayoutAttribute.cs.

using System;
using System.ComponentModel;

namespace LayoutSwapper.Components
{
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public sealed class DynamicLayoutAttribute : Attribute
    {
        public Type LayoutType { get; private set; }
    }
}

9. Inside the Components folder, add another new class named DynamicRouteView.cs.

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using System;
using System.Reflection;

namespace LayoutSwapper.Components
{
    public class DynamicRouteView : RouteView
    {
        [Inject]
        protected NavigationManager NavigationManager { get; set; }

        protected override void Render(RenderTreeBuilder builder)
        {
            var pageLayoutType = RouteData.PageType.GetCustomAttribute<LayoutAttribute>()?.LayoutType
                ?? DefaultLayout;
            string selectedCSS = string.Empty;

            if (RouteData.PageType.GetCustomAttribute<DynamicLayoutAttribute>() != null)
            {
                int? layoutParam = null;
                int? cssParam = null;

                var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
                Console.WriteLine($"AbsoluteUri: '{uri}'");

                if (Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query).TryGetValue("l", out var param1))
                {
                    layoutParam = Int32.Parse(param1);
                }

                if (Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query).TryGetValue("c", out var param2))
                {
                    cssParam = Int32.Parse(param2);
                }

                if (layoutParam.HasValue)
                {
                    Console.WriteLine($"Layout param = '{layoutParam}'");
                    switch (layoutParam.Value)
                    {
                        case 1:
                            pageLayoutType = typeof(Pages.AlternateLayout);
                            break;

                        case 2:
                            pageLayoutType = Type.GetType("blazor_dynamic_layoutcss.Shared.MainLayout, blazor-dynamic-layoutcss");
                            break;
                    }

                    switch (cssParam.Value)
                    {
                        case 1:
                            selectedCSS = "_content/LayoutSwapper/css/sample1.css";
                            break;

                        case 2:
                            selectedCSS = "_content/LayoutSwapper/css/sample2.css";
                            break;
                    }

                    Console.WriteLine($"pageLayoutTypeName type: '{pageLayoutType.FullName}'");
                    Console.WriteLine($"selectedCSS: '{selectedCSS}'");
                }

                if (cssParam.HasValue)
                {
                    Console.WriteLine($"CSS param = '{cssParam}'");
                }
            }

            var fiRenderPageWithParametersDelegate = typeof(RouteView)
              .GetField("_renderPageWithParametersDelegate", BindingFlags.Instance | BindingFlags.NonPublic);
            var _renderPageWithParametersDelegate = fiRenderPageWithParametersDelegate.GetValue(this);

            builder.OpenComponent<LayoutView>(0);
            builder.AddAttribute(1, nameof(LayoutView.Layout), pageLayoutType);
            builder.AddAttribute(2, nameof(LayoutView.ChildContent), _renderPageWithParametersDelegate);
            builder.CloseComponent();
            builder.AddMarkupContent(3, $"<link href=\"{selectedCSS}\" rel=\"stylesheet\">\r\n\r\n");
        }
    }
}

Let’s pause at this point to evaluate where we are in the process. The files in your LayoutSwapper project should now be organized as depicted in the image on the right:

Let’s also examine the DynamicRouteView code above because it really is the crux of our initiative here.

  • We can inject a NavigationManager instance because, after all, our class inherits from RouteView, which itself supports the IComponent interface.
  • We override the Render method in order to perform our custom logic. In this simple example, we are obtaining query string parameter values as the data that will drive the selection of both our layout page and our css class files. With a little imagination, you can see how we could inject another component that, perhaps, obtains this data from a database. Before doing so, that component could even examine the current user identity (if logged in) and use that to further customize the decision process.
switch (layoutParam.Value)
{
    case 1:
        pageLayoutType = typeof(Pages.AlternateLayout);
        break;

    case 2:
        pageLayoutType = Type.GetType("blazor_dynamic_layoutcss.Shared.MainLayout, blazor-dynamic-layoutcss");
        break;
}
  • The preceding code demonstrates two ways of referencing our layout pages. The first uses a simple typeof() method invocation to obtain the type of a class known to the current assembly (our LayoutSwapper project). The second approach uses the more flexible Type.GetType() method, accepting a fully-qualify type name. This latter approach gives us the power to reference types that are not known at design time and could, potentially, be fed in as parameters at runtime.
  • In the following code, there is a touch of reflection trickery used to get a needed to get the child content render fragment delegate. That is then followed by building out the LayoutView component and adding a link tag to reference our selected CSS file.
var fiRenderPageWithParametersDelegate = typeof(RouteView).GetField("_renderPageWithParametersDelegate", BindingFlags.Instance | BindingFlags.NonPublic);
var _renderPageWithParametersDelegate = fiRenderPageWithParametersDelegate.GetValue(this);

builder.OpenComponent<LayoutView>(0);
builder.AddAttribute(1, nameof(LayoutView.Layout), pageLayoutType);
builder.AddAttribute(2, nameof(LayoutView.ChildContent), _renderPageWithParametersDelegate);
builder.CloseComponent();
builder.AddMarkupContent(3, $"<link href=\"{selectedCSS}\" rel=\"stylesheet\">\r\n\r\n");

At this point, we are almost ready to test our creation, just a little more setup to do back in the Blazor project.

10. Be sure that the LayoutSwapper class library project is referenced as a project dependency by our original Blazor Wasm project.

11. Open the _Imports.razor file in the blazor-dynamic-layoutcss project and add a using statement for LayoutSwapper.Components.

12. Underneath the Pages folder, add a new razor page named SwappablePage.razor.

@page "/SwappablePage"
@attribute [DynamicLayout()]

<h3>Click the links below to view the same page with different layouts and CSS classes.</h3>

<div>
    <a href="SwappablePage?l=1&c=1">Alternate Layout, CSS Sample 1</a>
</div>
<div>
    <a href="SwappablePage?l=1&c=2">Alternate Layout, CSS Sample 2</a>
</div>
<div>
    <a href="SwappablePage?l=2&c=1">Main Layout, CSS Sample 1</a>
</div>
<div>
    <a href="SwappablePage?l=2&c=2">Main Layout, CSS Sample 2</a>
</div>

13. In the App.razor page, remove or comment out the default <RouteView> element name within the <Found> element and replace it with our new <DynamicRouteView> component.

<DynamicRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />

14. Build and run the web assembly project to experiment with swapping out the layout and css files in the SwappablePage page. Unless you added a menu item for this page, you will have to manually enter its navigation URL into the browser (i.e. https://localhost:5001/SwappablePage).

At first, nothing is really different until you try clicking the hyperlinks to vary the query string parameters. Then, you will see the same page route is using variable layouts and CSS classes.

Congratulations, you now have the beginnings of a dynamic, data-driven, page rendering system using Blazor! This allows you to develop applications that can persist user-selected configurations, display content differently based upon the user’s device type, or enable advanced multi-tenant scenarios.

Leave a Comment

Your email address will not be published.