Are you sure you need that 'else' - Extend your Blazor components

March 27, 2023 · 8 minute read · Tags: blazor

A few years ago I worked for a company with a large codebase.

We’d been going for a few years and, as it has a habit of doing, the existing code had spawned more code until finally, we’d reached a point where managing and maintaining the various applications was a bit of a slog.

Tell me if this sounds familiar:

  • A feature request comes in for a “small tweak” to some existing logic
  • You head in to the code to figure out where to make the change
  • Several hours later, the full enormity of the task hits you, you realise you’re no closer to finding the relevant code than you were two hours ago
  • You keep jumping from one method to another, trying to remember the convoluted path you took along the way
  • Finally, a few hours (or even days) later you find the promised land, the hallowed if statement you’ve been looking for
  • There’s already 15 branches to this thing, some of them nested a few layers deep, but you can finally see where the “small tweak” could be made
  • You take a momentary pause to think “this could probably do with a refactor” then throw another else condition in and deny you were ever here in the first place!

This is true for backend apps, but it applies to UI/front end to.

It doesn’t take much for a “simple” component to descend into chaos.

It starts off simple, a nice small component to show a panel (for example).

Panel

<div class="border border-2">
    <div class="p-4 bg-primary text-white">
        <h4 class="mb-0">@Title</h4>
    </div>
    <div class="p-4 fs-6">
        @ChildContent
    </div>
</div>
@code {

    [Parameter]
    public string Title { get; set; }

	[Parameter]
	public RenderFragment ChildContent { get; set; }

}

There’s nothing really to this, just a little bit of UI and parameters to set the title and contents…

<Panel Title="Sales (this month)">
    <p>This could be a chart or anything really...</p>
</Panel>

But it doesn’t take much for the complexity to start ramping up.

Let’s say we’re asked to make panels dismissible (so they can be hidden after the user has seen them).

Oh, and we also need Important panels, which have a red background for the title. Important panels should not be dismissible.

Here’s one attempt at handling these new requirements:

Panel (Advanced Edition!)

<div class="border border-2 @visibility">
    <div class="p-4 d-flex justify-content-between @TitleClass() text-white">
        <h4 class="mb-0">@Title</h4>
        @if (Type != PanelType.Important)
        {
            <button class="btn-close" @onclick="()=>dismissed = true"></button>
        }
    </div>
    <div class="p-4 fs-6">
        @ChildContent
    </div>
</div>
@code {

    [Parameter]
    public string Title { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public PanelType Type { get; set; }

    private bool dismissed = false;

    private string visibility => dismissed ? "d-none" : "d-block";
    
    string TitleClass()
    {
        switch (Type)
        {
            case PanelType.Default:
                return "bg-primary";
            case PanelType.Important:
                return "bg-danger";
            case PanelType.Inactive:
                return "bg-secondary";
        }
        return "bg-primary";
    }
    
    public enum PanelType
    {
        Important = 1,
        Inactive = 2,
        Default = 3
    }

    private void Dismiss()
    {
        if (Type != PanelType.Important)
        {
            dismissed = true;
        }
    }

}

(this is using Boostrap CSS utility classes to control the panel’s appearance).

Now we’ve embedded the idea of different panel types:

  • Important
  • Inactive
  • Default

With possibly more to come.

Two panels

With this we’ve met the requirement (for non-dismissible important panels) but, even at this early stage the complexity is creeping up.

We’ve ended up with conditional logic, checking the panel type, in multiple places, in the markup:

...

@if (Type != PanelType.Important)
{
    <button class="btn-close" @onclick="()=>dismissed = true"></button>
}

...

And the razor component’s code:

...

string TitleClass()
{
    switch (Type)
    {
        case PanelType.Default:
            return "bg-primary";
        case PanelType.Important:
            return "bg-danger";
        case PanelType.Inactive:
            return "bg-secondary";
    }
    return "bg-primary";
}

private void Dismiss()
{
    if (Type != PanelType.Important)
    {
        dismissed = true;
    }
}

...

Conditional logic tends to attract more conditional logic over time…

It seems clear this code is increasing in complexity, and likely to continue to grow organically as the feature requests pile up.

Design your component’s API

So let’s take a step back and ask a key question:

What do our panels need to be able to do.

It looks like we need to give them options to be:

  • Dismissible
  • Customisable (specifically the TitleClass we’re using for the panel heading)

At the moment we’ve put all the logic for controlling the appearance and behaviour of different types of panel in the Panel component itself.

One option would be to “dumb down” the panel component, to have less knowledge about our important panels etc.

We could make Panel instances more configurable, at a granular level, by expanding the public API of the Panel component:

<Panel Dismissible="false" TitleClass="bg-danger">
	Important Panel!
</Panel>
<Panel Dismissible="true" TitleClass="bg-primary">
	Some other Panel!
</Panel>

Here we’ve exposed parameters for Dismissible and TitleClass, which removes some of the complexity from the underlying Panel component:

<div class="border border-2 @visibility">
    <div class="p-4 d-flex justify-content-between @TitleClass text-white">
        <h4 class="mb-0">@Title</h4>
        @if (Dismissible)
        {
            <button class="btn-close" @onclick="()=>dismissed = true"></button>
        }
    </div>
    <div class="p-4 fs-6">
        @ChildContent
    </div>
</div>
@code {
    [Parameter]
    public string Title { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public string TitleClass { get; set; } = "bg-primary";

    [Parameter]
    public bool Dismissible { get; set; } = true;

    private bool dismissed = false;
    
    private string visibility => dismissed ? "d-none" : "d-block";
    
    private void Dismiss()
    {
        if (Dismissible)
        {
            dismissed = true;
        }
    }
}

But we’ve lost something here:

We can render Panel instances wherever we we like, and arbitrarily set the Dismissible and TitleClass parameters, but we’ve lost some all important domain knowledge in the process.

The business came to us and talked about Important panels, which always behave a certain way.

If we simplify that down down to a few primitive parameters (Dismissible and TitleClass) we’ve lost that context in our code.

What started as a clear business requirement for ‘important panels’ is now implemented as a generic Panel component, which just happens to have a certain combination of values (not collapsible, red).

With this approach we can’t easily locate, let alone change all important panels as new requirements come in, because we haven’t used the terminology ‘important’ anywhere in our code. Effectively we just have a number of panels with seemingly arbitrary combinations of parameter values.

On the other hand, the Panel component is looking quite simple and maintainable at this point, so what should we do?

Well, we can refactor that knowledge, and logic about Important Panels, into a separate component which, in turn, delegates to the generic Panel component.

ImportantPanel.razor

<Panel Title="@Title" 
	ChildContent="ChildContent" 
	TitleClass="bg-danger" 
	Dismissible="false"/>
@code {
    
    [Parameter]
    public string Title { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }
    
}

Now we’ve encapsulated what an ImportantPanel is, in terms of appearance and behaviour. We can use this panel as many times as we like:

<ImportantPanel Title="Another One?">
	<p>I mean how many panels are enough?</p>
</PanelImportant>

This small change brings numerous benefits:

  • If we ever get a requirement to tweak the colour, or behaviour of important panels, we can do it in one place
  • There’s less code to reason about, because it delegates to the existing Panel “black box” for the core Panel functionality
  • The ‘API’ for ImportantPanel is consistent, with no options to set Dismissible, because Important Panels can’t be dismissed
  • It should prove easy to find the code that relates to important panels because it’s in a single, well-named component
  • This ImportantPanel component doesn’t have any concerns about other panel types, so there’s no risk of behaviour leaking into other component types by mistake

As more requirements come in, we can create more, specific, panel variations.

“Thinking in components”

This ability to compose components together is a big part of building ‘modern’ web applications with Blazor.

I’ve been using component-based frameworks since Angular JS first burst on to the scene and I remember thinking back then, how powerful components were, but also how I wasn’t quite “getting it”.

My apps would start off simple, with one or two components, but then slowly grow in complexity, until they were, frankly, a bit of a mess of interconnected components.

Since then, with every new requirement, and a lot of trial and error, I’ve built up mental models, and a repeatable, consistent approach that makes it much faster, easier, and more enjoyable to build web apps using components.

The approach we’ve seen here is just one way you can really leverage Blazor’s component model to build better web apps.

But there’s more, lots more! 📚

Over the last few months or so I’ve been working on a new self-paced, interactive workshop that makes Blazor development easier and faster by retraining your brain to “think in components”.

Find out more about Practical Blazor Components below.

Practical Blazor Components

Learn to “think in components” - self-paced, interactive course, available now.

Build better Blazor applications, faster

Next up

Finally! Improved Blazor Server reconnection UX
.NET 9 changes how your Blazor Server app behaves when server connection is lost
.NET 9 improves JavaScript module importing for Blazor
.NET 9 ensures your users always get the latest version of your JS modules
How to use .NET 9 to ensure users always get the latest version of your stylesheets
.NET 9 changes how static files are served, and it solves a long-standing problem