JSRuntime в Blazor WebAssembly блокирует обновления DOM при первой загрузке

Я использую последнюю версию Blazor WebAssembly 3.2.0 Release Candidate

В шаблоне vanilla Blazor WASM я внес следующие изменения для тестирования / отладки проблемы:

В Program.cs я добавляю следующий синглтон, который будет вставлен на все страницы:

builder.Services.AddSingleton<ApplicationState>();

Класс выглядит так:

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

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

}

и вводится в _Imports.razor вместе с JSRuntime, поэтому они доступны для всех страниц:

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

Чтобы проверить, правильно ли проходит объект ApplicationState через приложение, я использовал его в следующих двух местах:

В NavManu.razor я оборачиваю один из пунктов меню вокруг BoolValue, чтобы показать / скрыть этот элемент:

@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>
}

А в Index.razor я добавил тег H2 под заголовком Hello World по умолчанию, чтобы отобразить StringValue.

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

У меня также есть файл javascript, расположенный в wwwroot/js/extensions.js, в нем есть единственная функция, используемая для получения данных cookie:

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

И включен как сценарий в wwwroot/index.html прямо под сценарием _framework/blazor.webassembly.js:

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

В App.razor у меня есть следующий блок @code:

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

Все работает, как ожидалось, и пункт меню виден, как и строковое значение в теге H2.

Однако, если я добавлю вызов моей функции Javasript через JSRuntime, например:

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

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

Запускается javascript, и значения AppState обновляются (я добавил журнал консоли, чтобы проверить это), НО навигация не обновляется, чтобы отображать элемент меню, а тег H2 не показывает новое строковое значение...

Если вы перейдете на страницу в меню, состояние обновится, но не изменится, если вы выполните жесткое обновление....

Я попытался добавить вызов StateHasChanged(), но это не сработало:

@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!
    }
}

Поскольку состояние, казалось, обновлялось после навигации, я также попытался вызвать событие навигации, также безрезультатно:

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

Я продолжаю пробовать еще кое-что, но ничего не помогает. StateHasChanged() действительно должен был быть исправлением для чего-то вроде этого, в моем понимании. Что еще я должен попробовать?

ОБНОВИТЬ

Как указала enet, мне нужно уведомлять нижестоящие компоненты об изменении состояния. Проблема была вызвана не JSRuntime, а задержкой, созданной в App.razor, которая привела к тому, что состояние объекта при жестком обновлении изменилось. В первом сценарии объект был обновлен ДО того, как другие компоненты начали рендеринг, поэтому они использовали уже обновленное состояние. Но как только возникала какая-либо задержка (например, вызов JSRuntime), им требовалось уведомление, чтобы обновить свое состояние, потому что у них была возможность выполнить рендеринг до задержки с использованием более старого состояния. Я заменил вызов JSRuntime на Task.Delay() на несколько миллисекунд, и это привело к той же проблеме! Это решило добавление триггера к событию.

1 ответ

Решение

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

Примечание. Служба является обычным классом... Она предоставляет службы другим объектам, и ее следует добавить в контейнер DI в методе Startup.ConfigureServices, чтобы сделать его доступным для запрашивающих клиентов. Добавьте это: в метод ConfigureServices:

 services.AddScoped<MessageService>();

Примечание. Как вы можете видеть, я определяю делегат события типа Action, который вызывается из метода доступа set свойства, когда пользователь вводит текст в текстовое поле в Component3. При запуске этого делегата текст, введенный Components3, будет отображаться в компоненте Index, который является родительским для Component2 (см. Код ниже).

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


 }
}

Обратите внимание, что мы напрямую привязываемся к свойству MessageService.Message, но для обновления отображения текста необходимо вызвать метод StateHasChanged.

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

     }
 }

Обратите внимание, что в Component3 мы привязываем MessageService.Message к текстовому полю, и эта привязка происходит каждый раз, когда вы нажимаете на клавиатуру (событие ввода по сравнению с событием изменения).

Вот и все, надеюсь, это поможет, и не стесняйтесь задавать любые вопросы.

Другие вопросы по тегам