44

Blazor Component Callback from a RenderFragment Template

 2 years ago
source link: https://www.codeproject.com/Articles/5329938/Blazor-Component-Callback-from-a-RenderFragment-Te
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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:

Copy Code
<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:

Copy Code
<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:

Copy Code
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:
    Copy Code
    <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:
    Copy Code
    <ComponentName>
        <FooterTemplate>
            <button @onclick="async ()
                => await context.InvokeAsync().ConfigureAwait(false)">
                Close
            </button>
        </FooterTemplate>
    </ComponentName>

    We can simplify this code using Method Group:

    Copy Code
    <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:
    Copy Code
    public interface IElementCallbackArgs
    {
        /* base interface */
    }
    
    public class MessageCallbackArgs : IElementCallbackArgs
    {
        public string? Message { get; set; }
    }
  2. Component:
    Copy Code
    <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:

    Copy Code
    <div>
        <h1>@HeaderText</h1>
        <p>@BodyText</p>
        @if (FooterTemplate is not null)
        {
            @FooterTemplate(
                new EventCallback<IElementCallbackArgs>(
                null,
                OnCallbackClicked))
        }
    </div>
  3. Usage:
    Copy Code
    <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:
    Copy Code
    public interface IElementCallback
    {
        EventCallback Execute { get; }
    }
    
    public interface IElementCallback<T>
    {
        EventCallback<T> Execute { get; }
    }
  2. Implementation:
    Copy Code
    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

    Shrink ▲   Copy Code
    <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
    Copy Code
    <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

Example 2 - Arguments

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

  1. Component: BasicComponent

    Shrink ▲   Copy Code
    <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
    Copy Code
    <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

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.

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK