Search code examples
c#blazorblazor-webassembly

Trouble with Async order of execution in App.razor and other razor pages


I am attempting to perform a global setup in the App.razor file, which would read some data from the browser local storage and then assign it some global state class.

However, I have run into a problem with the order of tasks being fulfilled. Consider the code below:

This is what I currently have:

@inject IJSRuntime JSRuntime;

<Router AppAssembly="@typeof(App).Assembly">
    @...
</Router>

@code {
    string token { get; set; } = default!;

    protected async override Task OnInitializedAsync()
    {
        token = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "token");
        stateService.token = token;
        System.Console.WriteLine("Token A : " + token);    
    }
}

The above correctly gets the local storage item, but not before the next page below loads. The problem is the page below needs access to the stateService.token set above.

and then in pages/login.razor.cs:

using Microsoft.AspNetCore.Components;

namespace Pages.Login
{
    public partial class Login : ComponentBase
    {
 
        protected override void OnAfterRender(bool firstRender)
        {
            token = stateService.Token;
            System.Console.WriteLine("Token B: " + token);        
        }
    }
}

The output is:

Token B: 
Token A: TestValueToken

But of course I would need it to be:

Token A: TestValueToken
Token B: TestValueToken

I know why the above is occuring but have no idea thow to fix it. Any suggestions would be welcomed.


Solution

  • You can do that in various ways:

    The following decribes how to do that using a service:

    TokenService.cs

     public class TokenService
        {
            private IJSRuntime JSRuntime;
           
            public TokenService(IJSRuntime JSRuntime)
            {
                this.JSRuntime = JSRuntime;
            }
            public async Task<string> GetToken ()
            {
                return await JSRuntime.InvokeAsync<string> 
                                    ("localStorage.getItem", "token");
            
            }
        }
    

    Program.cs

    builder.Services.AddScoped();

    App.razor (I guess this is not necessary any longer)

    @inject TokenService TokenService;
    
    @code {
     protected override async Task OnInitializedAsync()
      {
             System.Console.WriteLine("Token A : " + await TokenService.GetToken());
    
       }
    
    }
    

    login.razor

    @inject TokenService TokenService;
    
     @code {
         protected override async Task OnInitializedAsync()
                {
        
        
                    System.Console.WriteLine("Token B : " + await TokenService.GetToken());
        
                }
    
        // Or 
      //  protected override async Task OnAfterRenderAsync(bool 
      //                                              firstRender)
      //  {
      //         System.Console.WriteLine("Token B : " + await 
      //                                TokenService2.GetToken());
      //  }
    
      // Not clear why you want it in OnAfterRender{Async}
        
     }
    

    Here's another way how to do that, defining an event handler delegate that is raised when the value of the token has changed.

    TokenService.cs

     public class TokenService
        {
            private string token;
            public event EventHandler TokenSet; 
    
            public void SetToken (string token)
            { 
                this.token = token;
                OnTokenSet(new TokenSetEventArgs(this.token));
            }
    
            protected void OnTokenSet(TokenSetEventArgs e)
            {
                TokenSet?.Invoke(this, e);
            }
        }
    
        public class TokenSetEventArgs : EventArgs
        {
            public string Token { get; set; }
            public TokenSetEventArgs(string token) => Token = token;
    
        }
    

    Program.cs

    builder.Services.AddScoped();

    App.razor

    @inject IJSRuntime JSRuntime;
    
    @inject TokenService TokenService;
    
    
     @code {
            private string token { get; set; } 
    
            protected async override Task OnInitializedAsync()
            {
              await JSRuntime.InvokeVoidAsync("localStorage.setItem", 
                                                    "token", "XXXX" );
    
              token = await JSRuntime.InvokeAsync<string> 
                                   ("localStorage.getItem", "token");
                System.Console.WriteLine("Token A : " + token);
                TokenService.SetToken(token);
           }
       
        }
    

    Login.razor

    @inject TokenService TokenService;
    
    @code
    {
        protected override void OnInitialized()
        {
          // You'll also need to implement IDisposable....
            TokenService.TokenSet += GetToken;
    
        }
    
        private void GetToken(object sender, EventArgs args)
        {
    
            System.Console.WriteLine("Token B : " + ((TokenSetEventArgs)(args)).Token);
    
        }
    }