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 к текстовому полю, и эта привязка происходит каждый раз, когда вы нажимаете на клавиатуру (событие ввода по сравнению с событием изменения).
Вот и все, надеюсь, это поможет, и не стесняйтесь задавать любые вопросы.