0
votes

I'm using the latest Blazor WebAssembly 3.2.0 Release Candidate

From a vanilla Blazor WASM template I made the following adjustments to test/debug the issue:

In Program.cs I add the following singleton that will be injected into all pages:

builder.Services.AddSingleton<ApplicationState>();

The class looks like:

public class ApplicationState
{
    public bool BoolValue { get; set; }
    public string StringValue { get; set; }

    public ApplicationState()
    {
        BoolValue = false;
        StringValue = "Default value";
    }

}

and is injected in _Imports.razor along with JSRuntime so they are available to all pages:

@inject IJSRuntime JSRuntime
@inject BlazorApp1.State.ApplicationState AppState;

In order to test that the ApplicationState object is passing through the application properly I have it used in the following 2 locations:

In NavManu.razor I wrap one of the menu items around BoolValue to show/hide that item:

@if (AppState.BoolValue)
{
    <li class="nav-item px-3">
        <NavLink class="nav-link" href="counter">
            <span class="oi oi-plus" aria-hidden="true"></span> Counter
        </NavLink>
    </li>
}

And in Index.razor I added an H2 tag under the default Hello World header to display the StringValue

<h1>Hello, world!</h1>
<h2>@AppState.StringValue</h2>

I also have a javascript file located in wwwroot/js/extenstions.js it has a single function used to get cookie data:

function getCookie(name) {
    var nameEQ = name + "=";
    var ca = document.cookie.split(';');
    for (var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') c = c.substring(1, c.length);
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
    }
    return null;
};

And is included as a script in the wwwroot/index.html right below the _framework/blazor.webassembly.js script:

<script src="_framework/blazor.webassembly.js"></script>
<script src="js/extensions.js"></script>

In App.razor I have the following @code block:

@code
{
    protected override async Task OnInitializedAsync()
    {
        AppState.BoolValue = true;
        AppState.StringValue = "Testing 123...";
    }
}

Everything works as expected and the menu item is visible as well as the string value in the H2 tag.

However if I add a call to my Javasript function via JSRuntime like so:

@code
{
    protected override async Task OnInitializedAsync()
    {
        var jwtToken = await JSRuntime.InvokeAsync<string>("getCookie", "cookieName");

        AppState.BoolValue = true;
        AppState.StringValue = "Testing 123...";
    }
}

The javascript runs, and the AppState values are updated (I added a console log to test this) BUT the navigation does not update to show the menu item and the H2 tag does not show the new string value...

If you navigate to a page in the menu the state will update, but it won't again if you do a hard refresh....

I tried adding a StateHasChanged() call but this did not work:

@code
{
    protected override async Task OnInitializedAsync()
    {
        var jwtToken = await JSRuntime.InvokeAsync<string>("getCookie", "cookieName");

        AppState.BoolValue = true;
        AppState.StringValue = "Testing 123...";
        StateHasChanged(); // Does not work!
    }
}

Since the state seemed to update after navigating I also tried forcing a navigation event, also to no avail:

@code
{
    protected override async Task OnInitializedAsync()
    {
        var jwtToken = await JSRuntime.InvokeAsync<string>("getCookie", "cookieName");

        AppState.BoolValue = true;
        AppState.StringValue = "Testing 123...";

        var uri = new Uri(NavigationManager.Uri).GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
        NavigationManager.NavigateTo(uri);
    }
}

I'm continuing to try a few other things but nothing seems to work. StateHasChanged() really should have been the fix for something like this in my understanding. Anything else I should try?

UPDATE

As enet pointed out I need to notify downstream components when the state has changed. It wasn't the JSRuntime that caused an issue - but it was the DELAY that it created within App.razor that resulted in the object state seeming to be different on a hard refresh. In the first scenario the object was updated BEFORE the other components started rendering so they used the already updated state. But once there was any kind of delay (a call to JSRuntime for example) they required a notification in order to update their state because they had a chance to render prior to the delay using the older state. I replaced the JSRuntime call with a Task.Delay() of a few milliseconds and it resulted in the same issue! Adding a trigger to the event resolved this.

1
The main issue with your code is how to call the StateHasChanged method and from where. I'm going to post an existing code sample which demonstrate it. Just run the code, and see how components communicate with a service, and how they are being re-rendered, then if you have question, don't hesitate to ask.enet

1 Answers

2
votes

MessageService.cs

 public class MessageService
{
    private string message;
    public string Message
    {
        get => message;
        set
        {
            if (message != value)
            {
                message = value;
                if (Notify != null)
                {
                     Notify?.Invoke();
                }

            }
        }
    }

    public event Action Notify;
}

Note: The service is a normal class... It provides services to other objects, and it should be added to the DI container in Startup.ConfigureServices method to make it available to requesting clients. Add this: to the ConfigureServices method:

 services.AddScoped<MessageService>();

Note: As you can see I define an event delegate of the Action type, which is invoked from the property's set accessor, when the user type text into a text box in Component3. Triggering this delegate causes the text entered by Components3 to be displayed in the Index component which is the parent of Component2 (see code below).

Index.razor

  @page "/"

  @inject MessageService MessageService
  @implements IDisposable
  @inject IJSRuntime  JSRuntime

  <p>
   I'm the parent of Component2. I've got a message from my grand 
   child: @MessageService.Message
  </p>

  <Component2 />
  <p>@jwtToken</p>

  @code {
    private string jwtToken;

    protected override async Task OnInitializedAsync()
   {
       // await SetTokenAsync("my token");
       jwtToken = await GetTokenAsync();
       MessageService.Notify += OnNotify;
       MessageService.Message = "A message from Index";
    }

    public void OnNotify()
    {
    InvokeAsync(() =>
    {
        StateHasChanged();
    });
}

public void Dispose()
{
    MessageService.Notify -= OnNotify;
}

public async Task<string> GetTokenAsync()
       => await JSRuntime.InvokeAsync<string>("localStorage.getItem", "authToken");

public async Task SetTokenAsync(string token)
{
    if (token == null)
    {
        await JSRuntime.InvokeAsync<object>("localStorage.removeItem", "authToken");
    }
    else
    {
        await JSRuntime.InvokeAsync<object>("localStorage.setItem", "authToken", token);
    }


 }
}

Note that we directly bind to the MessageService.Message property, but the StateHasChanged method must be called to refresh the display of the text.

Component2.razor

<h3>Component2: I'm the parent of component three</h3>
<Component3/>

@code {

}

Component3.razor

 @inject MessageService MessageService

 <p>This is component3. Please type a message to my grand parent</p>
 <input placeholder="Type a message to grandpa..." type="text"
   @bind="@MessageService.Message" @bind:event="oninput" />

 @code {

     protected override void OnInitialized()
     {

        MessageService.Notify += () => InvokeAsync(() =>
        {
           StateHasChanged();
        });

     }
 }

Note that in Component3 we bind the MessageService.Message to a text box, and the binding occurs each time you press a key board( input event versus change event).

That is all, hope this helps, and don't hesitate to ask any question.