question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

@Key instruction doesn't work when call Render Tree Builder by hands in Blazor WebAssembly (.NET 5.0).

See original GitHub issue

Describe the bug

​ Hi, friend. It’s an issue about @Key instruction of ASP.NET Core Blazor in Web Assembly.

Requirement:

​ I am going to display many sub-components in a container-component, but the types of sub-components are different and cannot be determined during coding.

Solution:

​ So I have to realize this function through calling RenderTreeBuilder and RenderFragment by hands.

Issue:

​ But I found an issue that @Key instruction doesn’t work in my solution, for example: I have 5 sub-components with @key from 1 to 5 in my container-component now, the no.4 and no.5 sub-components are re-rendered after I removed the no.3 sub-component.

All sub-components behind the one I just removed will be re-rendered.

To Reproduce

You can use my simple project to reproduce this issue, Index.razor will show you that.

https://github.com/CuteLeon/BlazorKeyOfRenderFragment

1. Create a ASP.NET Core Blazor in Web Assembly of .NET 5.0

2. Create Sub-Component

ItemComponent.razor

@inject ILogger<ItemComponent> logger;

<button class="btn btn-danger m-2">@Value</button>

@code {
    [Parameter]
    public int Value { get; set; }

    private void ClickMe()
    {
        this.logger.LogInformation($"Click {this.Value}");
    }

    protected override void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);

        this.logger.LogWarning($"After render {this.Value}");
    }
}

3. Create a Container-Component which @Key instruction is working well.

@Key instruction is working well in this container component, But it’s not my solution cause it can not meet my requirement.

ContainerComponent.razor

@inject ILogger<ContainerComponent> logger;

<div class="mx-2 my-4 p-3 rounded rounded-lg shadow border border-danger">
    <h3>ContainerComponent</h3>
    <button class="btn btn-block btn-primary" @onclick="ClickAddButton">Add Ten</button>
    <button class="btn btn-block btn-warning" @onclick="ClickDeleteButton">Delete One</button>
    <hr />

    @foreach (var item in this.Items)
    {
        var key = $"Item_{item}";
        <ItemComponent @key="key" Value="item"></ItemComponent>
    }
</div>

@code {
    private List<int> Items = new List<int>();
    private int start = 0;

    protected override void OnInitialized()
    {
        base.OnInitialized();

        ClickAddButton();
    }

    private void ClickAddButton()
    {
        const int length = 10;
        this.logger.LogInformation($"Add {length} more items");
        Items.AddRange(Enumerable.Range(start, length));
        start += length;

        this.StateHasChanged();
    }

    private void ClickDeleteButton()
    {
        if (!Items.Any()) return;

        int index = Items.Count() / 2;
        this.logger.LogInformation($"Delete number {Items.ElementAt(index)} item");
        Items.RemoveAt(index);

        this.StateHasChanged();
    }
}

4. Create my Container-Component which @Key instruction doesn’t work.

This is my solution, but @Key instruction is not working in this container-component.

RenderContainerComponent.razor

@inject ILogger<RenderContainerComponent> logger;

<div class="mx-2 my-4 p-3 rounded rounded-lg shadow border border-danger">
    <h3>RenderContainerComponent</h3>
    <button class="btn btn-block btn-primary" @onclick="ClickAddButton">Add Ten</button>
    <button class="btn btn-block btn-warning" @onclick="ClickDeleteButton">Delete One</button>
    <hr />

    @foreach (var renderFragment in this.RenderFragments)
    {
        @renderFragment
    }
</div>

@code {
    private List<RenderFragment> RenderFragments = new List<RenderFragment>();
    private Type ItemComponentType = typeof(ItemComponent);
    private string PropertyOfItemComponent = nameof(ItemComponent.Value);
    private int start = 0;

    protected override void OnInitialized()
    {
        base.OnInitialized();

        ClickAddButton();
    }

    private void ClickAddButton()
    {
        const int length = 10;
        this.logger.LogInformation($"Add {length} more Render Fragments");
        RenderFragments.AddRange(Enumerable.Range(start, length).Select(index =>
        {
            RenderFragment render = builder =>
            {
                var key = $"RenderFragment_{index}";
                builder.OpenRegion(index);
                // Have to render a component via generic function.
                builder.OpenComponent(0, this.ItemComponentType);
                builder.AddAttribute(1, PropertyOfItemComponent, index);
                builder.SetKey(key);
                builder.CloseComponent();
                builder.CloseRegion();
            };
            return render;
        }));
        start += length;

        this.StateHasChanged();
    }

    private void ClickDeleteButton()
    {
        if (!RenderFragments.Any()) return;

        int index = RenderFragments.Count() / 2;
        this.logger.LogInformation($"Delete no.{index} Render Fragment");
        RenderFragments.RemoveAt(index);

        this.StateHasChanged();
    }
}

5. Display both Container-Components above in Index.razor

Index.razor

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<ContainerComponent></ContainerComponent>

<RenderContainerComponent></RenderContainerComponent>

6. What To Do:

  1. Start project and go to home page.
  2. Open Console of Browser and watch debug log.
  3. Click “Add Ten” and “Delete One” button in home page.
  4. You will find out that some of Sub-Components in RenderContainerComponent is logging “After render {Value}” in Browser’s Console, because @Key instruction is not working and these Sub-Components are re-rendered.

Further technical details

  • ASP.NET Core version: .NET 5.0.101
  • IDE: Visual Studio 2019 (16.8.3)

Thanks for your time! Waiting for your reply.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:5 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
SteveSandersonMScommented, Feb 4, 2021

Thanks for clarifying. The issue here is that you have the @key on the component inside your RenderFragment, rather than on the fragment itself. When Blazor renders RenderFragment instances, it wraps them inside an invisible “region” marker to ensure that the diffing system knows to treat each one as an independent group. Since there’s no key on that region, the diffing system doesn’t have the same opinion that you do about how they should be matched up across renders.

You can fix this by putting the @key on something wrapped around your render fragment. For example, here’s one way to do it: https://gist.github.com/SteveSandersonMS/257fc94ff8919ee12886a468d271d9ae

If you really wanted to avoid the <span> for some reason, you could define another component that simply emits the renderfragment, and put the key on that instead.

As of .NET 6, there will also be the option of using <DynamicComponent> for this scenario, which simplifies things further as you don’t have to write your own logic with RenderTreeBuilder.

0reactions
CuteLeoncommented, Feb 4, 2021

Hi @SteveSandersonMS : Thanks for your explaining, I have never thought that the solution would be so tiny. It really helps a lot, and great looking forward to the .Net 6!

Read more comments on GitHub >

github_iconTop Results From Across the Web

Handle errors in ASP.NET Core Blazor apps
This article describes how Blazor manages unhandled exceptions and how to develop apps that detect and handle errors.
Read more >
Blazor RenderTree Explained - InfoQ
Manually written RenderTree's can be problematic if the sequence number is not a static linear number.
Read more >
State Hasn't Changed? Why and when Blazor components ...
This means the component that houses (and handles) that event will re-render, but sibling or parent components won't. How it works “under the ......
Read more >
Blazor in MVC: Component gets rendered, but @onclick ...
To fix the actual @onclick issue I had to put these using statements in the Blazor component: @using Microsoft.AspNetCore.Components @using ...
Read more >
10 Blazor Features You Probably Didn't Know
Blazor is a cutting-edge framework for web development with .NET. In this blog, we'll explain the most important features you need to know ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found