3

I'm trying to create a BaseComponent with shared logic and template. For that I created this "base class" inside of MyBaseComponent.razor:

<div class="parent-container">
  <!-- Derived Content is expected here -->
</div>
<button onclick="@ButtonClick" disabled=@(!IsEnabled)>Click me</button>

@code{
  public virtual void ButtonClick() {
     // do nothing in base class
  }

  public bool IsEnabled { get; set; }
}

Then I want to implement multiple derived classes, e.g. MyDerivedComponentA.razor

@inherits MyBaseComponent

<p>Template inside of MyDerivedComponentA!</p>
<button onclick="@ActivateParentButton">Activate parent button</button>

@{
    base.BuildRenderTree(__builder);
}

@code{
  public override void ButtonClick() {
     // Do something in derived class
  }

  public void ActivateParentButton(){
     IsEnabled = true;
  }
}

This works with one exception: The template of the parent component will be rendered after the derived component's layout. I tried to use some RenderFragments like ChildContent and Body but to no avail.

Is there anything I'm missing? Otherwise, is there any other way to solve this specific use case of shared logic and templates?

2 Answers 2

4

This is one way you can achieve this.

Use a RenderFragment for the derived content in the base markup:

<div class="parent-container">
  @DerivedContent
</div>
<button onclick="@ButtonClick" disabled=@(!IsEnabled)>Click me</button>

@code{
  protected RenderFragment DerivedContent {get;set;} 

  public virtual void ButtonClick() {
     // do nothing in base class
  }

  public bool IsEnabled { get; set; }
}

Then set the DerivedContent in your derived component:

@inherits MyBase
@{
    DerivedContent = @<text>
<p>Template inside of MyDerivedComponentA!</p>
<button onclick="@ActivateParentButton">Activate parent button</button>
</text>;
}
@{
    base.BuildRenderTree(__builder);
}

@code{
  public override void ButtonClick() {
     // Do something in derived class
  }

  public void ActivateParentButton(){
     IsEnabled = true;
  }
}

And call the base BuildRenderTree as before.

Here's a demo

Sign up to request clarification or add additional context in comments.

Comments

4

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.

2 Comments

Nice solution but for future Googlers, this solution doesn't work with generic classes: public abstract partial class MyTemplate<T> : TemplateComponentBase { public MyTemplate() { _renderFragment = ... CSO103: The name 'identifier' does not exist in the current context
Again for future Googlers, you can try adding @typeparam T to your razor page to see if you can make generic classes work. I have not tried it specifically for this code but if you want to use this solution, it is something to try.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.