Blazor async cascading value always null - c#

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.

Related

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

How to create an Eventcallback which has the arguments in blazor?

I need to create a Eventcallback for this following method
public async Task methodA(bool isLoading)
{
IsLoading = isLoading;
this.StateHasChanged();
await Task.CompletedTask;
}
I've tried it using following
EventCallback callback=> EventCallback.Factory.Create(this, methodA);
but I am getting a error called
can't convert method group to EventCallback
I need to create this event callback and pass it to the child components using cascading parameters as following
<CascadingValue Value=callback>
#Body
</CascadingValue>
#code{
EventCallback callback=> EventCallback.Factory.Create(this, methodA);
public async Task methodA(bool isLoading)
{
IsLoading = isLoading;
this.StateHasChanged();
await Task.CompletedTask;
}
}
so what is wrong with above code ??and how to create an eventcallback for above method?

Error while trying to get data from API - Blazor [duplicate]

This question already has answers here:
Blazor problem rendering data returned from Web Api
(2 answers)
Closed 1 year ago.
I built an API in ASP.NET Core and the code looks like this:
public async Task<IEnumerable<Applicant>> GetApplicants()
{
return await appDbContext.Applicants.ToListAsync();
}
[HttpGet]
public async Task<ActionResult> GetApplicants()
{
try
{
return Ok(await applicantRepository.GetApplicants());
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, "Error retreiving data from the database");
}
}
Here we have how it looks in browser(guess that is fine):
Blazor (server) code:
public interface IApplicantService
{
Task<IEnumerable<Applicant>> GetApplicants();
}
public class ApplicantService : IApplicantService
{
private readonly HttpClient httpClient;
public ApplicantService(HttpClient httpClient)
{
this.httpClient = httpClient;
}
public async Task<IEnumerable<Applicant>> GetApplicants()
{
return await httpClient.GetJsonAsync<Applicant[]>("api/applicants");
}
}
public class ApplicantList : ComponentBase
{
[Inject]
public IApplicantService ApplicantService { get; set; }
public IEnumerable<Applicant> Applicants { get; set; }
protected override async Task OnInitializedAsync()
{
Applicants = (await ApplicantService.GetApplicants()).ToList();
}
}
And the page:
#page "/"
#inherits ApplicantList
<h1>Applicants</h1>
<div class="card-deck">
#foreach (var applicant in Applicants)
{
<div class="card m-3" style="min-width: 18rem; max-width:30.5%;">
<div class="card-header">
<h3>#applicant.Name</h3>
</div>
<div class="card-footer text-center">
View
Edit
Delete
</div>
</div>
}
</div>
I am facing null reference error. While debugging I see that Applicants is null
The solution to this is simple: wrap the foreach loop with an if statement like this:
#if( Applicants != null)
{
#foreach (var applicant in Applicants)
{
//...
}
}
Explanation: When you call the ApplicantService.GetApplicants() method in the lifecycle OnInitializedAsync method, the following occurs:
ApplicantService.GetApplicants() is called and awaited, the execution control is yielded to the calling code, till GetApplicants() completes...Blazor starts rendering the view portion of your component, but alas, the Applicants variable is not yet populated with data. It contains the null value, thus the exception.
Note: that when the GetApplicants() method completes, re-rendering occurs again, this time the Applicants variable contains the retrieved data.
Note: Don't use the GetJsonAsync method. Use instead the new set of methods and objects: Install-Package System.Net.Http.Json -Version 5.0.0

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