When a property of my ViewModel is updated, other property is updated asynchronous.
Todo.cshtml:
#page "/todo"
<h1>Todo (#todos.Count(todo => !todo.IsDone))</h1>
<ul>
#foreach (var todo in todos)
{
<li>
<input type="checkbox" bind="#todo.IsDone" />
<input bind="#todo.Title" />
</li>
}
</ul>
<input placeholder="Something todo" bind="#newTodo"/>
<button onclick="#AddTodo">Add todo</button>
#functions {
IList<TodoItem> todos = new List<TodoItem>();
string newTodo;
void AddTodo()
{
if (!string.IsNullOrWhiteSpace(newTodo))
{
todos.Add(new TodoItem { Title = newTodo });
newTodo = string.Empty;
}
}
}
TodoItem.cs:
public class TodoItem
{
private bool _isDone;
public string Title { get; set; }
public bool IsDone
{
get => _isDone;
set
{
_isDone = value;
Task.Run(() =>
{
//Simulate work
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));
//Update property
Title = Title + " - Done";
});
}
}
}
In synchronous (without Task.Run) this work fine, but in asynchronous the UI isn't updated.
I need explain the UI to update with StateHasChanged():
https://github.com/aspnet/Blazor/issues/1413
But I can't call this method in TodoItem (and I don't want TodoItem know Blazor component).
Have you a solution to update the UI?
You should do the following:
Define an action delegate in your class:
public event Action OnChange;
In this very class define a method NotifyStateChanged() as follows:
private void NotifyStateChanged() => OnChange?.Invoke();
This method triggers the OnChange event. You should call this method from your logic after fulfilling whatever task it does.
In your todo Component, add the StateHasChanged method to the event delegate used in your TodoItem class thus:
#functions
{
protected override void OnInit()
{
state.OnChange += StateHasChanged;
}
}
Easy answer is "just fire StateHasChanged(); after modify your var":
Task.Run(() =>
{
//Simulate work
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));
//Update property
Title = Title + " - Done";
StateHasChanged();
});
Because your method is async, rewrite as:
Task.Run(async () => //<--here async
{
//Simulate async work
Task.Run( async () => {
await Task.Run( () => {} ); //<--- await
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));
});
//Update property
Title = Title + " - Done";
StateHasChanged();
});
To avoid anti-pattern and write clean code, your ModelView may have a public event to let know UI it has changed, just connect this event on UI to StateHasChanged();.
I write here the Blazor Counter sample modified to do this:
#page "/counter"
<h1>Counter</h1>
<p>Current count: #currentCount</p>
<button class="btn btn-primary" onclick="#IncrementCount">
Click me #s <!-- here var -->
</button>
#functions {
int currentCount = 0;
string s = "";
void IncrementCount()
{
currentCount++;
Task.Run(() =>
{
//Simulate work
Task.Run( async () => {
await Task.Run( () => {} );
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));}
);
//Update property
s = s + " - Done";
StateHasChanged();
});
}
}
}
Edited
Call StateHasChanged from a thread is not more supported. Just change:
Task.Run(() =>
{
//Simulate work
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));
//Update property
Title = Title + " - Done";
StateHasChanged();
});
By
Invoke( () =>
{
...
Related
Let's say I have the following code in MainLayout.razor
<CascadingValue Value="#(async () => await someclass.GetValueAsnyc())" Name="asyncValue">
<article class="content px-4">
#Body
</article>
</CascadingValue>
And in Index.razor:
<h1>Hello!</h1>
#code{
[CascadingParameter(Name = "asyncValue")] public string AsyncValue{ get; set; }
protected override async Task OnInitializedAsync()
{
//do something with AsyncValue- it is always null
}
}
My assumption is that the declaration of the variable in Index.razor isn't awaiting the cascading value in Mainlayout.razor. It does work is I call the method synchronsly with .Result at the end, but that's not what I would like to do if possible.
Does anyone have any guidance or can point me in the right direction so that the cascading value loads asynchronously?
I think this should help.
First a consumer component. The cascaded value is a Task which may or may not have completed. To consume it you await it. If it's already completed the there's no awaiting to do. It provides the result immediately.
<h3>#this.value</h3>
<button class="btn btn-primary" #onclick=this.OnClick>Get Value Again</button>
#code {
private string value = string.Empty;
[CascadingParameter] private Task<string>? DataTask { get; set; }
protected override async Task OnInitializedAsync()
{
if (this.DataTask is not null)
this.value = await DataTask;
}
private async Task OnClick()
{
if (this.DataTask is not null)
this.value = await DataTask;
}
}
And a test page. I haven't put it in a layout or App as it's easier to see what's happening in a test page. The cascaded value is a Task<string> field that you assign a method to.
#page "/Test"
<PageTitle>Test</PageTitle>
<CascadingValue Value="this.DataTask">
<MyComponent />
</CascadingValue>
#code {
private Task<string>? DataTask;
protected override Task OnInitializedAsync()
{
DataTask = GetSomeDateAsync();
return Task.CompletedTask;
}
private async Task<string> GetSomeDateAsync()
{
await Task.Delay(5000);
return DateTime.Now.ToLongTimeString();
}
}
This is basically how the AuthenticationState cascade works.
The #onpaste is a blazor DOM event executed when content is pasted in an input.
now, I want to get this text content from typescript (blazor doesn't support it).
razor:
#inject IJSRuntime _Js
<input class="otp-input"
#ref="myInputRef"
#onpaste="HandleOnPaste" />
#code {
private ElementReference myInputRef { get; set; }
private async Task HandleOnPaste()
{
var pastedData = await _js.InvokeAsync<string?>("OtpInput.getPastedData")
// DO Something
}
}
ts:
class OtpInput {
static getPastedData(e: ClipboardEvent) {
let pastedData = e.clipboardData!.getData('text');
return pastedData;
}
}
this does not work and return error: TypeError: Cannot read properties of undefined (reading 'clipboardData')
You can use a different approach. Add event listener to the input element using javascript then call dotnet method and pass the event data. I use javascript but you can modify accordingly for typescript.
pasteInteropHelper.js:
export function addOnPasteEventListener(elem, componentInstance) {
elem.onpaste = (e) => {
var text = e.clipboardData.getData('text');
componentInstance.invokeMethod('HandlePaste', text);
}
}
razor component:
#inject IJSRuntime JS
<input #ref="_inputRef" />
#code {
private ElementReference _inputRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./pasteInteropHelper.js");
var selfReference = DotNetObjectReference.Create(this);
await module.InvokeVoidAsync("addOnPasteEventListener", _inputRef, selfReference);
}
}
[JSInvokable]
public void HandlePaste(string text)
{
Console.WriteLine(text);
}
}
I have written a program that updates pages regularly with the Blazor server app.
When this program is executed, the current time and the value from the database table are acquired every second and displayed on the page, but the processing in this timer stops after a certain amount of time.
Although it is not measured accurately, the processing in the timer stops after about 10 to 30 minutes.
However, when I look at the debug tool of the browser, there is no error, and there is no error on visual studio.
I searched for an example like this but couldn't find it and couldn't determine what was causing it.
I want the processing in this timer to work until I explicitly specify it as stopped.
What could be the cause of this?
I'm in trouble because I don't know the cause.
Please give me some advice.
Thank you.
private string Time { get; set; }
protected override void OnInitialized()
{
var timer = new System.Threading.Timer((_) =>
{
Time = DateTime.Now.ToString();
InvokeAsync(() =>
{
// Get the value from the database
databaseValue = TimerProcessGetValue();
StateHasChanged();
});
}, null, 0, 1000);
base.OnInitialized();
}
Process called in Timer
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
using(DbContext = DbFactory.CreateDbContext())
{
timerProcessValue = dbContext.TestTable.Single(x => x.Id == 1).TestValue;
}
return timerProcessValue;
}
postscript
The full text of the target component.
PageRefleshTest.cs
#page "/refresh-ui-manually"
#using System.Threading;
#using TestBlazorServer.Models;
#using Microsoft.EntityFrameworkCore;
#inject IDbContextFactory<SQLbeginnerContext> DbFactory
<h1>#Time</h1>
<h1>TestValue:#databaseValue</h1>
private string Time { get; set; }
private int? databaseValue { get; set; }
protected override void OnInitialized()
{
var timer = new System.Threading.Timer((_) =>
{
Time = DateTime.Now.ToString();
InvokeAsync(() =>
{
// Get the value from the database
databaseValue = TimerProcessGetValue().Result;
StateHasChanged();
});
}, null, 0, 1000);
base.OnInitialized();
}
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
using (var dbContext = DbFactory.CreateDbContext())
{
timerProcessValue = dbContext.TestTable.Single(x => x.Id == 1).TestValue;
}
return timerProcessValue;
}
Implemented using System.Timers.Timer
#page "/Refresh2"
#inject IDbContextFactory<SQLbeginnerContext> DbFactory
#using TestBlazorServer.Models
#using Microsoft.EntityFrameworkCore
#using System.Timers
#implements IDisposable
<h3>PageRefresh2</h3>
<h1>#Time</h1>
<h1>#currentCount</h1>
<h1>TestValue:#databaseValue/h1>
#code {
private int currentCount = 0;
private Timer timer2 = new(1000);
private string Time { get; set; }
private int? databaseValue { get; set; }
protected override void OnInitialized()
{
timer2.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer2.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
Time = DateTime.Now.ToString();
databaseValue = TimerProcessGetValue().Result;
StateHasChanged();
});
}
public void Dispose() => timer2.Dispose();
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
await using (var dbContext = DbFactory.CreateDbContext())
{
timerProcessValue = dbContext.TestTable.Single(x => x.Id == 1).TestValue;
}
return timerProcessValue;
}
}
It could of course be from the SignalR connection failing. That should reconnect however.
The main suspect here is .Result. You should always avoid .Wait() and .Result in async code. Aslo, your DB method is not really async, that might play a role too.
But that 'half hour' could also come from the Garbage collector throwing out the Timer object. See the purple Note on this page. When you have a Timer you should anchor it in a field and Dispose() it.
The next step is to make the eventhandler an async void, this is one of the very few cases that is called for. And then only Invoke the StatehasChanged call.
#implements IDisposable
...
#code {
System.Threading.Timer _timer; // use a private field
protected override void OnInitialized()
{
_timer = new System.Threading.Timer(async (_) =>
{
Time = DateTime.Now.ToString();
databaseValue = await TimerProcessGetValue();
await InvokeAsync(StateHasChanged);
}, null, 0, 1000);
// base.OnInitialized(); -- not needed
}
public void Dispose()
{
_timer?.Dispose();
}
}
And make the Db action actually async:
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
using (var dbContext = DbFactory.CreateDbContext())
{
timerProcessValue =
(await dbContext.TestTable.SingleAsync(x => x.Id == 1))
.TestValue;
}
return timerProcessValue;
}
Only you can test this in-situ, let us know if it works.
I have written a program that updates pages regularly with the Blazor server app.
When this program is executed, the current time and the value from the database table are acquired every second and displayed on the page, but the processing in this timer stops after a certain amount of time.
Although it is not measured accurately, the processing in the timer stops after about 10 to 30 minutes.
However, when I look at the debug tool of the browser, there is no error, and there is no error on visual studio.
I searched for an example like this but couldn't find it and couldn't determine what was causing it.
I want the processing in this timer to work until I explicitly specify it as stopped.
What could be the cause of this?
I'm in trouble because I don't know the cause.
Please give me some advice.
Thank you.
private string Time { get; set; }
protected override void OnInitialized()
{
var timer = new System.Threading.Timer((_) =>
{
Time = DateTime.Now.ToString();
InvokeAsync(() =>
{
// Get the value from the database
databaseValue = TimerProcessGetValue();
StateHasChanged();
});
}, null, 0, 1000);
base.OnInitialized();
}
Process called in Timer
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
using(DbContext = DbFactory.CreateDbContext())
{
timerProcessValue = dbContext.TestTable.Single(x => x.Id == 1).TestValue;
}
return timerProcessValue;
}
postscript
The full text of the target component.
PageRefleshTest.cs
#page "/refresh-ui-manually"
#using System.Threading;
#using TestBlazorServer.Models;
#using Microsoft.EntityFrameworkCore;
#inject IDbContextFactory<SQLbeginnerContext> DbFactory
<h1>#Time</h1>
<h1>TestValue:#databaseValue</h1>
private string Time { get; set; }
private int? databaseValue { get; set; }
protected override void OnInitialized()
{
var timer = new System.Threading.Timer((_) =>
{
Time = DateTime.Now.ToString();
InvokeAsync(() =>
{
// Get the value from the database
databaseValue = TimerProcessGetValue().Result;
StateHasChanged();
});
}, null, 0, 1000);
base.OnInitialized();
}
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
using (var dbContext = DbFactory.CreateDbContext())
{
timerProcessValue = dbContext.TestTable.Single(x => x.Id == 1).TestValue;
}
return timerProcessValue;
}
Implemented using System.Timers.Timer
#page "/Refresh2"
#inject IDbContextFactory<SQLbeginnerContext> DbFactory
#using TestBlazorServer.Models
#using Microsoft.EntityFrameworkCore
#using System.Timers
#implements IDisposable
<h3>PageRefresh2</h3>
<h1>#Time</h1>
<h1>#currentCount</h1>
<h1>TestValue:#databaseValue/h1>
#code {
private int currentCount = 0;
private Timer timer2 = new(1000);
private string Time { get; set; }
private int? databaseValue { get; set; }
protected override void OnInitialized()
{
timer2.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer2.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
Time = DateTime.Now.ToString();
databaseValue = TimerProcessGetValue().Result;
StateHasChanged();
});
}
public void Dispose() => timer2.Dispose();
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
await using (var dbContext = DbFactory.CreateDbContext())
{
timerProcessValue = dbContext.TestTable.Single(x => x.Id == 1).TestValue;
}
return timerProcessValue;
}
}
It could of course be from the SignalR connection failing. That should reconnect however.
The main suspect here is .Result. You should always avoid .Wait() and .Result in async code. Aslo, your DB method is not really async, that might play a role too.
But that 'half hour' could also come from the Garbage collector throwing out the Timer object. See the purple Note on this page. When you have a Timer you should anchor it in a field and Dispose() it.
The next step is to make the eventhandler an async void, this is one of the very few cases that is called for. And then only Invoke the StatehasChanged call.
#implements IDisposable
...
#code {
System.Threading.Timer _timer; // use a private field
protected override void OnInitialized()
{
_timer = new System.Threading.Timer(async (_) =>
{
Time = DateTime.Now.ToString();
databaseValue = await TimerProcessGetValue();
await InvokeAsync(StateHasChanged);
}, null, 0, 1000);
// base.OnInitialized(); -- not needed
}
public void Dispose()
{
_timer?.Dispose();
}
}
And make the Db action actually async:
public async Task<int?> TimerProcessGetValue()
{
int? timerProcessValue;
using (var dbContext = DbFactory.CreateDbContext())
{
timerProcessValue =
(await dbContext.TestTable.SingleAsync(x => x.Id == 1))
.TestValue;
}
return timerProcessValue;
}
Only you can test this in-situ, let us know if it works.
In my Blazor app I am making an API call to a back end server that could take some time. I need to display feedback to the user, a wait cursor or a "spinner" image. How is this done in Blazor?
I have tried using CSS and turning the CSS on and off but the page is not refreshed until the call is completed. Any suggestions would be greatly appreciated.
#functions {
UserModel userModel = new UserModel();
Response response = new Response();
string errorCss = "errorOff";
string cursorCSS = "cursorSpinOff";
protected void Submit()
{
//Show Sending...
cursorCSS = "";
this.StateHasChanged();
response = Service.Post(userModel);
if (response.Errors.Any())
{
errorCss = "errorOn";
}
//turn sending off
cursorCSS = "cursorSpinOff";
this.StateHasChanged();
}
}
Option 1: Using Task.Delay(1)
Use an async method.
Use await Task.Delay(1) or await Task.Yield(); to flush changes
private async Task AsyncLongFunc() // this is an async task
{
spinning=true;
await Task.Delay(1); // flushing changes. The trick!!
LongFunc(); // non-async code
currentCount++;
spinning=false;
await Task.Delay(1); // changes are flushed again
}
Option 1 is a simple solution that runs ok but looks like a trick.
Option 2: Using Task.Run() (not for WebAssembly)
On January'2020. #Ed Charbeneau published BlazorPro.Spinkit project enclosing long processes into task to don't block the thread:
Ensure your LongOperation() is a Task, if it is not, enclose it into a Task and await for it:
async Task AsyncLongOperation() // this is an async task
{
spinning=true;
await Task.Run(()=> LongOperation()); //<--here!
currentCount++;
spinning=false;
}
Effect
Spinner and server side prerendering
Because Blazor Server apps use pre-rendering the spinner will not appear, to show the spinner the long operation must be done in OnAfterRender.
Use OnAfterRenderAsync over OnInitializeAsync to avoid a delayed server-side rendering
// Don't do this
//protected override async Task OnInitializedAsync()
//{
// await LongOperation();
//}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await Task.Run(()=> LongOperation());//<--or Task.Delay(0) without Task.Run
StateHasChanged();
}
}
More samples
Learn more about how to write nice spinner you can learn from open source project BlazorPro.Spinkit, it contains clever samples.
More Info
See Henk Holterman's answer with blazor internals explanation.
Lot's of great discussion surrounding StateHasChanged(), but to answer OP's question, here's another approach for implementing a spinner, universally, for HttpClient calls to a backend API.
This code is from a Blazor Webassembly app...
Program.cs
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<SpinnerService>();
builder.Services.AddScoped<SpinnerHandler>();
builder.Services.AddScoped(s =>
{
SpinnerHandler spinHandler = s.GetRequiredService<SpinnerHandler>();
spinHandler.InnerHandler = new HttpClientHandler();
NavigationManager navManager = s.GetRequiredService<NavigationManager>();
return new HttpClient(spinHandler)
{
BaseAddress = new Uri(navManager.BaseUri)
};
});
await builder.Build().RunAsync();
}
SpinnerHandler.cs
Note: Remember to uncomment the artificial delay. If you use the out-of-the-box Webassembly template in Visual Studio, click the Weather Forecast to see a demo of the spinner in action.
public class SpinnerHandler : DelegatingHandler
{
private readonly SpinnerService _spinnerService;
public SpinnerHandler(SpinnerService spinnerService)
{
_spinnerService = spinnerService;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_spinnerService.Show();
//await Task.Delay(3000); // artificial delay for testing
var response = await base.SendAsync(request, cancellationToken);
_spinnerService.Hide();
return response;
}
}
SpinnerService.cs
public class SpinnerService
{
public event Action OnShow;
public event Action OnHide;
public void Show()
{
OnShow?.Invoke();
}
public void Hide()
{
OnHide?.Invoke();
}
}
MainLayout.razor
#inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
About
</div>
<div class="content px-4">
#Body
<Spinner />
</div>
</div>
</div>
Spinner.razor
Note: To add some variety, you could generate a random number in the OnIntialized() method, and use a switch statement inside the div to pick a random spinner type. In this method, with each HttpClient request, the end user would observe a random spinner type. This example has been trimmed to just one type of spinner, in the interest of brevity.
#inject SpinnerService SpinnerService
#if (isVisible)
{
<div class="spinner-container">
<Spinner_Wave />
</div>
}
#code
{
protected bool isVisible { get; set; }
protected override void OnInitialized()
{
SpinnerService.OnShow += ShowSpinner;
SpinnerService.OnHide += HideSpinner;
}
public void ShowSpinner()
{
isVisible = true;
StateHasChanged();
}
public void HideSpinner()
{
isVisible = false;
StateHasChanged();
}
}
Spinner-Wave.razor
Credit to: https://tobiasahlin.com/spinkit/
Note: There is a Nuget package for this spin kit. The drawback to the Nuget package is that you don't have direct access to the CSS to make tweaks. Here I've tweaked thee size of the spinner, and set the background color to match the site's primary color, which is helpful if you are using a CSS theme throughout your site (or perhaps multiple CSS themes)
#* Credit: https://tobiasahlin.com/spinkit/ *#
<div class="spin-wave">
<div class="spin-rect spin-rect1"></div>
<div class="spin-rect spin-rect2"></div>
<div class="spin-rect spin-rect3"></div>
<div class="spin-rect spin-rect4"></div>
<div class="spin-rect spin-rect5"></div>
</div>
<div class="h3 text-center">
<strong>Loading...</strong>
</div>
<style>
.spin-wave {
margin: 10px auto;
width: 200px;
height: 160px;
text-align: center;
font-size: 10px;
}
.spin-wave .spin-rect {
background-color: var(--primary);
height: 100%;
width: 20px;
display: inline-block;
-webkit-animation: spin-waveStretchDelay 1.2s infinite ease-in-out;
animation: spin-waveStretchDelay 1.2s infinite ease-in-out;
}
.spin-wave .spin-rect1 {
-webkit-animation-delay: -1.2s;
animation-delay: -1.2s;
}
.spin-wave .spin-rect2 {
-webkit-animation-delay: -1.1s;
animation-delay: -1.1s;
}
.spin-wave .spin-rect3 {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}
.spin-wave .spin-rect4 {
-webkit-animation-delay: -0.9s;
animation-delay: -0.9s;
}
.spin-wave .spin-rect5 {
-webkit-animation-delay: -0.8s;
animation-delay: -0.8s;
}
##-webkit-keyframes spin-waveStretchDelay {
0%, 40%, 100% {
-webkit-transform: scaleY(0.4);
transform: scaleY(0.4);
}
20% {
-webkit-transform: scaleY(1);
transform: scaleY(1);
}
}
##keyframes spin-waveStretchDelay {
0%, 40%, 100% {
-webkit-transform: scaleY(0.4);
transform: scaleY(0.4);
}
20% {
-webkit-transform: scaleY(1);
transform: scaleY(1);
}
}
</style>
It's beautiful
To answer the notice in #daniherrera's solution, there is three more elegant solution proposed here.
In short :
Implement INotifyPropertyChanged to the Model and invoke StateHasChanged() on a PropertyChangedEventHandler event property from the Model.
Use delegates to invoke StateHasChanged() on the Model.
Add a EventCallBack<T> parameter to the component or page of the View and assign it to the function that should change the render of the component and their parents. (StateHasChanged() isn't necessary in this one`)
The last option is the most simple, flexible and high level, but choose at your convenience.
Overall, I'll advise to use one of those solutions presented more than the await Task.Delay(1); one if security of your app is a concern.
Edit : After more reading, this link provide a strong explanation on how to handle events in C#, mostly with EventCallBack.
Don't do the same mistake as I did by testing wait spinner using Thread.Sleep(n).
protected override async Task OnInitializedAsync()
{
// Thread.Sleep(3000); // By suspending current thread the browser will freeze.
await Task.Delay(3000); // This is your friend as dani herrera pointed out.
// It creates a new task that completes
// after a specified number of milliseconds.
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
Not just for API call, but for every service call:
SpinnerService:
public class SpinnerService
{
public static event Action OnShow;
public static event Action OnHide;
public void Show()
{
OnShow?.Invoke();
}
public void Hide()
{
OnHide?.Invoke();
}
}
Spinner component:
Your spinner hier, in my case I have MudProgressCircular
#if (IsVisible)
{
<MudProgressCircular Color="Color.Primary"
Style="position: absolute;top: 50%;left: 50%;"
Indeterminate="true" />
}
#code{
protected bool IsVisible { get; set; }
protected override void OnInitialized()
{
SpinnerService.OnShow += ShowSpinner;
SpinnerService.OnHide += HideSpinner;
}
public void ShowSpinner()
{
IsVisible = true;
StateHasChanged();
}
public void HideSpinner()
{
IsVisible = false;
StateHasChanged();
}
}
ServiceCaller:
public class ServiceCaller
{
private readonly IServiceProvider services;
private readonly SpinnerService spinnerService;
public ServiceCaller(IServiceProvider services, SpinnerService spinnerService)
{
this.services = services;
this.spinnerService = spinnerService;
}
public async Task<TResult> CallAsync<TService, Task<TResult>>(Func<TService, TResult> method)
where TService : class
{
var service = this.services.GetRequiredService<TService>();
try
{
spinnerService.Show();
await Task.Delay(500); // ToDo: this line is not necessary
TResult? serviceCallResult = await Task.Run(() => method(service));
return serviceCallResult;
}
finally
{
spinnerService.Hide();
}
}
public async Task CallAsync<TService, TAction>(Func<TService, Action> method)
where TService : class
{
var service = this.services.GetRequiredService<TService>();
try
{
spinnerService.Show();
await Task.Delay(500); // ToDo: this line is not necessary
await Task.Run(() => method(service).Invoke());
}
finally
{
spinnerService.Hide();
}
}
}
How to use it?
#page "/temp"
#inject ServiceCaller serviceCaller;
<h3>Temp Page</h3>
<MudButton OnClick="CallMethodReturnsString">CallMethodReturnsString</MudButton>
<MudButton OnClick="CallVoidMethodAsync">CallVoidMethodAsync</MudButton>
<MudButton OnClick="CallTaskMethodAsync">CallTaskMethodAsync</MudButton>
<MudButton OnClick="CallMany">CallMany</MudButton>
#if (!string.IsNullOrEmpty(tempMessage)){
#tempMessage
}
#code{
string tempMessage = string.Empty;
// call method returns string
private async Task CallMethodReturnsString()
{
await serviceCaller.CallAsync<ITempService, string>(async x => this.tempMessage = await x.RetrieveStringAsync());
}
// call void method
private async Task CallVoidMethodAsync()
{
await serviceCaller.CallAsync<ITempService, Task>(x => () => x.MyVoidMethod());
}
// call task method
private async Task CallTaskMethodAsync()
{
await serviceCaller.CallAsync<ITempService, Task>(x => () => x.TaskMethod());
}
// call many methods
private async Task CallMany()
{
await serviceCaller.CallAsync<ITempService, Action>(x => async () =>
{
this.tempMessage = await x.RetrieveStringAsync();
x.MyVoidMethod();
x.TaskMethod();
});
}
}
use InvokeAsync(StateHasChanged), hopefully it will work.
protected async void Submit()
{
//Show Sending...
cursorCSS = "";
this.StateHasChanged();
response = Service.Post(userModel);
if (response.Errors.Any())
{
errorCss = "errorOn";
}
//turn sending off
cursorCSS = "cursorSpinOff";
await InvokeAsync(StateHasChanged);
}
Blazor Serverside - I needed to call StateHasChanged() to force the frontend to update so the spinner would show before the code moves onto the ajax call.
/* Show spinner */
carForm.ShowSpinner = true;
/* Force update of front end */
StateHasChanged();
/* Start long running API/Db call */
await _carRepository.Update(item);