razor icon indicating copy to clipboard operation
razor copied to clipboard

Add directive to not override BuildRenderTree

Open RdJNL opened this issue 1 year ago • 2 comments

Feature request

What

Add an @ directive that can be put at the top of a .razor file that disables overriding the BuildRenderTree method. Additionally, when this is implemented, it would be nice to also have a directive to seal the BuildRenderTree method.

Why

As a C#/OO programmer, I've always been annoyed by the way templates interact with component inheritance in frontend code. When inheriting a base component, there's typically two choices: either keep the base component's template entirely unchanged or create a new template from scratch.

In my opinion, the solution to this is the use of virtual/abstract methods to create part of the template. A subtype can then replace parts of the template without having to rewrite the whole template. Blazor allows this by creating methods that return RenderFragment.

But to implement those methods without having to use RenderTreeBuilder directly, you need to be inside a .razor file. And right now, a .razor file always results in a partial class that override BuildRenderTree, thereby discarding the entire template of the base component.

Workaround

Currently, there's a workaround to keep the base component's template, by adding this code to the root of the .razor file:

@{
    base.BuildRenderTree(__builder);
}

This works, but isn't pretty. There's also no way to seal the BuildRenderTree method, and if the method is sealed the workaround won't work anymore.

Remarks

  • When BuilderRenderTree is not overridden, it should not be possible to add template code to the root of the .razor file. (The generator can't put it anywhere.)
  • If there's no template code, it might be possible to allow the contents of @code to be at the root of the .razor file, together with the @ directives.
  • See also this somewhat related issue I created: #9959.

Example components

Demo of super and sub class that use this mechanism (using the workaround)

LoaderBase.razor:

@if(!HasLoaded)
{
    @:Loading...
}
else
{
    @Content()
}

@code {
    protected abstract RenderFragment Content();
}

LoaderBase.razor.cs:

public abstract partial class LoaderBase
{
    protected bool HasLoaded { get; private set; }

    protected sealed override async Task OnParametersSetAsync()
    {
        HasLoaded = false;
        await LoadAsync();
        HasLoaded = true;
    }

    protected abstract Task LoadAsync();
}

SomeComponent.razor:

@inherits LoaderBase

@{
    base.BuildRenderTree(__builder);
}

@code {
    protected override Task LoadAsync()
    {
        return Task.Delay(5000);
    }

    protected override RenderFragment Content()
    {
        return __builder =>
        {
            @:Done loading!
        };
    }
}

RdJNL avatar Feb 20 '24 21:02 RdJNL

the solution to this is the use of virtual/abstract methods to create part of the template

Do sections provide a viable alternative solution to this at all? https://learn.microsoft.com/en-us/aspnet/core/blazor/components/sections?view=aspnetcore-8.0

davidwengier avatar Feb 20 '24 21:02 davidwengier

@davidwengier I didn't know about sections, so I tried them. Unfortunately, it's not a good solution to what I want to achieve. I see multiple issues:

  • As far as I can see, there's no way to pass parameters to the SectionOutlet and then receive those parameters in the SectionContent.
  • It's not possible to use the same section twice.
  • There's no way to force a subtype to implement a section (like abstract), nor is there a way to seal a section.
  • When a super class and a sub class both implement the same section, both implementations are executed (but the result from the super class isn't displayed). Depending on what happens in the implementation this may have side effects or it may affect performance.

RdJNL avatar Feb 20 '24 22:02 RdJNL