我在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
的事件处理程序时,才会重新呈现包含该页面的页面。
这种方法的问题是应该处理组件提供的每个回调。我希望找到一种方法来自动通知包含更改的页面。
有几种方法可以通知包含页面。
-
你可以在子组件中创建一个通用的
EventCallback
,例如OnChange
,它将在每次有任何变化时被调用 -
您的
MyClass
可以实现INotifyPropertyChanged
,您的包含页面可以在PropertyChanged
事件处理程序中手动调用StateHasChanged()
-
您可以在子组件中调用
的EditContext.NotifyFieldChanged()
,在包含页EditContext.OnFieldChanged
eventandler中手动调用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事件,例如由事件处理程序引发的事件以及普通的Action
和Func
回调将被直接调用。
试着把它添加到你的组件中,看看会发生什么。
@implements IHandleEvent
//...
@code {
public async Task HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
await callback.InvokeAsync(arg);
// StateHasChanged();
}
}