Skip to content

Awaiting RenderFragment to render #11338

Closed
@Stamo-Gochev

Description

@Stamo-Gochev

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?

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions