skip to Main Content

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


  1. 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

    Login or Signup to reply.
  2. Your Question Edit states:

    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?

    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:

    async Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem item, object? obj)
    {
        var uiTask = item.InvokeAsync(obj);
    
        var isCompleted = uiTask.IsCompleted || uiTask.IsCanceled;
    
        if (!isCompleted)
        {
            this.StateHasChanged();
            await uiTask;
        }
    
        this.StateHasChanged();
    }
    

    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 calling StateHasChanged: the Renderer renders the component. Once SetAsync completes, the handler executes to completion, calls StateHasChanged and the component is rendered a second time.

    To demonstrate override the existing handler with this one in your component.

    async Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem item, object? obj)
    {
        await item.InvokeAsync(obj);
    }
    

    Now you have to manually call StateHasChanged in TestLocalStorage to renfer the component.

    See this answer for more detail – https://stackoverflow.com/a/76729783/13065781

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search