Click here to Skip to main content
15,881,089 members
Articles / Web Development / Blazor

Blazor Component Callback from a RenderFragment Template

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
16 Apr 2022CPOL6 min read 14.9K   111   5   6
Blazor Component method callback from an external RenderFragment Template
This article covers the problem and solution to enable Blazor Components to allow callbacks, with optional data, from an external template.

The Problem

I was working on a Blazor Component that needed to support external button(s) in the footer template that interacted with method(s) in the Component.

Typically, the Component would look something like:

HTML
<div>
    <h1>@HeaderText</h1>
    <p>@BodyText</p>
    @if (FooterTemplate is not null)
    {
        @FooterTemplate
    }
</div>

@code {
    [Parameter]
    public string HeaderText { get; set; }

    [Parameter]
    public string BodyText { get; set; }

    [Parameter]
    public RenderFragment? FooterTemplate { get; set; }

    private void OnClicked()
    {
        // do something here
    }
}

Then we would use the Component something like:

HTML
<ComponentName>
    <FooterTemplate>
        <button @onclick="OnClicked">Close</button>
    </FooterTemplate>
</ComponentName>

The issue here is that the button @onclick will call a local method and not call the method in the component.

The Solution

This article focuses on a solution using RenderFragment<> and EventCallback/EventCallback<> to enable calling methods in the component from external templates. We will also look at how to pass parameters back using the same solution.

TL;DR

Downloadable code for the solution can be found at the end of the article.

EventCallback

  1. Official Definition: A bound event handler delegate.
  2. Blazor University: The EventCallback class is a special Blazor class that can be exposed as a Parameter so that components can easily notify consumers when something of interest has occurred. When building Components with Data binding, we use an EventCallback to notify that a property has changed.

RenderFragment

Official Definition: A RenderFragment represents a segment of UI to render. RenderFragment<TValue> takes a type parameter that can be specified when the render fragment is invoked.

Implementation

When we look at the code for the constructor and the InvokeAsync method in EventCallback class, it is defined as follows:

C#
public EventCallback(IHandleEvent? receiver, MulticastDelegate? @delegate)
{
    this.Receiver = receiver;
    this.Delegate = @delegate;
}

public Task InvokeAsync(object? arg)
    => this.Receiver == null
    ? EventCallbackWorkItem.InvokeAsync<object>(this.Delegate, arg)
    : this.Receiver.HandleEventAsync(
        new EventCallbackWorkItem(this.Delegate), arg);
        
public Task InvokeAsync() => this.InvokeAsync((object) null);

What interests us here is that we can pass a method in on initialization and pass parameters (optionally) when invoked.

RenderFragment<TValue> allows us to expose objects/classes to the Template for the Component. We can now change the above problem code as follows:

  1. Component:
    HTML
    <div>
        <h1>@HeaderText</h1>
        <p>@BodyText</p>
        @if (FooterTemplate is not null)
        {
            @FooterTemplate(new EventCallback(null, OnCallbackClicked))
        }
    </div>
    
    @code {
        [Parameter]
        public string HeaderText { get; set; }
    
        [Parameter]
        public string BodyText { get; set; }
    
        [Parameter]
        public RenderFragment<EventCallback>? FooterTemplate { get; set; }
    
        private void OnCallbackClicked()
        {
            // do something here
        }
    }
  2. Usage:
    HTML
    <ComponentName>
        <FooterTemplate>
            <button @onclick="async ()
                => await context.InvokeAsync().ConfigureAwait(false)">
                Close
            </button>
        </FooterTemplate>
    </ComponentName>

    We can simplify this code using Method Group:

    HTML
    <ComponentName>
        <FooterTemplate>
            <button @onclick="context">
                Close
            </button>
        </FooterTemplate>
    </ComponentName>

So How Does This Work?

context is the EventCallback passed from the Component to the Template. When the Template button is pressed, the InvokeAsync method is invoked on the context and executes the delegate OnCallbackClicked method in the Component.

With the Method Group simplification above, the compiler automagically knows to call the InvokeAsync on the context (EventCallback) class as defined in the EventCallback class..

What if We Want to Pass Parameters Back to the Component?

We use the generic EventCallback<T> to pass one (1) or more parameters. For this, we will use an argument class.

  1. Arguments:
    C#
    public interface IElementCallbackArgs
    {
        /* base interface */
    }
    
    public class MessageCallbackArgs : IElementCallbackArgs
    {
        public string? Message { get; set; }
    }
  2. Component:
    HTML
    <div>
        <h1>@HeaderText</h1>
        <p>@BodyText</p>
        @if (FooterTemplate is not null)
        {
            @FooterTemplate(
                new EventCallback<IElementCallbackArgs>(
                null,
                new Action<MessageCallbackArgs> (args => OnCallbackClicked(args))))
        }
    </div>
    
    @code {
        [Parameter]
        public string HeaderText { get; set; }
    
        [Parameter]
        public string BodyText { get; set; }
    
        [Parameter]
        public RenderFragment<EventCallback<IElementCallbackArgs>>?
            FooterTemplate { get; set; }
    
        private void OnCallbackClicked(MessageCallbackArgs args)
        {
            // do something here
        }
    }

    Again, we can use Method Groups to simplify the code:

    HTML
    <div>
        <h1>@HeaderText</h1>
        <p>@BodyText</p>
        @if (FooterTemplate is not null)
        {
            @FooterTemplate(
                new EventCallback<IElementCallbackArgs>(
                null,
                OnCallbackClicked))
        }
    </div>
  3. Usage:
    HTML
    <ComponentName>
        <FooterTemplate>
            <button @onclick="async () => await OnClickedAsync(context)">
                Close
            </button>
        </FooterTemplate>
    </ComponentName>
    
    @code {
        private static async Task OnClickedAsync(
            EventCallback<IElementCallbackArgs> callback)
                => await callback.InvokeAsync(
                new MessageCallbackArgs
                {
                    Message = "message goes here"
                }).ConfigureAwait(false);
    }

    So like the first callback only, here we are doing the same invocation, however we are passing back data specific to the event / button press.

Improvements

The code as-is works as expected. What we can do is encapsulate the EventCallback in a wrapper interfaces and classes. The code below is a base implementation that allows to be expanded upon.

  1. Definition:
    C#
    public interface IElementCallback
    {
        EventCallback Execute { get; }
    }
    
    public interface IElementCallback<T>
    {
        EventCallback<T> Execute { get; }
    }
  2. Implementation:
    C#
    public class ElementCallback : IElementCallback
    {
        public ElementCallback(MulticastDelegate  @delegate)
            => Execute = new EventCallback(null, @delegate);
    
        public EventCallback Execute { get; }
    }
    
    public class ElementArgsCallback : IElementArgsCallback
    {
        public ElementArgsCallback(MulticastDelegate  @delegate)
            => Execute = new EventCallback<IElementCallbackArgs>(null, @delegate);
    
        public EventCallback<IElementCallbackArgs> Execute { get; }
    }

Example 1 - Basic

The following example has a button inside the component and a button in the template. This is to simulate when a component could have a preset button or allow for an optional custom button.

  1. Component: BasicComponent
    HTML
    <button type="button"
            class="btn btn-primary me-4"
            @onclick="ButtonClicked">
        OK
    </button>
    
    @if (ContentTemplate is not null)
    {
        @ContentTemplate(new ElementCallback(OnCallbackClicked))
    }
    <hr />
    <ul>
        @if (!Messages.Any())
        {
            <li>No buttons clicked...</li>
        }
        else
        {
            @foreach (string message in Messages)
            {
                <li>@message</li>
            }
        }
    </ul>
    <hr />
    
    @code {
        [Parameter]
        public RenderFragment<IElementCallback>? ContentTemplate { get; set; }
    
        private void ButtonClicked()
            => Clicked($"Button clicked in {nameof(BasicComponent)}");
    
        private void OnCallbackClicked()
            => Clicked("External button clicked");
    
        private readonly IList<string> Messages = new List<string>();
    
        private void Clicked(string message)
        {
            Messages.Add(message);
            InvokeAsync(StateHasChanged);
        }
    }
  2. Implementation:
    HTML
    <h2>Example 1 - Simple ElementCallback</h2>
    <BasicComponent>
        <ContentTemplate>
            <button type="button" class="btn btn-outline-success"
                    @onclick="context.Execute">
                Template OK
            </button>
        </ContentTemplate>
    </BasicComponent>
  3. Output:

    Image 1

The demonstration projects WasmComponentCallback1 and ServerComponentCallback1 can be found in the download below.

Example 2 - Arguments

This example expands on the first and passes back a message to the component.

  1. Component: BasicComponent
    HTML
    <button type="button"
            class="btn btn-primary me-4"
            @onclick="ButtonClicked">
        OK
    </button>
    
    @if (ContentTemplate is not null)
    {
        @ContentTemplate(new ElementArgsCallback(OnCallbackClicked))
    }
    <hr />
    <ul>
        @if (!Messages.Any())
        {
            <li>No buttons clicked...</li>
        }
        else
        {
            @foreach (string message in Messages)
            {
                <li>@message</li>
            }
        }
    </ul>
    <hr />
    
    @code {
        [Parameter]
        public RenderFragment<IElementArgsCallback>? ContentTemplate { get; set; }
        
        private void ButtonClicked()
            => Clicked($"Button clicked in {nameof(ArgsComponent)}");
    
        private void OnCallbackClicked(MessageCallbackArgs args)
            => Clicked(args.Message ?? "External button clicked");
    
        private readonly IList<string> Messages = new List<string>();
    
        private void Clicked(string message)
        {
            Messages.Add(message);
            InvokeAsync(StateHasChanged);
        }
    }
  2. Implementation:
    HTML
    <h2>Example 2 -  Message ElementCallback</h2>
    <ArgsComponent>
        <ContentTemplate>
            <button type="button" class="btn btn-outline-success"
                    @onclick="@(async () => await ClickedAsync(context.Execute))">
                Template OK
            </button>
        </ContentTemplate>
    </ArgsComponent>
    
    @code {
        private int _count = 1;
    
        private async Task ClickedAsync(
            EventCallback<IElementCallbackArgs> callback)
            => await callback.InvokeAsync(
                new MessageCallbackArgs
                {
                    Message = $"Message > Click # {_count++}"
                }).ConfigureAwait(false);
    }
  3. Output:

    Image 2

The demonstration projects WasmComponentCallback2 and ServerComponentCallback2 can be found in the download below.

Suggested Alternative Solution

An alternative solution from Shaun C Curtis was proposed where the Component itself is passed to the template, not an EventCallback.

This is not advised as you not only lose meaning of the context, but you expose properties and methods that should not be exposed to the template. Unexpected usage can and will most likely break your component.

A Better Alternative Solution

If an EventCallback with arguments is too limiting, then expose a Model class to encapsulate and control what is exposed externally to the template (RenderFragment) is recommended.

  1. Model & Interface
    C#
    public interface IMessageModel
    {
        void SetText(string Text);
    }
    
    public class MessageModel : IMessageModel
    {
        private string? _text;
    
        [Parameter]
        public string Text
        {
            get => _text ?? "";
            set
            {
                if (_text == value)
                    return;
    
                _text = value;
                TextChanged.InvokeAsync(value);
                OnTextChanged?.Invoke(value);
            }
        }
    
        // for two-way binding inside the component
        [Parameter]
        public EventCallback<string> TextChanged { get; set; }
    
        // for Callback from outside the component
        void IMessageModel.SetText(string text)
            => Text = text;
    
        public event Action<string>? OnTextChanged;
    }
  2. Component (AltArgsCompponent):
    HTML
     @implements IDisposable
    
    <button type="button" class="btn btn-primary me-4"
            @onclick="ButtonClicked">
        OK
    </button>
    
    @if (ContentTemplate is not null)
    {
        @ContentTemplate(Message)
    }
    <hr />
    <ul>
        @if (!Messages.Any())
        {
            <li>No buttons clicked...</li>
        }
        else
        {
            @foreach (string message in Messages)
            {
                <li>@message</li>
            }
        }
    </ul>
    <hr />
        @if (!string.IsNullOrWhiteSpace(Message.Text))
        {
            <p>@($"{LastMessageTimestamp} > {Message.Text}")</p>
        }
        else
        {
            <p>Waiting for a message...</p>
        }
    <hr />
    
    @code {
    
        [Parameter]
        public RenderFragment<IMessageModel>? ContentTemplate { get; set; }
    
        private readonly MessageModel Message = new();
        private string? LastMessageTimestamp;
    
        private void ButtonClicked()
            => Clicked($"Button clicked in {nameof(AltArgsCompponent)}");
    
        protected override void OnInitialized()
        {
            Message.OnTextChanged += OnMessageModelChanged;
            base.OnInitialized();
        }
    
        private void OnMessageModelChanged(string message)
        {
            LastMessageTimestamp = DateTime.Now.ToLongTimeString();
            Clicked(message);
        }
    
        private readonly IList<string> Messages = new List<string>();
    
        private void Clicked(string message)
        {
            Messages.Add(message);
            InvokeAsync(StateHasChanged);
        }
    
        public void Dispose()
        {
            Message.OnTextChanged -= OnMessageModelChanged;
        }
    }
  3. Usage (Index.razor):
    HTML
     <h1>Alternative Template Callback Demo</h1>
    
    <AltArgsCompponent>
        <ContentTemplate>
            <button class="btn btn-dark"
                    @onclick="_ => context.SetText(GetMessage())">Click Me</button>
        </ContentTemplate>
    </AltArgsCompponent>
    
    @code {
        private int _count = 1;
    
        private string GetMessage()
            => $"Message > Click # {_count++}";
    }
  4. Output:

    Image 3

This shows how to implement an alternative solution correctly when the EventCallback is not suitable.

There is a little bit more code here than my original solution. The MessageModel implements both two-way binding and event notification. It also uses an explicit interface to only expose methods that the Template is allowed to use.

This is also a MVVM-friendly design (Note: MVVM is outside the scope of this article).

The demonstration projects WasmComponentCallback3 and ServerComponentCallback3 can be found in the download below.

Another Alternative Solution

After updating the article with A Better Alternative SolutionShaun C Curtis suggested another solution using Func<T, Task>. This is an Asynchronous Deleagate sharing MessageCallbackArgs, like in example 2 above rather than an EventCallback.

Whilst his solution is in the comments below, I've included it here in the article for completeness.

  1. Model & Interface (MessageCallbackArgs & IElementCallbackArgs)
    C#
    public interface IElementCallbackArgs
    {
        /* base interface */
    }
    
    public class MessageCallbackArgs : IElementCallbackArgs
    {
        public string? Message { get; set; }
    }
  2. Component (AltArgsCompponent):
    HTML
    <button type="button" class="btn btn-primary me-4"
            @onclick="ButtonClicked">
        OK
    </button>
    
    @if (ContentTemplate is not null)
    {
        @ContentTemplate(CallForward)
    }
    <hr />
    <ul>
        @if (!Messages.Any())
        {
            <li>No buttons clicked...</li>
        }
        else
        {
            @foreach (string message in Messages)
            {
                <li>@message</li>
            }
        }
    </ul>
    <hr />
    
    @code {
    
        [Parameter]
        public RenderFragment<Func<MessageCallbackArgs, Task>>?
            ContentTemplate { get; set; }
    
        [Parameter]
        public string? Message { get; set; }
    
        private Func<MessageCallbackArgs, Task> CallForward => this.ExternalClick;
    
        private void ButtonClicked()
            => Clicked($"Button clicked in {nameof(AltArgsComponent)}");
    
        private Task ExternalClick(MessageCallbackArgs args)
        {
            Clicked(args.Message ?? "External button clicked");
            return Task.CompletedTask;
        }
    
        private readonly IList<string> Messages = new List<string>();
    
        private void Clicked(string message)
        {
            Messages.Add(message);
            InvokeAsync(StateHasChanged);
        }
    }
  3. Usage (Index.razor):
    HTML
    <h1>Alternative 2 Template Callback Demo</h1>
    
    <AltArgsComponent>
        <ContentTemplate>
            <button type="button" class="btn btn-outline-dark"
                    @onclick="(e) => context(message)">
                Template OK
            </button>
        </ContentTemplate>
    </AltArgsComponent>
    
    @code {
        private int _count = 1;
    
        private MessageCallbackArgs message => new()
        {
            Message = $"Message > Click # {_count++}"
        };
    }
  4. Output:

    Image 4

The demonstration projects WasmComponentCallback4 and ServerComponentCallback4 can be found in the download below.

Bonus - Blazor Server Apps (Embedded Wasm)

Embed a Blazor Web Assembly Application inside a Blazor Server Application

I am not a fan of Blazor Server applications, however the above solutions works for both Blazor WebAssembly and Blazor Server applications. So, for this update I have used a little trick to embed Blazor Web Assembly applications as RCL (Razor Component Library) in a Blazor Server Application.

To make this work you need to:

  1. Create a Blazor Server App
  2. Reference the Web Assembly project
  3. Remove unnecessary files (eg: dupelicate files in wwwroot, unused razor files, etc)
  4. Update the _Host.cshtml file with css references plus point to the WebAssembley App.razor

The new lean project structure will look like this:

Image 5

The  _Host.cshtml file will look something like:

HTML
@page "/"
@using Microsoft.AspNetCore.Components.Web
@namespace ServerComponentCallback4.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="WasmComponentCallback4.styles.css" rel="stylesheet" />
    <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
<body>
<app>
    <component type="typeof(WasmComponentCallback4.App)"
               render-mode="ServerPrerendered" />
</app>

<div id="blazor-error-ui">
    <environment include="Staging,Production">
        An error has occurred. This application may no longer respond until reloaded.
    </environment>
    <environment include="Development">
        An unhandled exception has occurred. See browser dev tools for details.
    </environment>
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

<script src="_framework/blazor.server.js"></script>
</body>
</html>

The key parts are:

  1.  Linking the Webassembly css stylesheet(s)
    HTML
    <link href="WasmComponentCallback4.styles.css" rel="stylesheet" />
  2. Pointing to the Web Assembly App razor class
    HTML
    <app>
        <component type="typeof(WasmComponentCallback4.App)"
                   render-mode="ServerPrerendered" />
    </app>

The key benefit of setting up the projects in this way is so that both the Blazor Web Assembly & Blazor Server applications use a single code base and stay in sync.

Identify Web Assembly application mode

I have also included a snippet of code to identify if the Web Assembly app is running on its own or as a RCL in a Blazor Server app. If you look at the two (2) Alternative solutions above, you can see I have included the Environment in the screenshots.

Here is the code that does the check:

C#
[Inject]
protected IJSRuntime? ijsRuntime { get; set; }

private string Environment
    => ijsRuntime! is IJSInProcessRuntime ? "WebAssembly" : "Server";

This code snippet can be found in the MainLayout.razor.

Working Example

Below is a link to the code used from concept to final implementation as used when I initially looked at the problem and is mentioned in the article above.

Download v1.2 source code - 816.2 KB

Summary

The final solution for enabling Component method callback, with optional data, from an external Template delivers a clean implementation that can be easily expanded on for any use case.

Enjoy!

History

  • v1.0 - 17th April, 2022 - Initial release
  • v1.1 - 19th April, 2022 - Added an alternative solution
  • v1.2 - 21st April, 2022 - Added an alternative solution 2 + Blazor Server app projects 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionAn alternative Solution? Pin
Shaun C Curtis18-Apr-22 11:22
mvaShaun C Curtis18-Apr-22 11:22 
AnswerRe: An alternative Solution? Pin
Graeme_Grant18-Apr-22 13:30
mvaGraeme_Grant18-Apr-22 13:30 
GeneralRe: An alternative Solution? Pin
Shaun C Curtis18-Apr-22 22:33
mvaShaun C Curtis18-Apr-22 22:33 
[Polite]
I did not intend you to update your article. There are many ways to solve/code a problem.

Yes, you expose the Parameters to external change, but you can do that with a simple `@ref` or cause problems internally in the component by using them inappropriately. Otherwise keep stuff that is internal private/protected and it's not exposed.
GeneralRe: An alternative Solution? Pin
Graeme_Grant18-Apr-22 23:25
mvaGraeme_Grant18-Apr-22 23:25 
GeneralRe: An alternative Solution? Pin
Shaun C Curtis19-Apr-22 0:14
mvaShaun C Curtis19-Apr-22 0:14 
GeneralRe: An alternative Solution? Pin
Graeme_Grant19-Apr-22 1:08
mvaGraeme_Grant19-Apr-22 1:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.