Component is not re-rendered after invoke StateHasChanged - c#

I have read the article "3 Ways to Communicate Between Components in Blazor" and have tried to do the same.
I have message component under the #body and depending on user actions in the #body components message has to be changed
#inject ClientMessage clientMessage
#inherits LayoutComponentBase
#using Site.Shared.Components
<div class="sidebar">
<AdminMenu />
</div>
<div class="main">
<div class="content px-4">
#Body
</div>
<Message/>
</div>
#code
{
protected async Task ChangeState()
{
await InvokeAsync(StateHasChanged);
}
protected override void OnInitialized()
{
clientMessage.MsgChange += ChangeState;
}
}
Message component:
#inject ClientMessage clientMessage
<div style="#(IsVisble ? "display:block" : "display:none")" class="#MsgClass" role="alert">
#if (clientMessage != null)
{
#clientMessage.Message
}
</div>
#code {
public bool IsVisble
{
get
{
if (string.IsNullOrEmpty(#clientMessage.Message))
{
return false;
}
return true;
}
}
public string MsgClass
{
get
{
if (clientMessage == null)
{
return string.Empty;
}
string msgClass;
switch (clientMessage.MsgType)
{
case EMsgType.Info:
msgClass = "alert alert-info";
break;
case EMsgType.Success:
msgClass = "alert alert-success";
break;
case EMsgType.Warning:
msgClass = "alert alert-warning";
break;
case EMsgType.Error:
msgClass = "alert alert-danger";
break;
case EMsgType.NoMsg:
default:
msgClass = string.Empty;
break;
}
return msgClass;
}
}
}
Message class
public class ClientMessage
{
public event Func<Task> MsgChange;
public ClientMessage(string msg, EMsgType msgType)
{
this.Message = msg;
this.MsgType = msgType;
NotifyStateChanged();
}
public void SetMsg(string msg, EMsgType msgType)
{
this.Message = msg;
this.MsgType = msgType;
NotifyStateChanged();
}
public string Message { get; set; }
public EMsgType MsgType { get; set; }
private void NotifyStateChanged()
{
if (MsgChange != null)
{
MsgChange.Invoke();
}
}
}
ClientMessage class is injected as a singleton by DI. If I invoke SetMsg(newMsgm, msgType) in the #body components then ChangeState() method is invoked but nothing happened, I mean component is not re-rendered. If I instead "InvokeAsync" use "Invoke" I have an error "The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.". If I reload the page I can see Message.
What do I wrong and how can I force to re-render message component?

I resolved this issue. Maybe it will help someone.
I localized the problem - when #body was changed after NavigationManager.NavigateTo("some_page") had been invoked StateHasChanged() does not re-rendered message component. I tried different places where I can fire StateHasChanged() and if I moved it to Message.razor it stars to work as expected.
It is better to read the documentation at the beggining than articles:)

Related

calling a JS function for use in razor component code file

I am attempting to use some JS code in my blazor client side components, I have followed some examples online but cannot get it to work.
Component.razor.cs
namespace MyApp.Web.Components.Select
{
public partial class Select
{
[Parameter]
public RenderFragment ChildContent { get; set; } = default!;
[Inject]
public IJSRuntime JSRuntime { get; set; }
private IJSObjectReference jsModule;
public void OnClick()
{
ShowAlertWindow();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
try
{
jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./scripts/test.js");
}
catch (Exception ex)
{
System.Console.WriteLine($"Failed to load JS module. Error: {ex}");
}
}
public void ShowAlertWindow()
{
System.Console.WrteLine("Showing Alert Window"); // this works
JSRuntime.InvokeVoidAsync("showAlert", "hello"); // this does nothing
}
}
}
Component.razor
<div class="wrapper">
<div #onclick="#OnClick">
#ChildContent
</div>
</div>
wwwroot/scripts/test.js
export function showAlert(message) {
alert(message);
}
At run time, the click should run the custom showAlert function in the JS file. but nothing seems to happen, no errors in browser dev console neither.
What I am doing wrong?
You have to use the IJSObjectReference that you obtained inside OnAfterRenderAsync to invoke the showAlert function:
jsModule.InvokeVoidAsync("showAlert", "hello");

Blazor Server - Local Storage after login

I'm working on a Blazor Server project using the default Microsoft Identity Platform.
My goal is to get/create a user in my db and save it to local storage after microsoft login is completed.
In the startup I'm sucessfully able to use the OnTokenValidated event to do some action after login. However it's not possible to write to local storage in this stage since the page isn't rendered yet.
I'd like to do something like this which is possible with Webassembly.
<RemoteAuthenticatorView Action="#Action" OnLogInSucceeded="SomeCode" />
Does anyone know a way to do this without using a solution like adding OnAfterRenderAsync in the MainLayout, which will fire on each page reload. I'd like to call a method after the Identity login redirects back to my site in a state where LocalStorage is accessible.
You do need to put some code in OnAfterRenderAsync, but in this demo I've put it in App and check if the component has already rendered.
Here's a demo and some code to interact with LocalStorage. You should be able to adapt it to fit your needs.
First a service to encapsulate getting and setting to Local Storage
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
public class LocalStorageService
{
private readonly ProtectedLocalStorage _storage;
public LocalStorageService(ProtectedLocalStorage storage)
=> _storage = storage;
public async ValueTask<CommandResult> SaveAsync<TRecord>(CommandRequest<TRecord> request)
{
if (request.Record is not null)
await _storage.SetAsync(request.StorageName, request.Record);
// No return so we return success!
return CommandResult.Success();
}
public async ValueTask<RecordQueryResult<TRecord>> ReadAsync<TRecord>(RecordQueryRequest<TRecord> request)
{
// We need to cover the situation were the component calling this is in the initial page
// and Blazor server is trying to statically render the page
try
{
var result = await _storage.GetAsync<TRecord>(request.StorageName);
return new RecordQueryResult<TRecord> { Successful = result.Success, Record = result.Value, Message = $"Failed to retrieve a value for {request.StorageName}" };
}
catch
{
return new RecordQueryResult<TRecord> { Successful = false, Message = $"Failed to retrieve a value for {request.StorageName}" };
}
}
}
The CQS Request and Result objects:
public record CommandRequest<TRecord>(string StorageName, TRecord Record);
public record RecordQueryRequest<TRecord>(string StorageName);
public record CommandResult
{
public bool Successful { get; init; }
public string Message { get; init; } = string.Empty;
public static CommandResult Success()
=> new CommandResult { Successful = true };
public static CommandResult Failure(string message)
=> new CommandResult { Successful = false };
}
public record RecordQueryResult<TRecord>
{
public TRecord? Record { get; init; }
public bool Successful { get; init; }
public string Message { get; init; } = string.Empty;
public static RecordQueryResult<TRecord> Success(TRecord record)
=> new RecordQueryResult<TRecord> { Record = record, Successful = true };
public static RecordQueryResult<TRecord> Failure(string message)
=> new RecordQueryResult<TRecord> { Successful = false };
}
Registered like this:
builder.Services.AddScoped<LocalStorageService>();
My simple Data:
public record TestData( string LastSaved);
Add code to App to set as if you are getting data after login. This implements a custom after render handler.
#inject LocalStorageService Service
#implements IHandleAfterRender
<CascadingAuthenticationState>
<Router AppAssembly="#typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)" />
<FocusOnNavigate RouteData="#routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="#typeof(MainLayout)">
<p role="alert">Sorry, theres nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
#code {
private bool _hasCalledOnAfterRender;
// implements a custom IHandleAfterRender handler
async Task IHandleAfterRender.OnAfterRenderAsync()
{
// Only do if first render and the data in local storage is empty
if (!_hasCalledOnAfterRender && !await GetData())
{
var newData = new TestData($"Saved at {DateTime.Now.ToLongTimeString()}");
var result = await this.Service.SaveAsync<TestData>(new CommandRequest<TestData>("TestData", newData));
_hasCalledOnAfterRender = true;
}
}
private async Task<bool> GetData()
{
var result = await this.Service.ReadAsync<TestData>(new RecordQueryRequest<TestData>("TestData"));
return result?.Successful ?? false;
}
}
And my test route/page to display the data.
#page "/"
#inject LocalStorageService Service
#implements IDisposable
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<div class="bg-black text-white m-3">
Test Data Last Saved at : #this.data.LastSaved
</div>
<div class="m-3">
<button class="btn btn-primary" #onclick=SaveToLocal>Save Data to Local</button>
</div>
#code {
private TestData data = new TestData(string.Empty);
protected override async Task OnInitializedAsync()
{
await this.GetData();
this.Service.StorageChanged += this.DataChanged;
}
private async void DataChanged(object? sender, EventArgs e)
{
await this.GetData();
await this.InvokeAsync(StateHasChanged);
}
private async Task<bool> GetData()
{
var result = await this.Service.ReadAsync<TestData>(new RecordQueryRequest<TestData>("TestData"));
data = result?.Record ?? new TestData(string.Empty);
return result?.Successful ?? false;
}
private async Task SaveToLocal()
{
var newData = new TestData($"Saved at {DateTime.Now.ToLongTimeString()}");
var result = await this.Service.SaveAsync<TestData>(new CommandRequest<TestData>("TestData", newData));
await this.GetData();
}
public void Dispose()
=> this.Service.StorageChanged -= this.DataChanged;
}

onpaste input event in blazor and typescript

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);
}
}

Exception not handling globally in .net core blazor

I have API calls utility in my blazor web project. I have added condition where if I get unauthorized response from API, I am throwing the unauthorized error and trying to catch it in program.cs file so I can redirect user to login page. while throwing error blazor engine returning error in browser.
Utitlity.cs
public async Task<CurrentResponse> GetAsync(IHttpClientFactory _httpClient, string url)
{
try
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Clear();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", GetClaimValue(CustomClaimTypes.AccessToken));
var client = _httpClient.CreateClient("FSMAPI");
HttpResponseMessage httpResponseMessage = await client.SendAsync(request);
if(httpResponseMessage.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
ManageUnAuthorizedError();
}
CurrentResponse response = JsonConvert.DeserializeObject<CurrentResponse>(httpResponseMessage.Content.ReadAsStringAsync().Result);
return response;
}
catch (Exception exc)
{
throw exc;
}
}
private void ManageUnAuthorizedError(/*IHttpClientFactory _httpClient*/)
{
throw new UnauthorizedAccessException(HttpStatusCode.Unauthorized.ToString());
}
Program.cs
app.UseExceptionHandler(c => c.Run(async context =>
{
var exception = context.Features
.Get<IExceptionHandlerPathFeature>()
.Error;
var response = new { error = exception.Message };
if(exception.Message == HttpStatusCode.Unauthorized.ToString())
{
context.Response.Redirect("/Login");
}
}));
Here's a possible solution based on the information you've provided in the question.
You need to interact with the UI through Services
A notification service:
public class NeedToAuthenticateService
{
public string ErrorMessage { get; set; } = string.Empty;
public event EventHandler? AuthenticationRequired;
public void NotifyAuthenticationRequired()
=> AuthenticationRequired?.Invoke(this, new EventArgs());
}
This is a "simple" emulation of your API call done through a service that interfaces with the NeedToAuthenticateService and raises the AuthenticationRequired event.
public class APIReaderService
{
private NeedToAuthenticateService needToAuthenticateService;
public APIReaderService(NeedToAuthenticateService needToAuthenticateService)
{
this.needToAuthenticateService = needToAuthenticateService;
}
public void GetData()
{
// If you get an error
needToAuthenticateService.ErrorMessage = "You need to log in!";
needToAuthenticateService.NotifyAuthenticationRequired();
}
}
A simple demo Login page showing the message.
#page "/Logon"
<h3>Logon</h3>
#inject NeedToAuthenticateService needToAuthenticateService
<div class="p-3">
#this.needToAuthenticateService.ErrorMessage
</div>
#code {
}
A modified MainLayout page which registers and event handler with NeedToAuthenticateService and triggers a navigate event when AuthenticationRequired is raised.
#inherits LayoutComponentBase
#inject NeedToAuthenticateService needToAuthenticateService
#inject NavigationManager NavManager
#implements IDisposable
<PageTitle>BlazorApp1</PageTitle>
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
About
</div>
<article class="content px-4">
#Body
</article>
</main>
</div>
#code {
protected override void OnInitialized()
=> this.needToAuthenticateService.AuthenticationRequired += GoToLogIn;
private void GoToLogIn(object? sender, EventArgs e)
=> NavManager.NavigateTo("/Logon");
public void Dispose()
=> this.needToAuthenticateService.AuthenticationRequired -= GoToLogIn;
}
And finally the registered services in Program
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddScoped<APIReaderService>();
builder.Services.AddScoped<NeedToAuthenticateService>();

Display wait or spinner on API call

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);

Categories

Resources