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.
Want to build interactive web apps with C#? Check SSW's Blazor Consulting page.
There are numerous frameworks available for front-end web development, with different developers preferring different tools. Some people like Angular, some like React, and some prefer Blazor. Let us take a look at the benefits of Blazor.
Full-Stack Development with .NET
Blazor allows developers to use C# and .NET for both client-side and server-side development. This unification means that developers can leverage their existing .NET skills to build entire applications without needing to switch languages or frameworks.
Component-Based Architecture
Blazor's component-based architecture allows for the creation of reusable components, promoting clean and maintainable code. Components in Blazor encapsulate UI and behavior, making it easier to manage complex applications.
Seamless Integration
Blazor seamlessly integrates with existing .NET libraries and frameworks, enabling developers to use a wide range of tools and resources. This integration ensures that developers can take full advantage of the rich .NET ecosystem.
High Performance
Blazor offers high performance through its WebAssembly-based client-side model. WebAssembly allows Blazor applications to run directly in the browser, providing near-native execution speed. For server-side Blazor, SignalR ensures efficient real-time communication.
Easy to Learn
For developers familiar with C# and .NET, Blazor is easy to pick up and start using. The learning curve is minimal, especially for those already experienced with the .NET ecosystem. Blazor's syntax and structure are straightforward, making it accessible for new developers as well.
Robust Tooling
Blazor benefits from excellent tooling support provided by Visual Studio and Visual Studio Code. These tools offer powerful debugging, IntelliSense, and project management capabilities, enhancing developer productivity and experience.
Strong Community and Support
Blazor has a strong and growing community, along with extensive documentation and resources. Microsoft actively supports Blazor, ensuring regular updates and improvements. The community provides numerous tutorials, forums, and libraries to help developers succeed.
Testability
Blazor applications are easy to test thanks to community support with bUnit, a testing library for Blazor components. bUnit allows developers to write unit tests for Blazor components, ensuring code quality and reliability.
References
Prepare for the future of web development by checking out these Blazor learning resources.
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
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 stateBenefits 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
(akaViewModel
) sectionImplementing 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 thePropertyChanged
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
interfaceOne of the main considerations with the
BaseState
abstraction is to keep theT State
as aprotected
member and not expose it publicly. This restricts the possibility of external changes to ourT State
.The next code snippet shows the
Counter
class which is a shared state object that is wrapped by the genericBaseState<Counter>
. This enables us to notify listeners when theCounter
state is explicitly changed.The
CounterState
implementation will call theOnPropertyChanged()
method whenever we explicitly changed the protectedCounter
.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 theCounter
object has been changedIn order for us to inject our
CounterState
object into a page or component, we must register it as a service (typically inProgram.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 componentThe 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 (byBaseState<>
) and our custom handler code will be executed.The Counter page example below calls
StateHasChanged()
when thePropertyChanged
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()
orReset()
methods are invoked, any listeners on the page will invoke the handling code attached to thePropertyChanged
event - and be able to invokeStateHasChanged
in order to update their respective views.The real value of implementing
INotifyPropertyChanged
(or by using an abstraction likeBaseClass<T>
above) is when the same shared state object is used multiple times on the same page and having thePropertyChanged
event handlers invoked from a single interaction and automatically keeping the view up to date for all components.Although this mitigates an issue with the AppState pattern, it is still not a complete solution for all scenarios. For more complex scenarios, consider using a Redux state management pattern. Fluxor is a NuGet package implementing the Redux pattern for Blazor.
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 theHttpClient
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
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 />
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>
References
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