Add directive to not override BuildRenderTree
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
BuilderRenderTreeis 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
@codeto 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!
};
}
}
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 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
SectionOutletand then receive those parameters in theSectionContent. - 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.