Skip to content

Awaiting RenderFragment to render #11338

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Stamo-Gochev opened this issue Jun 18, 2019 · 7 comments
Closed

Awaiting RenderFragment to render #11338

Stamo-Gochev opened this issue Jun 18, 2019 · 7 comments
Assignees
Labels
area-blazor Includes: Blazor, Razor Components question
Milestone

Comments

@Stamo-Gochev
Copy link

Stamo-Gochev commented Jun 18, 2019

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?

@mkArtakMSFT mkArtakMSFT added the area-blazor Includes: Blazor, Razor Components label Jun 18, 2019
@Stamo-Gochev
Copy link
Author

@SteveSandersonMS @rynowak @javiercn Can you add some quick thoughts on the above as this is somewhat blocking for me. Is this approach valid in general?

@mkArtakMSFT
Copy link
Contributor

Thanks for contacting us, @Stamo-Gochev.
@SteveSandersonMS what would be the recommended approach here?

@SteveSandersonMS
Copy link
Member

If you want a parent to perform some actions on a child, use the combination of ref and OnAfterRender.

ref captures a reference to a child, which becomes initialized during the next render. Then OnAfterRender can perform invocations on that child.

@Stamo-Gochev
Copy link
Author

@SteveSandersonMS This is indeed what I used as the JS interop call wasn't necessary in the real scenario. Still, I cannot figure out why such a call fixes the async behavior.

@mkArtakMSFT mkArtakMSFT modified the milestones: disu, Discussions Jun 21, 2019
@SteveSandersonMS
Copy link
Member

Still, I cannot figure out why such a call fixes the async behavior.

@ref fields aren't populated until after rendering, because the thing you want to reference is created as an aspect of rendering.

Closing as the question seems to be answered :)

@Stamo-Gochev
Copy link
Author

Stamo-Gochev commented Jun 25, 2019

@SteveSandersonMS I agree, it is seems reasonable for a child component and its ref to be populated after OnAfterRender. This is also the approach I have used and I agree that this is a correct approach.

What I still cannot figure out is why making a JS interop call in:

public async void OnClick() 
    {
        ....
        // the JS interop call 
        var result = await JSRuntime.InvokeMethodAsync(...);

        RenderFragment += CreateChildComponent;
        StateHasChanged();  

        // ChildComponentRef is NOT null
        ChildComponentRef.Show();
    }

makes the ChildComponentRef available as the OnClick method is run "synchronously".

Here is a list of method calls when OnClick is run with var result = await JSRuntime.InvokeMethodAsync(...)

  1. var result = await JSRuntime.InvokeMethodAsync(...)
  2. RenderFragment += CreateChildComponent;
  3. StateHasChanged();
  4. OnAfterRender - the ChildComponent is initialized and thus ChildComponentRef is not null
  5. ChildComponentRef.Show(); - suceeds

Here is a list of method calls when OnClick is run without the JS interop call:

  1. RenderFragment += CreateChildComponent;
  2. StateHasChanged();
  3. ChildComponentRef.Show(); - throws a null reference error
  4. OnAfterRender - too late, step 3 has thrown

I understand that the proper way to 'await" StateHasChanged is in the built-in lifecycle methods, so I was puzzled to find out that a JS interop call has any effect on this or at least this is what I see while debugging.

@SteveSandersonMS
Copy link
Member

Your JS interop call is async, so you're interrupting the method's execution while the JS interop call occurs. Blazor is performing a render in the meantime, which populates your ChildComponentRef. So by the time execution continues after your await, the reference is populated.

@ghost ghost locked as resolved and limited conversation to collaborators Dec 3, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components question
Projects
None yet
Development

No branches or pull requests

3 participants