Problem
ProtectedLocalStorage injected property hangs when trying to set a property. I was able to replicate with a default server Blazor app with the following code in the front page:
@page "/"
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?"/>
<button @onclick="TestLocalStorage">@GetValue</button>
@code {
[Inject]
ProtectedLocalStorage LocalStorage { get; set; }
private int counter;
public int GetValue
{
get
{
if (counter > 0)
{
var inte = LocalStorage.GetAsync<int>("counter");
return inte.Result.Value;
}
return 0;
}
}
private async Task TestLocalStorage()
{
counter++;
await LocalStorage.SetAsync("counter", counter);
await InvokeAsync(StateHasChanged);
}
}
If you click debug and stop at counter++
and try to step through the code, you’ll find that it just hangs on await LocalStorage.SetAsync("counter", counter);
What am I doing wrong?
EDIT
So apparently, and I didn’t know this: "SetAsync" rerenders the components. I’m not sure why and I’d really like to know why. My thinking was that setting a value in local storage just did that, and it didn’t re-render so when I ran up against this, it was a huge question mark on why. Essentially what I need to do is set it and forget it and fetch the value on load in different ways without colliding a "getter" property with reading values directly from local storage.
Abstracting this behind a service doesn’t necessarily fix the problem because regardless: await SetAsync
rerenders. Setting a local storage value should be "set and forget" actually, asynchronously in its own thread and fetching it can rendered without locking. The following now works:
Page 1:
@page "/"
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?"/>
<a href="/CheckValue">GO</a>
<button @onclick="TestLocalStorage">Click Me</button><br />
<span>@counter</span>
@code {
[Inject]
ProtectedLocalStorage LocalStorage { get; set; }
private int counter;
private void TestLocalStorage()
{
counter++;
LocalStorage.SetAsync("counter", counter);
}
}
Now the page that actually loads it onload:
@page "/CheckValue"
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
<h3>CheckValue</h3>
<div>CHECKING VALUE: @Value</div>
@code {
[Inject]
ProtectedLocalStorage ProtectedLocalStorage { get; set; } = default!;
private int Value { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
var val = await ProtectedLocalStorage.GetAsync<int>("counter");
if (val.Success)
{
Value = val.Value;
}
await base.OnAfterRenderAsync(firstRender);
await InvokeAsync(StateHasChanged);
}
}
NOTE on ASYNC and Deadlocks
As some have suggested, I’m aware of deadlocks and Async calls. The .Result
is poor code and I’m certainly not suggesting that that is proper code nor code I should use. The Deadlock
answers on SO do not satisfy this particular post because I understand how that works and is actually a bug that doesn’t require me to get on Stackoverflow to fix. The first answer that explains the await ProtectedLocalStorage.SendAsync
to re-render the components satisfies why I was experiencing issues.
2
Answers
When you
await
on LocalStorage.SetAsync() then Blazor will re-render. All render code is sync but yours uses.Result
. That will deadlock with the InvokeAsync line.General .net/C# : avoid
.Result
and.Wait()
in async code.Blazor: avoid async code in the render section
Your Question Edit states:
Not true.
The
ComponentBase
UI handler re-renders the component.TestLocalStorage
is the handler for<button @onclick="TestLocalStorage">@GetValue</button>
.The handler implemented by
ComponentBase
looks like this:SetAsync
is a true async routine and yields. It’s running on the UI thread [the Synchronisation Context] so the yield allows the next queued activity to execute. This is the render queued by callingStateHasChanged
: the Renderer renders the component. OnceSetAsync
completes, the handler executes to completion, callsStateHasChanged
and the component is rendered a second time.To demonstrate override the existing handler with this one in your component.
Now you have to manually call
StateHasChanged
inTestLocalStorage
to renfer the component.See this answer for more detail – https://stackoverflow.com/a/76729783/13065781