Reputation: 1513
I'm facing a strange behaviour in Blazor, and I'd like to understand exactly what is going on.
I have this page:
@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 */
}
}
This is 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();
}
}
and MyClass
:
public class MyClass
{
public bool IsSet { get; set; }
}
In this case the component instance has an handler for OnSet, and the page behaves as expected - this is the page when I click on the button:
IsSet: True
----------------------------------------
MyComponent
IsSet: True
----------------------------------------
[ Set value ]
As soon as I remove the OnSet handler in the page (<MyComponent Param1="myObj"></MyComponent>
), this is what happens when I click the button:
IsSet: False
----------------------------------------
MyComponent
IsSet: True
----------------------------------------
This seems strange to me because the handler, when specified, is actually an empty function.
I also tried to add StateHasChanged
in the component:
private async Task SetVal()
{
Param1.IsSet = true;
if (OnSet.HasDelegate) await OnSet.InvokeAsync();
StateHasChanged();
}
but this has no effect.
This also happens if I specity OnSet="default(Action)"
.
Upvotes: 1
Views: 793
Reputation: 30440
This answer is intended to add more detail to the other two answers.
All Razor Components inherit from ComponentBase
by default.
ComponentBase
implements the IHandleEvent
interface which defines a custom event handler that the Renderer calls for all UI events associated with the component.
The implementation looks like this.
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();
}
All UI events driven by the Renderer are handled by the handler. All non UI events, such as those raised by event handlers and normal Action
and Func
callbacks are invoked directly.
Try adding this to your component to see what happens.
@implements IHandleEvent
//...
@code {
public async Task HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
await callback.InvokeAsync(arg);
// StateHasChanged();
}
}
Upvotes: 0
Reputation: 27370
In short, the containing component doesn't rerender automatically, when something happens in a child component.
This is expected and documented behaviour, see the blazor docs -> components -> rendering
Component rerenders when:
StateHasChanged
methodIn your case, when you click MyComponent's button, the event handler rerenders MyComponent
.
The containing page is rerendered only when there is the event handler for MyComponent.OnSet
.
The problem with this approach is that one should take care of every callback provided by the component. I hoped to find a way to automatically notify the containing page of the change.
There are several ways of notifying the containing page.
You can create one universal EventCallback
in the child component, e.g. OnChange
, which would be called each time anything has changed
Your MyClass
can implement INotifyPropertyChanged
and your containing page can call StateHasChanged()
manually in PropertyChanged
event handler
You can call EditContext.NotifyFieldChanged()
in child component and StateHasChanged()
manually in EditContext.OnFieldChanged
eventhandler in Containing page
A side note:
Two way binding like @bind-SomeProperty="..."
does update the containing component because it creates the event handler for you. It is just syntactic sugar.
Upvotes: 3
Reputation: 273804
I'd like to understand exactly what is going on
You need a StatehasChanged() to happen on the page. Attaching an (empty) method to the EventCallback will do that.
I also tried to add StateHasChanged in the component:
The component is already re-rendered because of the @onclick
, but that doesn't cascade 'up' to the parent.
The empty method is an acceptable workaround.
The shortest form is OnSet="() => {}"
Upvotes: 2