Jexodus
Jexodus

Reputation: 44

BackgroundService event to trigger UI update in Blazor Server

Question Up Front: I am looking for a way to trigger a UI update from a BackgroundService in a Blazor Server app.

I have a Blazor Server app that displays data from a DB. The data in the DB is sourced from an external API and transformed/calculated prior to storing/updating. As this is an ongoing process and user agnostic, I have implemented this using a BackgroundService (First time using background services).

public class DataManagementService(IServiceScopeFactory scopeFactory, IApiService apiService, IEncryptionService encryptionService) : BackgroundService 
{
    private readonly IApiService _apiService = apiService;
    private readonly IEncryptionService _encryptionService = encryptionService;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        Log.Information("================= DataManagementService Starting =================");
        Log.Information("================= Connecting to API =================");
            
        await ConnectToApi();
        
        while (!stoppingToken.IsCancellationRequested)
        {
            Log.Information("================= Data Update Starting =================");
            await UpdateData();
            Log.Information("================= Data Update Completed =================");
            await Task.Delay(60000, stoppingToken);
        }
    }
}

The BackgroundService is functioning correctly in relation to updating the data. During some iterations of the data updating process, there may be information that needs to be updated within the UI. Is there a way to trigger a UI update from the BackgroundService? My initial thought was that I may be able to use events to achieve this and have the UI subscribed to those events, but I have not been able to find any information that supports this theory.

The way I currently have this working is by using a crude implementation of running a UI update every minute; however, I am looking for a more efficient, reactive (no time lag between update event and display) and arguably "proper" way.

Grateful for any assistance or guidance you can provide.

Upvotes: 0

Views: 48

Answers (2)

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30310

It's fairly simple. Here's a demo that gets the time as a string and pushes it to the Home page.

First, a singleton service for the front end to consume:


public class MyTimeProvider
{
    public event EventHandler<string>? TimeChanged;

    public void OnTimerUpdated(object? data)
    {
        TimeChanged?.Invoke(this, DateTime.Now.ToLongTimeString());
    }
}

My Background Service that does the timer stuff.

public class TimeBackgroundService : BackgroundService, IDisposable
{
    private Timer? _timer;
    private readonly MyTimeProvider _timeProvider;
    private CancellationToken _cancellationToken = new();
    private bool _processing;
    public TimeBackgroundService(MyTimeProvider timeProvider)
    {
        _timeProvider = timeProvider;
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _cancellationToken = stoppingToken;
        _timer = new Timer(OnTimerCallback, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
        return Task.CompletedTask;
    }

    private void OnTimerCallback(object? data)
    {
        if (_cancellationToken.IsCancellationRequested)
        {
            _timer?.Change(Timeout.Infinite, Timeout.Infinite);
            return;
        }

        if (_processing)
            return;

        _processing = true;

        //Do some work here
        _timeProvider.OnTimerUpdated(data);

        _processing = false;
    }

    public void dispose()
    {
        _timer?.Dispose();
    }
}

Service registration:


builder.Services.AddHostedService<TimeBackgroundService>();
builder.Services.AddSingleton<MyTimeProvider>();

And then Home.razor:

@page "/"
@inject MyTimeProvider _service
@implements IDisposable

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<p>@_message</p>

@code {
    private string _message = "Hello, world!";

    protected override void OnInitialized()
    {
        _service.TimeChanged += OnServiceEvent;
    }

    private void OnServiceEvent(object? sender, string message)
    {
        _message = message;
        // Need to update the UI.  Use InvokeAsync to ensure it's executed on the Sync Context
        this.InvokeAsync(this.StateHasChanged);
    }

    public void Dispose()
    {
        _service.TimeChanged -= OnServiceEvent;
    }
}

Note:

You can use a timer to implement the loop. It uses a processing flag to prevent a possible race condition in the loop.

Upvotes: 2

Mayur Ekbote
Mayur Ekbote

Reputation: 2080

Use MVVM toolkit, it conforms to the publisher/subscriber pattern and it will absolve you of many of the scope/dependency headaches.

https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/messenger

Upvotes: 0

Related Questions