Reputation: 8506
My question is similar to this one, but instead of pertaining to a Blazor server app, I'm asking in the context of a Blazor webassembly app. I realize that there is only one (UI) thread in this browser execution context, but I figure there must be some kind of framework for a worker or background service. All my googling has come up empty.
I simply need to kick off a background service that polls a web API continually every second for the lifetime of the app.
Upvotes: 4
Views: 4495
Reputation: 1516
SpawnDev.BlazorJS.WebWorkers adds multithreading via Workers and SharedWorkers to Blazor WebAssembly. (I am the developer.) It's as simple as adding the Nuget package, slight modification to you Program.cs and then you can call any registered service interface on a Worker or SharedWorker thread.
// Program.cs
// ...
using SpawnDev.BlazorJS;
using SpawnDev.BlazorJS.WebWorkers;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
// Add SpawnDev.BlazorJS.BlazorJSRuntime
builder.Services.AddBlazorJSRuntime();
// Add SpawnDev.BlazorJS.WebWorkers.WebWorkerService
builder.Services.AddWebWorkerService();
// Add services that will be called on the main thread and/or worker threads (Worker services must use interfaces)
builder.Services.AddSingleton<IMathsService, MathsService>();
// build and Init using BlazorJSRunAsync (instead of RunAsync)
await builder.Build().BlazorJSRunAsync();
Then use it
[Inject]
WebWorkerService workerService { get; set; }
// Get a new WebWorker
var webWorker = await workerService.GetWebWorker();
// Get WebWorker service proxy
var workerMathService = webWorker.GetService<IMathsService>();
// Call async methods on your worker service
var result = await workerMathService.CalculatePi(piDecimalPlaces);
I am currently using it for my OpenCVSharp Blazor app that allows the video processing to happen in multiple background workers.
GitHub repo: https://github.com/LostBeard/SpawnDev.BlazorJS
Nuget page: https://www.nuget.org/packages/SpawnDev.BlazorJS.WebWorkers
Notes: Supports .Net 6, 7, and 8. Supports SharedWorkers. Does not require special HTTP headers. Works on all browsers that support Workers and WebAssembly. See GitHub page for more information.
Upvotes: 5
Reputation: 2601
I see two different approaches. The first is a simple timer-based call in your AppCompontent
. The second is to create a javascript web worker and call it via interop.
Timer-based in App
Component
@inject HttpClient client
@implements IDisposable
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
@code {
private async void DoPeriodicCall(Object state)
{
//a better version can found here https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#timer-callbacks
var response = await client.GetFromJsonAsync<Boolean>("something here");
//Call a service, fire an event to inform components, etc
}
private System.Threading.Timer _timer;
protected override void OnInitialized()
{
base.OnInitialized();
_timer = new System.Threading.Timer(DoPeriodicCall, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
}
public void Dispose()
{
//maybe to a "final" call
_timer.Dispose();
}
}
The result can be observed in the developer tools.
Javascript Web worker
A good starting point for background workers can be found here.
If you want to use the result of the call in your WASM app, you need to implement JS interop. The App
component calls a javascript method that starts the worker. The javascript method has three inputs, the URL, the interval, and the reference to the App
component. The URL and interval are wrapped inside a "cmd" object and passed to the worker when the worker starts. When the worker finished the API call, it sends a message to the javascript back. This javascript invokes a method on the app component.
// js/apicaller.js
let timerId;
self.addEventListener('message', e => {
if (e.data.cmd == 'start') {
let url = e.data.url;
let interval = e.data.interval;
timerId = setInterval( () => {
fetch(url).then(res => {
if (res.ok) {
res.json().then((result) => {
self.postMessage(result);
});
} else {
throw new Error('error with server');
}
}).catch(err => {
self.postMessage(err.message);
})
}, interval);
} else if(e.data.cmd == 'stop') {
clearInterval(timerId);
}
});
// js/apicaller.js
window.apiCaller = {};
window.apiCaller.worker = new Worker('/js/apicallerworker.js');
window.apiCaller.workerStarted = false;
window.apiCaller.start = function (url, interval, dotNetObjectReference) {
if (window.apiCaller.workerStarted == true) {
return;
}
window.apiCaller.worker.postMessage({ cmd: 'start', url: url, interval: interval });
window.apiCaller.worker.onmessage = (e) => {
dotNetObjectReference.invokeMethodAsync('HandleInterval', e.data);
}
window.apiCaller.workerStarted = true;
}
window.apiCaller.end = function () {
window.apiCaller.worker.postMessage({ cmd: 'stop' });
}
You need to modify the index.html to reference the apicaller.js script. I'd recommend including it before the blazor framework to make sure it is available before.
...
<script src="js/apicaller.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
...
The app component needs to be slightly modified.
@implements IAsyncDisposable
@inject IJSRuntime JSRuntime
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
@code {
private DotNetObjectReference<App> _selfReference;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
_selfReference = DotNetObjectReference.Create(this);
await JSRuntime.InvokeVoidAsync("apiCaller.start", "/sample-data/weather.json", 1000, _selfReference);
}
}
[JSInvokable("HandleInterval")]
public void ServiceCalled(WeatherForecast[] forecasts)
{
//Call a service, fire an event to inform components, etc
}
public async ValueTask DisposeAsync()
{
await JSRuntime.InvokeVoidAsync("apiCaller.stop");
_selfReference.Dispose();
}
}
In the developer tools, you can see a worker does the calls.
Concurrency, multithreading and other concerns
The worker is a true multithreading approach. The thread pooling is handle by the browser. Calls within the worker won't block any statements in the "main" thread. However, it is not as convenient as the first approach. What method to choose depends on your context. As long as your Blazor application wouldn't do much, the first approach might be a reasonable choice. In case your Blazor app already has tons of things to do, offloading to a worker can be very beneficial.
If you go for the worker solution but need a non-default client like with authentication or special headers, you would need to find a mechanism to synchronize the Blazor HttpClient
and the calls to the fetch
API.
Upvotes: 9