Graeme
Graeme

Reputation: 839

How to use RenderTreeBuilder for RenderFragment<> in Blazor

I am implementing an EventCallback in a RenderFragment parameter to enable an external close button to fire an event within a custom component:

public RenderFragment<IElementInfo>? ContentTemplate { get; set; }

Here is the interface:

public interface IElementInfo
{
    EventCallback CloseCallback { get; }
}

And to use:

<ContentTemplate>
    <button type="button"
            @onclick="@(async () =>
              await RaiseCallback(context.CloseCallback))">
        Cancel
    </button>
</ContentTemplate>

[edit] This is what the compiler generates for the above usage:

__builder.AddAttribute<ClosedEventArgs>(22, "Closed", RuntimeHelpers.TypeCheck<EventCallback<ClosedEventArgs>>(EventCallback.Factory.Create<ClosedEventArgs>((object) this, new Action(OnClosed))));

[end edit]

There is more code that is needed to wire it all up. As it is it works.

What I am trying to do is add a simple method for passing a title and a razor component to wire-up using the RenderTreeBuilder manually. Here is a working version for a non-generic RenderFragment:

[edit]

public void Show(string? title, Type contentType)
{
    if (contentType.BaseType != typeof(ComponentBase))
        throw new ArgumentException
            ($"{contentType.FullName} must be a Blazor Component");

    RenderFragment content = x =>
    {
        x.OpenComponent(1, contentType);
        x.CloseComponent();
    };

    Show(new Options
    {
        HeaderText = title,
        ContentTemplate = content
     });
}

public void Show(Options options)
{
    Container container = new Container(options, _provider);
    _contents.Add(container);

    // notify new content
    OnUpdated?.Invoke();
}

Sample usage:

Service.Show(
    "Sample Title",
    typeof(SampleRazorFile));
// where SampleRazorFile is a seperate razor file

[end edit]

Where I am stumped is with changing with the RenderTreeBuilder for working with the generic RenderFragment<IElementInfo>.

[edit]

The question is regarding RenderTreeBuilder - can it return a Renderfragment<TValue> type?

[end edit]

There is next to no information out there on this specific task. Has anyone done anything like this and have any suggestions?

Upvotes: 3

Views: 5269

Answers (2)

Graeme
Graeme

Reputation: 839

I have concluded that you can not do it. I have added solutions to support both RenderFragment and RenderFragment<TValue>.

Actually, the solution is unexpectedly simple - cast the RenderFragment to the generic type.

So, for my example above, the answer is as follows:

ContentTemplate = (RenderFragment<IElementInfo>)(_context =>
    builder =>
    {
        builder.OpenElement(2, "div");
        // trimmed...
        builder.CloseElement();
    }
);

How I found this solution was to enable emitting of Roslyn generated files. Add the following to the project file:

<PropertyGroup>
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

Hope that this helps someone! 😊

Upvotes: 4

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30177

I'm not absolutely sure of your context and if you have it right. You don't set the "context" externally, you set it within the component and pass it to the external RenderFragment block.

Here's some code for a component to display the WeatherForecast rows in FetchData that demonstreates how to use the RenderFragment<> in a RenderFragment block.

<h3>ListOfWeather</h3>
<table class="table">
    <thead>
        @this.HeaderTemplate
    </thead>
    @this.Rows
</table>

@code {
    [Parameter] public List<WeatherForecast> ForecastList { get; set; } = new List<WeatherForecast>();

    [Parameter] public RenderFragment<WeatherForecast>? ListRowTemplate { get; set; }

    [Parameter] public RenderFragment<WeatherForecast>? HeaderTemplate { get; set; }

    private RenderFragment Rows => (builder) =>
    {
        if (this.ListRowTemplate is null)
            return;

        builder.OpenElement(0, "tdbody");
        foreach (var forecast in ForecastList)
            builder.AddContent(2, this.ListRowTemplate(forecast));

        builder.CloseElement();
    };
}

For reference FetchData looks like this:

@page "/fetchdata"

<PageTitle>Weather forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

<ListOfWeather ForecastList="this.Forecasts">
    <HeaderTemplate>
        <tr>
            <th>Date</th>
            <th>Temp. (C)</th>
            <th>Temp. (F)</th>
            <th>Summary</th>
        </tr>

    </HeaderTemplate>
    <ListRowTemplate>
        <tr>
            <td>@context.Date.ToShortDateString()</td>
            <td>@context.TemperatureC</td>
            <td>@context.TemperatureF</td>
            <td>@context.Summary</td>
            <td></td>
        </tr>
    </ListRowTemplate>
</ListOfWeather>

@code {
    private List<WeatherForecast> Forecasts = new List<WeatherForecast>();

    [Inject] private WeatherForecastService? service { get; set; }
    private WeatherForecastService Service => service!;

    protected override async Task OnInitializedAsync()
    {
        this.Forecasts = await Service.GetForecastAsync(DateTime.Now);
    }
}

Upvotes: 0

Related Questions