Blazor:由组件更新的值-未更新的页面



我在Blazor中遇到了一个奇怪的行为,我想了解到底发生了什么。

我有这个页面:

@page "/"
<div>IsSet: @myObj.IsSet</div>
<MyComponent Param1="myObj" OnSet="OnSetHandler"></MyComponent>
@code
{
private MyClass myObj = new();
private void OnSetHandler()
{
/* this is actually an empty function */
}
}

这是MyComponent:

<hr />
<h6>MyComponent</h6>
<div>IsSet: @Param1.IsSet</div>
<button type="button" @onclick="SetVal">Set value</button>
<hr />
@code {
[Parameter] public MyClass Param1 { get; set; }
[Parameter] public EventCallback OnSet { get; set; }
private async Task SetVal()
{
Param1.IsSet = true;
if (OnSet.HasDelegate) await OnSet.InvokeAsync();
}
}

MyClass:

public class MyClass
{
public bool IsSet { get; set; }
}

在这种情况下,组件实例有一个用于启动的处理程序,并且页面的行为与预期一样-这是我单击按钮时的页面:

IsSet: True
----------------------------------------
MyComponent
IsSet: True
----------------------------------------
[ Set value ]

一旦我在页面(<MyComponent Param1="myObj"></MyComponent>)中删除了发作处理程序,这就是当我单击按钮时发生的事情:

IsSet: False
----------------------------------------
MyComponent
IsSet: True
----------------------------------------

这对我来说似乎很奇怪,因为指定的处理程序实际上是一个空函数。

我还尝试在组件中添加StateHasChanged:

private async Task SetVal()
{
Param1.IsSet = true;
if (OnSet.HasDelegate) await OnSet.InvokeAsync();
StateHasChanged();
}

但是没有效果。

如果指定OnSet="default(Action)",也会发生这种情况。

简而言之,当子组件中发生某些情况时,包含该组件的组件不会自动呈现。

这是预期的和记录的行为,参见blazor文档->组件→呈现

组件呈现时:

  • 在应用父组件更新后的一组参数后
  • 应用级联参数的更新值后。
  • 在事件通知并调用其自己的事件处理程序之一之后。
  • 调用自己的StateHasChanged方法后

在你的例子中,当你点击MyComponent的按钮时,事件处理程序呈现MyComponent。只有当存在MyComponent.OnSet的事件处理程序时,才会重新呈现包含该页面的页面。


这种方法的问题是应该处理组件提供的每个回调。我希望找到一种方法来自动通知包含更改的页面。

有几种方法可以通知包含页面。

  1. 你可以在子组件中创建一个通用的EventCallback,例如OnChange,它将在每次有任何变化时被调用

  2. 您的MyClass可以实现INotifyPropertyChanged,您的包含页面可以在PropertyChanged事件处理程序中手动调用StateHasChanged()

  3. 您可以在子组件中调用EditContext.NotifyFieldChanged(),在包含页

    EditContext.OnFieldChangedeventandler中手动调用StateHasChanged()

边注:

@bind-SomeProperty="..."这样的双向绑定确实更新了包含组件,因为它为您创建了事件处理程序。这只是语法糖。

我想知道到底发生了什么

您需要在页面上发生stathaschanged()。给EventCallback附加一个(空)方法就可以了。

我还尝试在组件中添加StateHasChanged:

由于@onclick,组件已经被重新渲染了,但这并没有'向上'级联到父组件。

empty方法是一种可接受的解决方法。
最短形式为OnSet="() => {}"

此回答旨在为其他两个答案添加更多细节。

所有Razor组件默认继承自ComponentBase

ComponentBase实现了IHandleEvent接口,该接口定义了一个自定义事件处理程序,Renderer为与组件关联的所有UI事件调用该事件处理程序。

实现如下:

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;
}
// Broken out as used elsewhere in the component lifecycle
private async Task CallStateHasChangedOnAsyncCompletion(Task task)
{
try
{
await task;
}
catch // avoiding exception filters for AOT runtime support
{
// Ignore exceptions from task cancellations, but don't bother issuing a state change.
if (task.IsCanceled)
return;
throw;
}
StateHasChanged();
}

所有由Renderer驱动的UI事件都由处理器处理。所有非UI事件,例如由事件处理程序引发的事件以及普通的ActionFunc回调将被直接调用。

试着把它添加到你的组件中,看看会发生什么。

@implements IHandleEvent
//...
@code {
public async Task HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
await callback.InvokeAsync(arg);
// StateHasChanged();
}
}