SSW Foursquare

Rules to Better Blazor - 8 Rules

Blazor lets you build interactive web UIs using C# instead of JavaScript. Blazor apps are composed of reusable web UI components implemented using C#, HTML, and CSS. Both client and server code is written in C#, allowing you to share code and libraries.

Get started with Blazor

Want to build interactive web apps with C#? Check SSW's Blazor Consulting page.

  1. Do you know the best Blazor learning resources?

    Prepare for the future of web development by checking out these Blazor learning resources.

  2. Do you share common types and logic in Blazor?

    Due to Blazor using C#, your client and server can share the same model library - sharing behavior and data.

    This will reduce the amount of code you need to write, and make it easier to maintain.

    To share your classes between the client and server, just create a class library and reference it in your client and server projects.

    See Blazor Code Sharing Example as an example.

    References

  3. Do you know Blazor doesn't support stopping event propagation?

    Workaround - When you have two fields and you want to handle tabbing out of the last one to do some stuff and save.

    • If you handle onkeydown the value isn't set yet
    • If you handle onkeyup it triggers on the first field instead of the last one

    To solve the issue change the binding to @bind:event="oninput" so the value is captured before key down.

    The reason is due to the problem outlined in Keyup event behavior on tab.

    There are various workarounds such as using eventPropagation in JavaScript: Catching TAB key press with keyup.

    However, eventPropagation is not supported in Blazor as per this comment on Compiler support for @oneventname:preventDefault and @oneventname:stopPropagation.

    So, to prevent this issue an OnInput binding lets the value be set before the keydown event is triggered.

    This workaround can cause unexpected side effects if you have a custom setter so you shouldn't do that in tandem with this.

  4. State Management - Do you use the AppState pattern?

    The AppState pattern is one of the simplest State Management patterns to implement with Blazor WebAssembly.

    To start implementing the pattern, declare a class that describes the collection of fields that represents the state of a page, a form, or a model.

    Here are some basic example state objects:

    public class Counter
    {
        public int Counter { get; set; }
    }
    
    public class RegistrationForm
    {
        public Guid FormId { get; set; }
        public string EmailAddress { get; set; }
        public string GivenName { get; set; }
        public string Surname { get; set; }
        public string JobTitle { get; set; }
    }
    
    public class TimesheetEntry
    {
        public int Id { get; set; }
        public int ClientId { get; set; }
        public string ClientName { get; set; }
        public int ProjectId { get; set; }
        public string ProjectName { get; set; }
        public decimal HourlyRate { get; set; }
        public DateTime StartTime { get; set; }
        public DateTime EndTime { get; set; }
        public string Notes { get; set; }
    }
    
    public class Timesheet
    {
        public int Id { get; set; }
        public string UserName { get; set; }
        public TimesheetEntry[] Entries { get; set; }
    }

    Typically, these state objects would be hydrated from user input or a request to the backend API. In order for us to use this state object, we first need to register it as an injectable service (in Program.cs):

    builder.Services.AddScoped<Counter>();
    builder.Services.AddScoped<RegistrationForm>();
    builder.Services.AddScoped<Timesheet>();

    Once registered, we can use the @inject directive to inject the object into a page or component:

    @page "/counterWithState"
    
    @* Inject our CounterState and use it in the view and/or code section *@
    @inject Counter _state
    
    <PageTitle>Counter</PageTitle>
    
    @* we can reference the state object in the Razor markup *@
    <p>Current count: @_state.Count</p>
    
    @* Note: Due to user interaction, the page will refresh and show updated state value, even though we have not called StateHasChanged *@
    <button type="button" @onclick="IncrementCount">Click me</button>
    <button type="button" @onclick="Reset">Reset</button>
    
    @code {
    
        private void IncrementCount()
        {
            // we can modify the state object in the @code section
            ++_state.Count;
        }
    
        private void Reset()
        {
            _state.Count = 0;
        }
    }

    Alternatively if we are using code-behind (separate .razor and .razor.cs files), then we can use the [Inject] attribute to inject the object as a parameter for the code-behind class.

    Note: Constructor based injection is not supported for Blazor code-behind. Only Parameter based injection is supported.

    public partial class Counter : ComponentBase
    {
        [Inject]
        public Counter State { get; set; }
    
        private void IncrementCount()
        {
            ++_state.Count;
        }
    
        private void Reset()
        {
            _state.Count = 0;
        }
    }

    Drawbacks of basic AppState pattern

    ❌ We are unable to react to state changes made to the state object by other components

    ❌ We can modify the state but the page will not refresh to reflect the change

    ❌ We need to call StateHasChanged() manually when we modify the state

    Benefits of basic AppState pattern

    ✅ Implementation is trivial - register, inject, consume

    ✅ Works for very basic scenarios - especially if there are basic user interactions and basic state mutations directly on the @code (aka ViewModel) section

  5. State Management - Do you use the AppState pattern with State Change Notification?

    Implementing the INotifyPropertyChanged interface is one of the most popular and .NET native approaches to notify other components of changes to a shared state object.

    Implementing the INotifyPropertyChanged interface allows listeners (other pages / components / classes) to be notified when the PropertyChanged event is invoked.

    Listeners subscribe to the event by adding their own handling code to the PropertyChanged event.

    In this example we made the BaseState class generic so that we can have a reusable abstraction that works for all types of state objects.

    public abstract class BaseState<T> : INotifyPropertyChanged
    {
        private T _state;
    
        public BaseState(T initialState)
        {
            _state = initialState;
        }
    
        protected T State => _state;
    
        public event PropertyChangedEventHandler? PropertyChanged = null!;
    
        protected void OnPropertyChanged([CallerMemberName] string name = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }

    Figure: Generic State object that implements INotifyPropertyChanged interface

    One of the main considerations with the BaseState abstraction is to keep the T State as a protected member and not expose it publicly. This restricts the possibility of external changes to our T State.

    The next code snippet shows the Counter class which is a shared state object that is wrapped by the generic BaseState<Counter>. This enables us to notify listeners when the Counter state is explicitly changed.

    The CounterState implementation will call the OnPropertyChanged() method whenever we explicitly changed the protected Counter.

    public class Counter
    {
        public int Count { get; set; }
    }
    
    public class CounterState : BaseState<Counter>
    {
        public CounterState() : base(
            new Counter()
            {
                Count = 0
            })
        {
        }
    
        public void Reset()
        {
            State.Count = 0;
            OnPropertyChanged();
        }
    
        public void Increment()
        {
            ++State.Count;
            OnPropertyChanged();
        }
    }

    Figure: Implementation of the generic BaseState<T> object to notify listeners when the Counter object has been changed

    In order for us to inject our CounterState object into a page or component, we must register it as a service (typically in Program.cs).

    // register our CounterState object with a scoped lifetime
    builder.Services.AddScoped<CounterState>();

    Figure: Registering CounterState so that it can be injected to a page or component

    The ideal time to add a state change handler is when the page/component is being initialized via OnInitializedAsync().

    protected override async Task OnInitializedAsync()
    {
        _state.PropertyChanged += async (s, p) => await InvokeAsync(StateHasChanged);
    
        await base.OnInitializedAsync();
    }

    Once a property is changed, the PropertyChanged event will be invoked (by BaseState<>) and our custom handler code will be executed.

    The Counter page example below calls StateHasChanged() when the PropertyChanged event is invoked to refresh the view to display the latest state.

    @page "/counterWithPropertyChangeNotification"
    @implements IDisposable
    
    @* Inject our scoped CounterState and use it in the view / code section *@
    @inject CounterState _state
    
    <PageTitle>Counter with Observed State</PageTitle>
    
    <p class="h2">Counter with Observed State</p>
    <p class="mb-4">Current count: @_state.Value.Count</p>
    
    @* Note: Due to user interaction, the page will refresh and show updated state value, even though we have not called StateHasChanged *@
    <button type="button" class="btn btn-primary" @onclick="IncrementCount">Click me</button>
    <button type="button" class="btn btn-warning" @onclick="Reset">Reset</button>
    
    @code {
    
        protected override async Task OnInitializedAsync()
        {
            _state.PropertyChanged += async (s, p) => await InvokeAsync(StateHasChanged);
    
            await base.OnInitializedAsync();
        }
    
        public void Dispose()
        {
            _state.PropertyChanged -= async (s, p) => await InvokeAsync(StateHasChanged);
        }
    
        private void IncrementCount()
        {
            _state.Increment();
        }
    
        private void Reset()
        {
            _state.Reset();
        }
    }

    Figure: Full example showing how to inject state, subscribe to state changes and how to unsubscribe from state changes

    Note: Remember to unsubscribe from the PropertyChanged event to avoid any memory leaks. See rule about when to implement IDisposable.

    Whenever the IncrementCount() or Reset() methods are invoked, any listeners on the page will invoke the handling code attached to the PropertyChanged event - and be able to invoke StateHasChanged in order to update their respective views.

    The real value of implementing INotifyPropertyChanged (or by using an abstraction like BaseClass<T> above) is when the same shared state object is used multiple times on the same page and having the PropertyChanged event handlers invoked from a single interaction and automatically keeping the view up to date for all components.

  6. Do you decouple your API implementation from your Blazor components?

    When creating Blazor components that communicate with web APIs it is often tempting to inject the HttpClient and use it directly to send requests. While this is quick and easy to accomplish, this tightly couples the component to the HttpClient and the specific implementation of the web API. The downside of this tight coupling is that the component cannot be easily refactored for breaking changes (e.g. routes, payloads, querystrings, auth, etc) then the blast radius of changes can be quite substantial. The problem grows even further if accessing the API from multiple components. Another downside is that the component is no longer unit testable. Integration tests will be necessary which will require more effort to implement.

    Following the Dependency Inversion Principle (DIP) from the SOLID principles means that we should favour coding towards an interface rather than concrete implementations.

    Using an abstract client interface to interact with a web API has multiple benefits. One major benefit is that the component is decoupled from the concrete implementation of the web API client. Decoupled Blazor components can be unit tested with a mock implementation of the web API client. The decoupled concrete implementation of the web API client can also be tested in isolation without any UI concerns, and the code is more reusable in that it could be packaged and reused in other applications without any code duplication.

    @inject HttpClient Http
    
    @code {
      private WeatherForecast[]? forecasts;
    
      protected override async Task OnInitializedAsync()
      {
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
      }
    }

    Figure: Bad example - Component depends on HttpClient directly

    @inject IWeatherForecastClient WeatherForecastClient
    
    @code {
      private ICollection<WeatherForecast>? forecasts;
    
      protected override async Task OnInitializedAsync()
      {
        forecasts = await WeatherForecastClient.GetForecastsAsync();
      }
    }

    Figure: Good example - Component depends on web API client abstraction

    References

  7. Do you use the EditorRequired attribute for required parameters in Blazor?

    When you create a Blazor component, view parameters are marked with the [Parameter] attribute to indicate that they must be supplied by the parent component. By default, this is not enforced, which may lead to errors where you forget to pass in parameters where you use the component.

    You should use the [EditorRequired] attribute to mark parameters that are required in your Blazor component.


    TestComponent.razor

    <h3>@Name</h3>
    
    @code {
        [Parameter]
        public string? Name { get; set; }
    }

    Index.razor

    @page "/"
    
    <PageTitle>Home</PageTitle>
    
    <TestComponent />

    Figure: Bad example - Developers could forget to pass a variable to the Name property


    TestComponent.razor

    <h3>@Name</h3>
    
    @code {
        [Parameter, EditorRequired]
        public string? Name { get; set; }
    }

    Index.razor

    @page "/"
    
    <PageTitle>Home</PageTitle>
    
    <TestComponent />

    ide warning
    Figure: Good example - The IDE warns developers if they forget the Name parameter


    You should configure this warning (RZ2012) as an error so your IDE will fail to build if you are missing a required parameter. Add <WarningsAsErrors>RZ2012</WarningsAsErrors> to your Blazor .csproj file:

    <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
    	<PropertyGroup>
    		<TargetFramework>net7.0</TargetFramework>
    		<Nullable>enable</Nullable>
    		<ImplicitUsings>enable</ImplicitUsings>
    		<WarningsAsErrors>RZ2012</WarningsAsErrors>
    	</PropertyGroup>
    </Project>

    build error
    Figure: Good example - Build fails with an error

    References

  8. Do you use bUnit for unit tests in Blazor?

    Unit testing is an essential part of the software development process, especially for Blazor applications. bUnit is a testing library specifically designed for Blazor components, making it easier to write robust unit tests. It is installed via a NuGet package and can be used with any testing framework such as xUnit.

    When you use bUnit, you can simulate user interactions and assert component behavior in a way that is close to how your users will interact with your application. This can significantly increase the reliability of your components.


    Let's look at an example of a simple component that increments a counter when a button is clicked.

    ExampleComponent.razor

    <button class="incrementButton" @onclick="IncrementCount">Click me</button>
    <p>Current count: @currentCount</p>
    
    @code {
        public int currentCount = 0;
    
        public void IncrementCount()
        {
            currentCount++;
        }
    }

    Let's write a unit test for this component, asserting that the counter is incremented when the button is clicked.

    ExampleComponentTests.cs

    using Bunit;
    using Xunit;
    
    public class ExampleComponentTests
    {
        [Fact]
        public void ExampleComponent_ClickingButton_IncrementsCount()
        {
            // Arrange
            using var ctx = new TestContext();
            var cut = ctx.RenderComponent<ExampleComponent>();
    
            // Act
            cut.Find(".incrementButton").Click();
    
            // Assert
            cut.Instance.currentCount.ShouldBe(1);
        }
    }

    Figure: Good example - Using bUnit to test a Blazor component


    This is a very simple example, but the same concepts apply to more complex components. bUnit also provides a number of other features that make it easier to write unit tests for Blazor components, such as the ability to mock services and inject them into components.

    Complex components such as complicated searching and filtering are good candidates for bUnit tests, to ensure that a component behaves as expected.

    References

Need some help with Blazor?

Meet our experts
We open source. Powered by GitHub