Skip to content
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

How to use BuildRenderTree() as a RenderFragment? #10499

Open
UniMichael opened this issue Jun 18, 2024 · 10 comments
Open

How to use BuildRenderTree() as a RenderFragment? #10499

UniMichael opened this issue Jun 18, 2024 · 10 comments
Labels
area-compiler Umbrella for all compiler issues untriaged

Comments

@UniMichael
Copy link

UniMichael commented Jun 18, 2024

When extending an existing component, one can use the base.BuildRenderTree(__builder) to render the base component's HTML.
However, using base.BuildRenderTree(__builder) inside a component as a RenderFragment ChildContent will fail.

Here is an example:

// MyWrapper.razor

<h1>Wrapper</h1>
@ChildContent

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
// MyComponent.razor

@Inherits SomeComponent

<div>
    <h1>My Component</h1>
    <MyWrapper>
        @{
            base.BuildRenderTree(__builder);
        }
    </MyWrapper>
</div>

The above fails with the following error:

  System.AggregateException: One or more errors occurred. (TypeError: Cannot read properties of undefined (reading 'parentNode'))
       ---> System.InvalidOperationException: TypeError: Cannot read properties of undefined (reading 'parentNode')
         at Microsoft.AspNetCore.Components.RenderTree.Renderer.InvokeRenderCompletedCallsAfterUpdateDisplayTask(Task updateDisplayTask, Int32[] updatedComponents)

This can be fixed by replacing the base.BuildRenderTree(__builder) block with a RenderFragment factory method:

// MyComponent.razor

@Inherits SomeComponent

<div>
    <h1>My Component</h1>
    <MyWrapper>
        @WrapInRenderFragment()
    </MyWrapper>
</div>

@code {
    private RenderFragment WrapInRenderFragment() => builder => base.BuildRenderTree(builder);
}

My understanding is that this happens because RenderFragment is a delegate that operates on a RenderTreeBuilder, whereas running the code base.BuildRenderTree(__builder) is used to generate the contents of the component's BuildRenderTree() method, which are two different things.

I'm unable to find much documentation on the subject of using the BuildRenderTree() method inside a RenderFragment. Is the above the correct way to handle this or will it lead to unexpected complications down the line?


Edit:

You can also replace the private method with a Razor expression that returns a lambda instead, which leads me to believe that RenderFragments are handled differently by the Razor code generator (I'm guessing they're invoked when found):

<MyWrapper>
    @((RenderTreeBuilder builder) => base.BuildRenderTree(builder))
</MyWrapper>
@davidwengier davidwengier added the area-compiler Umbrella for all compiler issues label Jun 18, 2024
@jjonescz
Copy link
Contributor

Calling base.BuildRenderTree(__builder); seems unsupported - you are relying on an implementation detail that a __builder variable is available. On the other hand, relying on RenderFragments looks good to me.

@UniMichael
Copy link
Author

Calling base.BuildRenderTree(__builder); seems unsupported - you are relying on an implementation detail that a __builder variable is available. On the other hand, relying on RenderFragments looks good to me.

Just to be sure, when you say calling base.BuildRenderTree(__builder) is unsupported, are you specifically talking about calling it in this context (i.e. inside a custom component that expects a RenderFragment?

I'm asking because it seems like using base.BuildRenderTree(__builder) is the recommended way of rendering a base component's HTML (usually, at least).

@jjonescz
Copy link
Contributor

when you say calling base.BuildRenderTree(__builder) is unsupported, are you specifically talking about calling it in this context (i.e. inside a custom component that expects a RenderFragment?

I meant in general, accessing __builder looks like a hack that might not work in all situations. That's why it also doesn't work in your example (see below).

But I will let other @dotnet/razor-compiler folks chime in whether they think this is something we should support and hence somehow make this work.


Note: in your example, the <Wrapper> component introduces a new builder (__builder2) like this in the generated code:

__builder.OpenComponent<global::BlazorApp1.Components.MyWrapper>(2);
__builder.AddAttribute(3, "ChildContent", (global::Microsoft.AspNetCore.Components.RenderFragment)((__builder2) => {
    base.BuildRenderTree(__builder);
}));

So you are referring to the wrong builder and it's failing at runtime. That should also demonstrate why relying on __builder is a hack today.

@UniMichael
Copy link
Author

Is there a "correct" approach to rendering a component's base HTML, then? Or is it unsupported and the hack the only way to do it currently?

@jjonescz
Copy link
Contributor

Is there a "correct" approach to rendering a component's base HTML, then?

As I already said, the RenderFragment approaches look good to me. I think they will behave as expected. Either one of those:

@code {
    private RenderFragment WrapInRenderFragment() => builder => base.BuildRenderTree(builder);
}
@((RenderTreeBuilder builder) => base.BuildRenderTree(builder))

@UniMichael
Copy link
Author

UniMichael commented Jun 19, 2024

Sorry, your previous comment made me think you meant using base.BuildRenderTree(__builder) is unsuported everywhere (outside of this use case with nested components and RenderFragments).

It's still the correct way to render a base component normally , right?

An example of what I mean:

// Foo.razor

@inherits Bar

@{
    base.BuildRenderTree(__builder);
}

@code {
    // Override some of Bar's methods/defaults here.
}

@jjonescz
Copy link
Contributor

It's still the correct way to render a base component normally , right?

Although it works today, I wouldn't recommend it.

Let me summarize:

  • Accessing __builder anywhere is a hack and I wouldn't recommend it. It won't work in nested components. Even though it works in some scenarios, it's still an implementation detail of the compiler - we might not change it if lots of people depend on it, but in theory we could change it in a breaking way at any point in the future.
  • Calling base.BuildRenderTree anywhere seems fine. But you shouldn't pass __builder into it per the previous bullet point.
  • Using RenderFragment is also a supported scenario.
  • Combining the last two bullet points => you should do something like @((RenderTreeBuilder builder) => base.BuildRenderTree(builder)) everywhere.

@UniMichael
Copy link
Author

UniMichael commented Jun 19, 2024

Okay, gotcha. So this is the proper way of doing it, if we're not replacing the default HTML (well, ignoring the fact that we could just define a .cs file and extend the class without using any Razor syntax, of course).

// Foo.razor

@inherits Bar

@((RenderTreeBuilder builder) => base.BuildRenderTree(builder))

@code {
    // Override some of Bar's methods/defaults here.
}

Thanks a lot, that clears things up nicely!


Edit: For the record, it would probably be a good idea to update the Blazor docs to include this, since every search result seems to reference the other GitHub issue as the correct way of doing things (and it was closed, so we can't leave a comment there anymore).

@danroth27
Copy link
Member

Adding @SteveSanderson @javiercn @mkArtakMSFT

I think we need to get crisp on what is and isn't supported when it comes to Blazor component inheritance. As @UniMichael pointed out, we've pointed users to __builder based workarounds before. If we're now saying that touching __builder isn't supported at all, then we probably need to do a scrub of the Blazor docs to make sure it isn't recommended anywhere. And we should document what our actual guidance is for component inheritance. We may also want to consider adding compiler features to make the experience better.

@davidwengier
Copy link
Contributor

My 2c: We definitely should stop telling people to use __builder. At the end of the day its an internal compiler construct that could change, and as Jan pointed out above, sometimes people would need to use __builder2 (or maybe even __builder3? 😛), and we don't have a good way to allow users to find out which one to use.

Having said that, we shouldn't do it until we have an alternate suggestion that our users will be happy with.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-compiler Umbrella for all compiler issues untriaged
Projects
None yet
Development

No branches or pull requests

4 participants