Click here to Skip to main content
15,887,214 members
Articles / Web Development / Blazor

Blazor Server. Making the Most of Fluxor

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
7 Aug 2023CPOL8 min read 10.7K   109   10  
How to use Fluxor to progress applications along clearly defined pathways
This article illustrates how the Fluxor middleware’s messaging system can be applied to the default Blazor Server application to simplify its design and improve its functionality.

Introduction

A common reason why Blazor Server apps tend to use Fluxor, is to persist the state of Blazor components. So, for example, the default application’s Counter page does not reset its click count to zero every time it comes back into scope. But there is more to Fluxor than that, it’s an integrated package of Message Oriented Middleware (MOM) that can progress an entire application along clearly defined pathways.

Fluxor Patterns and Terminology

The patterns and terminology used in the following example are the ones illustrated in the Fluxor documentation. Pathways through an application are defined on a ‘use-case’ basis. These are sections of functionality that have an event that has an effect that changes the state of the application in some way. The main components of the pathway, the event, the event handler and the state management handler have to be provided but they are very loosely coupled together so that there is no need for any user defined integration software other than the provision of simple message entities. The use-case pattern for building an application differs from the more traditional tiered architecture pattern. Tiered Architecture consists of functional layers that are usually implemented as services. It's a multi-layered cake approach. The use-case pattern takes the multi-layered cake and implements it as a series of slices cut vertically through the layers. So a service may not need to be a dedicated entity if its functionality can be implemented slice by slice within use-cases.

Use-Case Examples

Two use-cases can be identified within the default Blazor Server application. A Counter use-case, where a button is clicked, then a click count is calculated, stored and displayed and a Forecast use- case where the initialization of a page results in a weather forecast being uploaded, displayed and stored. The following example of the Counter use-case shows how the Fluxor pattern of ‘event, effect, state change’ can be applied to a simple pathway.

A Counter-Use Case Example

In this example, the event is a button click, the effect is a recalculated click count and the state change is that the application gets a new count total. A good way to start building the use-case is to define the messages that make up the message trail along the pathway.

Actions

In the Fluxor documentation, messages are referred to as actions. They are simple record types with descriptive names. Two actions are required:

C#
public record EventCounterClicked();
public record SetCounterTotal(int TotalCount);

The action's name is usually preceded by the message type. The standard message types are:

  1. Events
  2. Commands
  3. Documents

Document messages hold data. My preference is to use the word Set to describe Document actions that are used to change the state. This nomenclature can be very useful when debugging messages that have been sent or received out of sequence.

Defining the State

The state is the read-only immutable data store that’s used to hold the click count total. The state is a record type, the definition of a state is slightly more complicated than that of an action as it needs to have a default constructor.

C#
[FeatureState]
//defines a record with a public readonly property TotalCount
public record CounterState(int TotalCount)
{
    //The required default constructor
    public CounterState() : this(0)
    {

    }
}

Defining the Counter Page

The aim here is to implement a component that does one thing well. In the case of a page, that one thing is managing the UI. The component should be loosely coupled and reuseable. It does not need to know anything about any other component or have any predefined functionality such as counting the clicks and storing the click count. This is easily achieved by using the component’s OnClicked handler to dispatch an EventCounterClicked message to Fluxor’s message router.

Blazor
@page "/counter"
@using BlazorFluxor.Store
@using BlazorFluxor.Store.CounterUseCase
@using Fluxor.Blazor.Web.Components
@using Fluxor;
@inherits FluxorComponent

 <PageTitle >Counter </PageTitle >

 <h1 >Counter </h1 >

 <p role="status" >Current count: @CounterState!.Value.TotalCount </p >

 <button class="btn btn-primary" @onclick="OnClicked" >Click me </button >

@code {
    [Inject]
    protected IState <CounterState >? CounterState { get; set; }
    [Inject]
    public IDispatcher? Dispatcher { get; set; }
    private void OnClicked()
    {
        Dispatcher!.Dispatch(new EventCounterClicked());
    }
}

The injected IState<CounterState> instance has a Value property that is used to reference the State, it also has a StateChanged event. The FluxorComponent, that the page inherits, subscribes to the StateChanged event and this results in the page re-rendering every time the state’s TotalCount is updated by the middleware. Any other FluxorComponent that shares the same CounterState TotalCount and is in scope will also rerender. So, for example, a shopping cart component will have its item count updated when the Count page button is clicked.

The Effects Class

Actions that have effects other than updating the state are usually handled in an Effects class. The Effects class name is plural but it is only a collection in the sense that it can contain multiple message handlers. The name of the action handler method can be anything but all handlers must have the same signature and be decorated with the EffectMethod attribute. The following handler handles the EventCounterClicked action and determines the effect that the receipt of the action has on the state of the application.

C#
[EffectMethod]
public Task HandleEventCounterClicked
       (EventCounterClicked action,IDispatcher dispatcher)
{
    int totalCount = _counterState.Value.TotalCount + 5;
    dispatcher.Dispatch(new SetCount (totalCount));
    return Task.CompletedTask;
}

The SetCount action has its TotalCount property set to the updated count value and is then dispatched to the message router. The SetCount action is handled within a Reducers class.

The Reducers Class

The Reducers class is the state management class. It has handlers that reduce two or more records into a single new record instance. It is the only path through which the State can be changed. The format of this class is similar to that of the Effects class. All handlers must have the same signature and be decorated with the ReducerMethod attribute.

C#
    [ReducerMethod]
    public static CounterState ReduceSetCounterTotal
                  (CounterState state,SetCounterTotal action)
    {
        return state with { TotalCount = action.TotalCount };
    }
}

Record types have an optimized way of updating themselves by the use of the keyword with. What the reduce method is doing is returning a new state instance that is the same as the old state but with the TotalCount property updated to that of the action’s TotalCount property

Configuration

Configuration is not as difficult as it may seem as Fluxor relates the messages to their handlers and supplies all the parameters required by the Reducer methods and Effects methods as well as the IDispatcher and IState instances that are injected into components. You do not need to populate the container with these instances, the Fluxor Service takes care of that. The service needs to be added to the builder section of the Program class.

C#
builder.Services.AddFluxor(options = >
   {
     options.ScanAssemblies(typeof(Program).Assembly);

     #if DEBUG
      options.UseReduxDevTools();
     #endif
   });

The Redux Dev Tools option is included in the code above. It is a useful browser extension that plots the message trail and can show the state’s values at each stage along the trail. The last bit of configuration that Fluxor requires is to add the tag <Fluxor.Blazor.Web.StoreInitializer/> as the first line in App.razor. The recommended folder structure is shown below, Store is Fluxor's root directory.

A Forecast-Use Case

In the default application, a forecast use case is handled almost entirely within the FetchData page. That page manages both the UI and the injected WeatherForecast service. There is no state management so new daily forecasts are loaded every time the page comes into scope. Database errors are handled by the default error handler and are not database specific. The code below implements a single page Fluxor based forecast use case that maintains the state of the page so it does not update every time it comes into scope. The UI is controlled by binding types in the page to properties in an immutable State record and by populating the page with smart components that render only when the State requires them. The WeatherForecast service is implemented entirely within an Effects class so that the page has the single responsibility of rendering the UI.

The Forecast State

The immutable State record is defined like this:

Blazor
using BlazorFluxor.Data;
using Fluxor;
using System.Collections.Immutable;
namespace BlazorFluxor.Store.ForecastUseCase
{
    [FeatureState]
    public record ForecastState(
        ImmutableList<WeatherForecast> Forecasts,
        string? Message,
        bool IsError,
        bool IsLoading)
{
    public ForecastState() : this(ImmutableList.Create<WeatherForecast>(), 
                                  null, false, true)
    {

    }
}
}

The Forecasts list holds the daily forecasts and the Message string is for storing error messages. The two bools are used by the smart components to determine if they should render. The truth table below shows the possible settings of the State’s bools and the component that renders for each of the four possible settings.

IsLoading IsError Show
False False Data Table
True False Spinner
False True Error Dialog
True True Not Defined

The Forecast Effects Class

The WeatherForecast service is implemented entirely within the ForecastUseCase.Effects class so there is no need to inject the WeatherForecast service. An asynchronous stream is used to retrieve the daily forecasts so that each daily forecast can be displayed as soon as it becomes available from the stream. This is a better option than using a method that returns a Task<IEnumerable<Weatherforecast>> as the Task would have to complete before any data could be displayed and the page rendering would be delayed.

C#
    [EffectMethod]
    public async Task HandleEventFetchDataInitialized
           (EventMsgPageInitialized action, IDispatcher dispatcher)
    {
        try
        {
            await foreach (var forecast in ForecastStreamAsync
                          (DateOnly.FromDateTime(DateTime.Now), 7))
            {
                dispatcher.Dispatch(new SetForecast(forecast, false, false));
            }
        }
        catch (TimeoutException ex)
        {
            dispatcher.Dispatch(new SetDbError(ex.Message, true, false));
        }
    }

In order to demonstrate error handling, the GetForecastAsyncStream method will time out the first time that it is called and the error dialog will be shown. Subsequent calls will not time out.

C#
    public async IAsyncEnumerable<WeatherForecast> 
           ForecastStreamAsync(DateOnly startDate, int count)
    {
        int timeout = _cts == null ? 1500 : 2000;//timeout on first call only
        using var cts = _cts = new(timeout);

        try
        {
            await Task.Delay(1750, _cts.Token);
        }
        //make sure the correct OperationCanceledException is caught here
        catch (OperationCanceledException) when (_cts.Token.IsCancellationRequested)
        {
            throw new TimeoutException("The operation timed out.Please try again");
        }

        for (int i = 0; i < count; i++)
        {
            int temperatureIndex = Random.Shared.Next(0, Temperatures.Length);
            int summaryIndex = temperatureIndex / 4;//relate summary to a temp range
            await Task.Delay(125); //simulate slow data stream
            yield return new WeatherForecast
            {
                Date = startDate.AddDays(i),
                TemperatureC = Temperatures[temperatureIndex],
                Summary = Summaries[summaryIndex]
            };
         }
     }

The method uses integer division to relate a temperature range to an appropriate summary value:

C#
private static readonly string[] Summaries = new[]
{
   "Freezing", "Cold", "Mild", "Hot"
};
private static readonly int[] Temperatures = new[]
{
    0,-2,-4,-6,//index range 0-3 relates to summaries[index/4]==summaries[0]
    2,6,8,10,  //index range 4-7 relates to summaries[index/4]==summaries[1]
    12,14,16,18,
    23,24,26,28
};

The Forecast Reducers Class

A reducer updates the Forecasts list by calling the list’s Add method. The method is designed so that it creates a new updated instance of the list without the usual overhead associated with adding values and creating a new list.

C#
public static ForecastState ReduceSetForecast(ForecastState state, SetForecast action)
{
    return state with
    {
        Forecasts = state.Forecasts.Add(action.Forecast),
        IsError = action.IsError,
        IsLoading = action.IsLoading
    };
}

The WeatherForecast Table

The WeatherForecastTable is an example of a smart component. It simply uses a template table with an added IsShow bool, if this is set to true, the component will render.

Blazor
@if (IsShow)
{
    <TableTemplate Items="Forecasts" Context="forecast">
        <TableHeader>
            <th>Date</th>
            <th>Temp. (C)</th>
            <th>Temp. (F)</th>
            <th>Summary</th>
        </TableHeader>
        <RowTemplate>
            <td>@forecast.Date.ToShortDateString()</td>
            <td>@forecast.TemperatureC</td>
            <td>@forecast.TemperatureF</td>
            <td>@forecast.Summary</td>
        </RowTemplate>
    </TableTemplate>
}
@code {
#nullable disable
    [Parameter]
    public IEnumerable<WeatherForecast> Forecasts { get; set; }

    [Parameter]
    public bool IsShow { get; set; }
}

The FetchData Page

Blazor
@inherits FluxorComponent
@inject NavigationManager NavManager

<PageTitle>Weather Forecasts</PageTitle>

<h1>Weather Forecasts</h1>

<p>This component simulates fetching data from an async stream. 
   Each forecast is listed as soon as it is available.</p>

<Spinner IsVisible=@IsShowSpinner />
<WeatherForecastTable IsShow="@IsShowTable" Forecasts="@Forecasts" />

<TemplatedDialog IsShow="@IsShowError">
    <OKDialog Heading="Error"
              BodyText="Whoops, an error has occurred."
              OnOK="NavigateToIndex">@ForecastState!.Value.Message</OKDialog>
</TemplatedDialog>

The Spinner is available as a NuGet package and the templated components are based on the examples in the .NET Foundation’s Blazor-Workshop. The code section below is mainly concerned with simplifying the logic for displaying each of the components.

C#
@code {
    [Inject]
    protected IState<ForecastState>? ForecastState { get; set; }
    [Inject]
    public IDispatcher? Dispatcher { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        if (ForecastState!.Value.IsLoading is true)
        {
            Dispatcher!.Dispatch(new EventMsgPageInitialized());
        }
    }
    //An expression body definition of a read-only property
    protected IEnumerable<WeatherForecast> Forecasts => ForecastState!.Value.Forecasts!;

    protected bool IsShowError => (ForecastState!.Value.IsLoading is false && 
                                   ForecastState!.Value.IsError is true);
    protected bool IsShowTable => (ForecastState!.Value.IsLoading is false && 
                                   ForecastState!.Value.IsError is false);
    protected bool IsShowSpinner => (ForecastState!.Value.IsLoading is true && 
                                     ForecastState!.Value.IsError is false);

    private void NavigateToIndex()
    {
        Dispatcher!.Dispatch(new SetStateToNew());
        NavManager.NavigateTo("/");
    }
}

Conclusion

The examples illustrate the benefits of using Fluxor. It produces clear pathways that scale well and are easy to follow and debug. In the example, only one use-case is considered but in an enterprise application, there will be many. Everyone of them will require actions, effect handlers, reducers and a state so the code base is going to be large. But that's ok as we all believe in the maxim that verbose code can be smart code. Don't we?

Acknowledgement

I would like to acknowledge Peter Morris, the author of Fluxor. He is an excellent developer and his GitHub page is well worth following.

History

  • 4th July, 2023: Initial version
  • 8th August, 2023: Added a Fetch Data example

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Student
Wales Wales
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --