Используя элемент управления 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++;
}
}
Приведенный выше код работает отлично:
Теперь я меняю метод IncrementCount() на этот:
private void IncrementCount()
{
SynchronizationContext.Current!.Post(
delegate
{
_counterValue++;
},
null);
}
Теперь он ведет себя так:
Т.е. У меня всегда ошибка на единицу.
Чтобы это исправить, мне нужно изменить свой код на:
private void IncrementCount()
{
SynchronizationContext.Current!.Post(
delegate
{
_counterValue++;
StateHasChanged();
},
null);
}
Теперь, после добавления вызова в StateHasChanged(), он ведет себя так, как задумано:
Вышеупомянутое использование SynchronizationContext является минимальным примером более крупного приложения, в котором мне приходится отправлять вызовы через этот механизм (в первую очередь потому, что мне нужно вызывать другие диалоговые окна WinForms).
Я хочу понять, почему требуется вызов StateHasChanged, поскольку я хочу этого избежать.
В моем реальном приложении (не в приведенном выше минимальном примере) это потребовало бы повторного рендеринга, чтобы оно подходило для приложения.
Может ли кто-нибудь объяснить мне, почему поведение отличается при изменении переменной напрямую или при изменении ее через SynchronizationContext?
Есть ли какие-нибудь изменения, чтобы это работало без дополнительного вызова StateHasChanged()?
В моем реальном приложении я выполняю примерно следующие шаги:
Вот почему я пишу свой код внутри вызова SynchronizationContext.Current!.Post(), как рекомендовано здесь.
Я также пытался поместить свой код в очередь и обработать эту очередь в обработчике событий Application.Idle моего приложения WinForms, но это ведет себя точно так же (ошибка отклонения на единицу).
Спасибо, @Джими. Я обновил свой вопрос, добавив более подробную информацию.





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
Я не понимаю, почему вы хотите изменить его на Task. Это добавляет много накладных расходов, если вам это никогда не понадобится async. Вы можете просто изменить его на Task вручную, если позже он станет async.
Большое спасибо, @MrCakaShaunCurtis. Я обновил свой вопрос, добавив более подробную информацию.
@flackoverstow — У меня могут быть моменты старшего возраста, но не настолько старшего! Если вы введете await в метод в Visual Studio, он автоматически добавит асинхронность к методу. Вы можете не заметить. Здесь возникает много-много вопросов о том, где это, вероятно, произошло.
@LukeVo - накладных расходов не так уж и много, потому что Task.Completed возвращает кешированный синглтон. Я приведу цитату из подробной статьи Стивена Тауба «Как Async/Await действительно работает в C#». Необходимость выделять дополнительный объект только для возврата таких данных является неудачной (обратите внимание, что это было и в случае с APM). Для неуниверсальных методов, возвращающих Task, метод может просто возвращать одноэлементную уже выполненную задачу, и фактически один такой синглтон предоставляется Task в форме Task.CompletedTask.
@LukeVo — код, который вызывает IncrementCount, это var task = callback.InvokeAsync(arg);
_counterValue— это захваченная переменная в ее текущем состоянии. Если вы увеличите его заранее, а затем сделаете, например,_counterValue++; SynchronizationContext.Current?.Post((state) => _counterValue += 0, null);, вы должны получить тот же результат. В противном случае может показаться, что первый вызов не имеет никакого эффекта, поскольку значение переменной отображается так, как оно было записано.