The solution suggested by MisterMagoo does the job, but it's a bit ugly and messy. It's impossible to implement a more elegant solution with ComponentBase as you don't have access to the internal private render state fields. You can't create and pass an alternative render fragment to the Renderer.
Here's the solution I currently use:
Create a new base component based on ComponentBase with the fields we need access to changed from private to protected.
Here's the code:
public abstract class TemplateComponentBase : IComponent, IHandleEvent, IHandleAfterRender
{
protected RenderFragment _renderFragment;
private RenderHandle _renderHandle;
protected bool _initialized;
protected bool _hasNeverRendered = true;
protected bool _hasPendingQueuedRender;
protected bool _hasCalledOnAfterRender;
public TemplateComponentBase()
{
_renderFragment = builder =>
{
_hasPendingQueuedRender = false;
_hasNeverRendered = false;
BuildRenderTree(builder);
};
}
protected virtual void BuildRenderTree(RenderTreeBuilder builder) {}
protected virtual void OnInitialized() {}
protected virtual Task OnInitializedAsync() => Task.CompletedTask;
protected virtual void OnParametersSet() { }
protected virtual Task OnParametersSetAsync() => Task.CompletedTask;
protected virtual bool ShouldRender() => true;
protected virtual void OnAfterRender(bool firstRender) { }
protected virtual Task OnAfterRenderAsync(bool firstRender) => Task.CompletedTask;
protected Task InvokeAsync(Action workItem) => _renderHandle.Dispatcher.InvokeAsync(workItem);
protected Task InvokeAsync(Func<Task> workItem) => _renderHandle.Dispatcher.InvokeAsync(workItem);
protected void StateHasChanged()
{
if (_hasPendingQueuedRender)
return;
if (_hasNeverRendered || ShouldRender() || _renderHandle.IsRenderingOnMetadataUpdate)
{
_hasPendingQueuedRender = true;
try
{
_renderHandle.Render(_renderFragment);
}
catch
{
_hasPendingQueuedRender = false;
throw;
}
}
}
void IComponent.Attach(RenderHandle renderHandle)
{
if (_renderHandle.IsInitialized)
throw new InvalidOperationException($"The render handle is already set. Cannot initialize a {nameof(ComponentBase)} more than once.");
_renderHandle = renderHandle;
}
public virtual Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
if (!_initialized)
{
_initialized = true;
return this.RunInitAndSetParametersAsync();
}
else
return this.CallOnParametersSetAsync();
}
private async Task RunInitAndSetParametersAsync()
{
this.OnInitialized();
var task = this.OnInitializedAsync();
if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled)
{
this.StateHasChanged();
try
{
await task;
}
catch // avoiding exception filters for AOT runtime support
{
if (!task.IsCanceled)
throw;
}
}
await this.CallOnParametersSetAsync();
}
private Task CallOnParametersSetAsync()
{
this.OnParametersSet();
var task = this.OnParametersSetAsync();
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled;
StateHasChanged();
return shouldAwaitTask ?
this.CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
}
private async Task CallStateHasChangedOnAsyncCompletion(Task task)
{
try
{
await task;
}
catch
{
if (task.IsCanceled)
return;
throw;
}
StateHasChanged();
}
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
var task = callback.InvokeAsync(arg);
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled;
StateHasChanged();
return shouldAwaitTask ?
this.CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
}
Task IHandleAfterRender.OnAfterRenderAsync()
{
var firstRender = !_hasCalledOnAfterRender;
_hasCalledOnAfterRender |= true;
OnAfterRender(firstRender);
return this.OnAfterRenderAsync(firstRender);
}
}
Used as-is it's has exactly the same functionality as ComponentBase.
You can now create a template component and override the new method. You now have access to the render controlling fields.
MyTemplate
@inherits TemplateComponentBase
<div class="@this.mainCss">
<div class="h4">Page Template</div>
@Childcontent
<div>
This content is within the template page below the child content
</div>
<div class="p-2 text-end">
<button class="btn btn-outline-dark" @onclick=ChangeSet>Change colour</button>
</div>
</div>
public abstract partial class MyTemnplate : TemplateComponentBase
{
private bool aglow = true;
protected virtual RenderFragment? Childcontent => (builder) => this.BuildRenderTree(builder);
protected abstract RenderFragment BaseContent { get; }
public MyTemplate()
{
_renderFragment = builder =>
{
_hasPendingQueuedRender = false;
_hasNeverRendered = false;
builder.AddContent(0, BaseContent);
};
}
private string mainCss => aglow
? "p-2 bg-secondary text-white"
: "p-2 bg-primary text-white";
private void ChangeSet()
=> aglow = !aglow;
}
And then use the temnplate in the index page:
Index
@page "/"
@inherits MyTemplate
<div class="@this.css">
<div class="h2">Hello Blazor</div>
<div>
This content is within the child content page
</div>
<div>
<button class="btn btn-outline-primary" @onclick=ChangeSet>Change colour</button>
</div>
</div>
@code {
protected override RenderFragment BaseContent => (builder) => base.BuildRenderTree(builder);
private string css => alight
? "bg-light text-primary p-2 m-2"
: "bg-dark text-white p-2 m-2";
private bool alight = true;
private void ChangeSet()
=> alight = !alight;
}
The only thing you must implement is:
protected override RenderFragment BaseContent => (builder) => base.BuildRenderTree(builder);
To set the correct base content. I haven't figured out how to do this automatically.
I've added a couple of UI events to make it interactive and show that the re-rendering happens correctly.