Description
The issue is about server-side blazor.
I am trying to follow the Manual RenderTreeBuilder logic topic in order to initialize a child component from a parent component.
// ParentComponent.razor
@inheirts ParentComponentBase
...
<button @onclick="@OnClick">Add another component</button>
...
// ParentComponent.razor.cs
public class ParentComponentBase : ComponentBase
{
public RenderFragment RenderFragment { get; set }
public ChildComponent ChildComponentRef { get; set; }
public async void OnClick()
{
....
RenderFragment += CreateChildComponent;
StateHasChanged();
ChildComponentRef.Show();
}
private RenderFragment CreateChildComponent() => builder =>
{
builder.OpenComponent(0, typeof(ChildComponent));
builder.AddComponentReferenceCapture(1, childComponentRef =>
ChildComponentRef = (ChildComponent)childComponentRef);
builder.CloseComponent();
};
}
// ChildCompoent.razor.cs
public class ChildComponent : ComponentBase {
public void Show() { ... }
}
The basic idea is: when the button in the ParentComponent
is clicked, the ChildComponent
is dynamically initialized and then its Show
method is called.
The problem I am facing is that the OnClick
handler runs asynchronously:
public async void OnClick()
{
....
RenderFragment += CreateChildComponent;
StateHasChanged();
// ChildComponentRef is null
ChildComponentRef.Show();
}
so the call ChildComponent.Show()
throws a null reference exception as the code in CreateChildComponent
hasn't finished yet (probably due to the call to StateHasChanged()
). If StateHasChanged
isn't called, then the fragment is not rendered at all.
I tried things like wrapping the call in a Task.Run()
:
public async void OnClick()
{
....
await Task.Run(() =>
{
RenderFragment += CreateChildComponent;
StateHasChanged();
}).ContinueWith(() =>
{
ChildComponentRef.Show();
});
}
but this fails with an exception:
The current thread is not associated with the renderer's synchronization context. Use Invoke() or InvokeAsync() to switch execution to the renderer's synchronization context when triggering rendering or modifying any state accessed during rendering.
so this surely isn't the blazor way of doing things.
Interestingly, making a JS interop call makes the OnClick
method run "synchronously", so the fragment is appended and the ChildComponentRef.Show()
is working:
public async void OnClick()
{
....
var result = await JSRuntime.InvokeMethodAsync(...); <-- fixes the issue
RenderFragment += CreateChildComponent;
StateHasChanged();
// ChildComponentRef is NOT null
ChildComponentRef.Show();
}
Why the JS interop call fixes the problem? Is there another way to append a fragment that initializes a component and then use this component right away in a synchronous manner?