Почему значение Blazor не отображается сразу после его изменения?

Используя элемент управления BlazorWebView ( введение) внутри формы WinForms .NET 8, у меня отображается этот файл Counter.razor:

<p><button @onclick = "IncrementCount">Increment counter</button></p>
<p>@_counterValue</p>

@code
{
    private int _counterValue = 0;

    private void IncrementCount()
    {
        _counterValue++;
    }
}

Правильное поведение

Приведенный выше код работает отлично:

  1. Пользователь нажимает кнопку.
  2. Переменная увеличивается.
  3. На странице отображается новое значение переменной.

Неправильное поведение

Теперь я меняю метод IncrementCount() на этот:

private void IncrementCount()
{
    SynchronizationContext.Current!.Post(
        delegate
        {
            _counterValue++;
        }, 
        null);
}

Теперь он ведет себя так:

  1. Пользователь нажимает кнопку.
  2. Переменная увеличивается (теперь равна «1»).
  3. На странице по-прежнему отображается «0».
  4. Пользователь нажимает кнопку еще раз.
  5. Переменная снова увеличивается. (Теперь это «2»).
  6. На странице отображается цифра «1».
  7. и так далее.

Т.е. У меня всегда ошибка на единицу.

Исправляем это

Чтобы это исправить, мне нужно изменить свой код на:

private void IncrementCount()
{
    SynchronizationContext.Current!.Post(
        delegate
        {
            _counterValue++;
            StateHasChanged();
        }, 
        null);
}

Теперь, после добавления вызова в StateHasChanged(), он ведет себя так, как задумано:

  1. Пользователь нажимает кнопку.
  2. Переменная увеличивается.
  3. На странице отображается новое значение переменной.

Контекст

Вышеупомянутое использование SynchronizationContext является минимальным примером более крупного приложения, в котором мне приходится отправлять вызовы через этот механизм (в первую очередь потому, что мне нужно вызывать другие диалоговые окна WinForms).

Я хочу понять, почему требуется вызов StateHasChanged, поскольку я хочу этого избежать.

В моем реальном приложении (не в приведенном выше минимальном примере) это потребовало бы повторного рендеринга, чтобы оно подходило для приложения.

Мой вопрос

Может ли кто-нибудь объяснить мне, почему поведение отличается при изменении переменной напрямую или при изменении ее через SynchronizationContext?

Есть ли какие-нибудь изменения, чтобы это работало без дополнительного вызова StateHasChanged()?


Обновление 1

В моем реальном приложении я выполняю примерно следующие шаги:

  1. Пользователь нажимает кнопку Blazor.
  2. Отображается форма WinForms.
  3. После закрытия формы мне нужно обновить некоторые значения в Blazor.

Вот почему я пишу свой код внутри вызова SynchronizationContext.Current!.Post(), как рекомендовано здесь.

Я также пытался поместить свой код в очередь и обработать эту очередь в обработчике событий Application.Idle моего приложения WinForms, но это ведет себя точно так же (ошибка отклонения на единицу).

_counterValue — это захваченная переменная в ее текущем состоянии. Если вы увеличите его заранее, а затем сделаете, например, _counterValue++; SynchronizationContext.Current?.Post((state) => _counterValue += 0, null);, вы должны получить тот же результат. В противном случае может показаться, что первый вызов не имеет никакого эффекта, поскольку значение переменной отображается так, как оно было записано.
Jimi 27.03.2024 17:16

Спасибо, @Джими. Я обновил свой вопрос, добавив более подробную информацию.

Uwe Keim 27.03.2024 17:28
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
134
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

IncrementCounter отправляется в контекст синхронизации процессом событий пользовательского интерфейса средства рендеринга. Он передается как callback внутри ComponentBase обработчика пользовательского интерфейса.

Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
    var task = callback.InvokeAsync(arg);
    var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
        task.Status != TaskStatus.Canceled;

    // After each event, we synchronously re-render (unless !ShouldRender())
    // This just saves the developer the trouble of putting "StateHasChanged();"
    // at the end of every event callback.
    StateHasChanged();

    return shouldAwaitTask ?
        CallStateHasChangedOnAsyncCompletion(task) :
        Task.CompletedTask;
}

Полный код можно найти здесь: https://github.com/dotnet/aspnetcore/blob/63c8031b6a6af5009b3c5bb4291fcc4c32b06b10/src/Components/Components/src/ComponentBase.cs#L322

Итак, что вы делаете, это публикуете это

{
    _counterValue++;
} 

как блок кода в очередь контекста синхронизации за IncrementCounter. IncrementCounter выполняет весь рендеринг до выполнения вашего кода и изменяет _counterValue.

При непосредственном изменении мутация происходит до вызова StateHasChanged в обработчике пользовательского интерфейса.

Вопрос: Почему вы хотите разместить код непосредственно в контексте синхронизации?

ComponentBase имеет встроенную функцию InvokeAsync для вызова любого кода, который должен быть выполнен в контексте синхронизации, см. - https://github.com/dotnet/aspnetcore/blob/63c8031b6a6af5009b3c5bb4291fcc4c32b06b10/src/Components/Components/src/ComponentBase.cs# Л178

Примечание [личное]:

Я теперь всегда меняю:

private void IncrementCount()
{ }

К этому:

private Task IncrementCount()
{ 
    //work
    return Task.CompletedTask;
}

Это не позволяет Visual Studio автоматически делать это при вводе await:

private async void IncrementCount()
{ }

Обновлять:

На основании вашего обновления вам необходимо вызвать StateHasChanged опубликованным [анонимным] методом.

Обратите внимание, что вы можете отключить все автоматические вызовы StateHasChanged в компоненте следующим образом:

@page "/counter"
@implements IHandleEvent

@code {
    Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
    {
        return callback.InvokeAsync(arg);
    }
}

Тогда вы звоните StateHasChanged только тогда, когда вам нужно.

Что, вы имеете в виду, что раньше обычно писали «async void» и не могли помешать себе это сделать?! :D

flackoverstow 27.03.2024 17:18

Я не понимаю, почему вы хотите изменить его на Task. Это добавляет много накладных расходов, если вам это никогда не понадобится async. Вы можете просто изменить его на Task вручную, если позже он станет async.

Luke Vo 27.03.2024 17:20

Большое спасибо, @MrCakaShaunCurtis. Я обновил свой вопрос, добавив более подробную информацию.

Uwe Keim 27.03.2024 17:27

@flackoverstow — У меня могут быть моменты старшего возраста, но не настолько старшего! Если вы введете await в метод в Visual Studio, он автоматически добавит асинхронность к методу. Вы можете не заметить. Здесь возникает много-много вопросов о том, где это, вероятно, произошло.

MrC aka Shaun Curtis 27.03.2024 17:28

@LukeVo - накладных расходов не так уж и много, потому что Task.Completed возвращает кешированный синглтон. Я приведу цитату из подробной статьи Стивена Тауба «Как Async/Await действительно работает в C#». Необходимость выделять дополнительный объект только для возврата таких данных является неудачной (обратите внимание, что это было и в случае с APM). Для неуниверсальных методов, возвращающих Task, метод может просто возвращать одноэлементную уже выполненную задачу, и фактически один такой синглтон предоставляется Task в форме Task.CompletedTask.

MrC aka Shaun Curtis 27.03.2024 17:35

@LukeVo — код, который вызывает IncrementCount, это var task = callback.InvokeAsync(arg);

MrC aka Shaun Curtis 27.03.2024 18:06

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