Click here to Skip to main content
15,880,796 members
Articles / Web Development / Blazor

Get the Data out of Your Blazor UI

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
8 Aug 2022MIT4 min read 4.8K   33   4   2
How to move the data and its management out of the Blazor UI
This article takes the standard Blazor template and shows how to move data and its management out of the UI.

This article takes the standard Blazor template and demonstrates how to move the data and its management out of the UI. There are many questions posted on forums and sites by programmers where the root cause of their problem is trying to manage data within the UI. The quick answer to many is a bit more inter component wiring to patch it together, but fundamentally, the design is flawed. Add a bit more functionality and it all breaks again.

Here's a typical example:

C#
private WeatherForecast[]? forecasts;

protected override async Task OnInitializedAsync()
    => forecasts = await ForecastService.GetForecastAsync(DateTime.Now);

Recognise this block of code? It comes directly from FetchData in the Blazor templates. It's Microsoft distributed code, which gives it a stamp of approval it doesn't really deserve.

I've tried to keep things as simple as possible in the article. The code is For Demo Purposes: it's not full production code. I've left out stuff that would make it more difficult to read and understand. Read my footnote in the Appendix for more information on the kind of stuff that's missing.

Repository

The project and the latest version of this article are in the Blazr.Data Github Repository.

Starting Point

The starting solution for the code is the standard Blazor Server template. I can keep things simpler in Server, and debugging is quicker and easier. Note that the solution is implemented with Nullable enabled.

The Solution

First some re-organisation. The UI is currently plugged directly into the back-end data service. We need to re-build the data pipeline to look like this:

UI <=> View Service <=> Data Service <=> Data Store

The WASM your data pipeline would look like this:

UI <=> View Service <=> API Data Service <=> [Network] <=> 
       Controller <=> Server Data Service <=> Data Store

WeatherForecastDataService

Rename WeatherForecastService to WeatherForecastDataService. This combines the Data Service and Data Store layers.

It now:

  1. maintains an internal list of WeatherForcast objects.
  2. GetRecordsAsync provides a copy of WeatherForcast not a reference to the internal list. What an ORM such as Entity Framework would do.
  3. returns result objects containing both status information and data.
  4. returns IEnumerable collections: not lists or arrays.
  5. has an AddRecordAsync method to add a record to the data store.
C#
public class WeatherForecastDataService
{
    private List<WeatherForecast>? WeatherForecasts { get; set; }

    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", 
        "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public ValueTask<RecordCommandResult> AddRecordAsync(WeatherForecast record)
    {
        if (WeatherForecasts is not null)
        {
            var insertRecord = new WeatherForecast
            {
                Date = record.Date,
                TemperatureC = record.TemperatureC,
                Summary = record.Summary
            };
            WeatherForecasts.Add(insertRecord);
            return ValueTask.FromResult(RecordCommandResult.Successful());
        }

        return ValueTask.FromResult(RecordCommandResult.Failure("Can't add record"));
    }

    public ValueTask<RecordListResult<WeatherForecast>> GetRecordsAsync()
    {
        if (WeatherForecasts is null)
            GetForecasts();

        var list = new List<WeatherForecast>();
        foreach (var item in WeatherForecasts!)
            list.Add(new WeatherForecast
            {
                Date = item.Date,
                TemperatureC = item.TemperatureC,
                Summary = item.Summary
            });

        return ValueTask.FromResult
        ( RecordListResult<WeatherForecast>.Successful(list.AsEnumerable()));
    }

    private void GetForecasts()
    {
        var startDate = DateTime.Now;
        this.WeatherForecasts = 
        Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = startDate.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }).ToList();
    }
}

Result Records

These are the objects returned by the data layer. They are records because they need to be serializable to use in API calls.

RecordListResult is returned by all collection/list queries.

C#
public record RecordListResult<TRecord>
{
    public IEnumerable<TRecord> Items { get; init; } = Enumerable.Empty<TRecord>();
    public bool Success { get; init; }
    public string Message { get; init; } = string.Empty;

    public static RecordListResult<TRecord> Successful(IEnumerable<TRecord> items)
        => new RecordListResult<TRecord> { Items = items, Success = true };

    public static RecordListResult<TRecord> Failure(string message)
        => new RecordListResult<TRecord> { Success = false, Message = message };
}

RecordCommandResult is returned by all commands: Add/Delete/Update.

C#
public record RecordCommandResult
{
    public bool Success { get; init; }
    public string Message { get; init; } = string.Empty;

    public static RecordCommandResult Successful()
        => new RecordCommandResult { Success = true };

    public static RecordCommandResult Failure(string message)
        => new RecordCommandResult { Success = false, Message = message };
}

WeatherForecastViewService

WeatherForecastViewService is the View Service. It provides the data to the UI.

It:

  1. obtains the registered Data service through DI on object instantiation.
  2. provides methods to get and add records.
  3. provides the record collection.
  4. provides an event the UI can use for list update notifications.
C#
public class WeatherForecastViewService
{
    private WeatherForecastDataService _dataService;

    public WeatherForecastViewService
           (WeatherForecastDataService weatherForecastDataService)
        => _dataService = weatherForecastDataService;

    public IEnumerable<WeatherForecast> 
        Records { get; private set; } = Enumerable.Empty<WeatherForecast>();

    public WeatherForecast? Record { get; private set; }

    public string LatestErrorMessage { get; private set; } = string.Empty;

    public event EventHandler? ListUpdated;

    public async ValueTask<bool> GetRecordsAsync()
    {
        var result = await _dataService.GetRecordsAsync();
        this.LatestErrorMessage = result.Message;
        if (result.Success)
        {
            this.Records = result.Items;
            return true;
        }
        return false;
    }

    public async ValueTask<bool> AddRecordAsync()
    {
        this.Record = new WeatherForecast
        {
            Date = DateTime.Now,
            TemperatureC = 20,
            Summary = "Testing"
        };
        var result = await _dataService.AddRecordAsync(Record);
        this.LatestErrorMessage = result.Message;

        if (result.Success)
        {
            if (await this.GetRecordsAsync())
                this.ListUpdated?.Invoke(this, EventArgs.Empty);
        }
        return result.Success;
    }
}

Service Registration

Register these two services in Program. WeatherForecastViewService is Scoped: each SPA session has its own instance.

C#
builder.Services.AddSingleton<WeatherForecastDataService>();
builder.Services.AddScoped<WeatherForecastViewService>();

FetchData

We can now update the FetchData UI Component. It:

  1. injects the registered instance of WeatherForecastViewService.
  2. loads the View in OnInitializedAsync.
  3. uses Service.Records as its data source. There's no data held directly in the component.
C#
@page "/fetchdata"
@using Blazr.Data.Data
@inject WeatherForecastViewService Service

<PageTitle>Weather forecast</PageTitle>

<h1>Weather Forecasts</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (this.Service.Records == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in this.Service.Records)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    protected override async Task OnInitializedAsync()
        => await Service.GetRecordsAsync();
}

Adding an Editor

WeatherForecastEditorForm

This emulates an edit form. It's designed to be inline, so has control over show/hide. It could be a modal dialog.

It has one Parameter, no public properties and one public method.

  • The parent form communicates directly with the component through the Show method.
  • The component communicates with the parent through the FormClosed callback.

The internal Add and Exit methods close the component by setting show to false, and then invoke the callback to inform the parent of closure.

The form injects the registered instance of WeatherForecastViewService and uses AddRecordAsync to add a record to the data store.

C#
@inject WeatherForecastViewService Service 

@if (this.show)
{
    <div class="m-2 p-3 bg-light border border-1 border-primary">
    <h4>NewWeatherForecastForm</h4>
    <div class="container-fluid">
        <div class="row">
            <div class="col-12 text-secondary">
                Normally edit controls appear here
            </div>
        </div>
        <div class="row">
            <div class="col-12 text-end">
                <button class="btn btn-sm btn-success" 
                        @onclick=AddRecord>Add Record</button>
                <button class="btn btn-sm btn-dark" @onclick=Exit>Exit</button>
            </div>
        </div>
    </div>
    </div>
}

@code {
    [Parameter] public EventCallback FormClosed { get; set; }

    private bool show { get; set; } = false;

    private async Task AddRecord()
    {
        await Service.AddRecordAsync();
        show = false;
        await FormClosed.InvokeAsync();
    }

    private void Exit()
    {
        show = false;
        FormClosed.InvokeAsync();
    }

    public void ShowForm()
    {
        show = true;
        StateHasChanged();
    }
}

Note that ShowForm calls StateHasChanged. It's not a UI event handler, so there's no automated render events.

FetchData

The modified FetchData.

There's:

  1. a button block for Add A New Record: the block display is controlled by addForm.
  2. WeatherForecastEditorForm referenced to a local private field.
  3. an event receiver for the View Service ListUpdated event.
  4. a receiver for the edit form FormClosed callback.
  5. IDisposable implemented to de-register the event handler correctly.
C#
@page "/fetchdata"
@using Blazr.Data.Data
@inject WeatherForecastViewService Service

<PageTitle>Weather forecast</PageTitle>

<WeatherForecastEditorForm @ref=this.form FormClosed=this.OnFormClosed />

@if (!addForm)
{
    <div class="container-fluid">
        <div class="row">
            <div class="col-12 text-end">
                <button class="btn btn-sm btn-primary" 
                        @onclick=ShowAddForm>Add A New Record</button>
            </div>
        </div>
    </div>
}

<h2>Weather Forecasts</h2>

<span class="pl-c">//... the weather forecast table

@code {
    private bool addForm = false;
    private WeatherForecastEditorForm form = default!;

    protected override async Task OnInitializedAsync()
    {
        await Service.GetRecordsAsync();
        this.Service.ListUpdated += OnListUpdate;
    }

    private void ShowAddForm()
    {
        addForm = true;
        form.ShowForm();
    }

    private void OnFormClosed()
        => addForm = false;

    private void OnListUpdate(object? sender, EventArgs e)
        => this.InvokeAsync(StateHasChanged);

    public void Dispose()
        => this.Service.ListUpdated += OnListUpdate;
}

Summary

Some notes:

  1. You don't need to call StateHasChanged in UI event handlers such as Exit and ShowForm: the ComponentBase UI event handler calls them automatically. The only call in the code is in ShowForm in the editor. This is a standard method so there's no automated calls.

  2. All calls into the data pipeline return Result objects. These provide a mechanism for returning both the result and status information about the request.

  3. Using events in the View layer provide a simple mechanism for maintaining state. It's very easy to shortcut the design process and start wiring components together. But you quickly code an unmanaged mess that's impossible to debug and cluttered with calls to StateHasChanged to try (and often fail to) keep everything in sync.

Appendix

What's Missing

As I said in the introduction, this code is For Demo Purposes. There's nothing wrong with it: I just kept it simple. Here are a few complexities that would appear in my production code:

  1. My services would be heavy on generics to boilerplate a lot of the code.
  2. View to Data services would be implemented through interfaces to decouple the Core/Business domain code from the Data domain code.
  3. Each code domain would reside in different projects to enforce dependency rules.
  4. Collection requests would always be constrained with request objects defining paging.
  5. Componentization of UI. For example, the Add a New Record block would be a component or RenderFragment block.
This article was originally posted at https://github.com/ShaunCurtis/Blazr.Data

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Retired Cold Elm
United Kingdom United Kingdom
Ex Geologist, Project Manager, Web Hoster, Business Owner and IT Consultant. Now, a traveller to places less travelled. And part time developer trying to keep up!

If you feel like saying thanks, the next time you see a charity request, DONATE. No matter how small, no matter who, it'll count. If you have a choice in the UK, Barnados.

Comments and Discussions

 
QuestionC++ Pin
2022 Dude15-Aug-22 1:16
2022 Dude15-Aug-22 1:16 
AnswerRe: C++ Pin
Shaun C Curtis18-Aug-22 4:24
mvaShaun C Curtis18-Aug-22 4:24 
Sorry but I don't know!

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.