Search code examples
blazorblazor-webassemblyasp.net-blazor

Blazor pass context to RenderFragment


So, I have a page that contains a grid

SearchPage.razor

<FiGrid @ref="Grid"
    <Columns>
        @Columns
    </Columns>
    <EditFormTemplate Context="contextEntity">
        @EditFormTemplate(contextEntity)
    </EditFormTemplate>
</FiGrid>

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

protected RenderFragment<object> EditFormTemplate { get; set; }
}

this page has a RenderFragment to render the columns and one to render an editForm whenever is necessary

i inherit from this SearchPage to customize this 2 props:

CustomSearchPage.razor

@inherits SearchPage

@{
    base.BuildRenderTree(__builder);
}

@code {

    private static RenderFragment _customColumns = __builder =>
    {
        <CustomColumn FieldName="@nameof(Factory.Id)" Width="75px"/>
        <CustomColumn FieldName="@nameof(Factory.BusinessName)"/>
        <CustomColumn FieldName="@nameof(Factory.Address)"/>
    };

    private static RenderFragment<object> _customEditFormTemplate = editModel => __builder =>
    {
        <CustomEditForm Entity="editModel"/>;
    };

    protected override void OnInitialized()
    {
        Columns = _customColumns;
        EditFormTemplate = _customEditFormTemplate;
        base.OnInitialized();
    }

}

but it gives me this error on the console:

Operation is not valid due to the current state of the object.

maybe I'm using the wrong approach, in that case, how should I accomplish the same thing?

I need to be able to inherit from a page and pass the page a custom edit form that takes the context of the template

basically I want to pass the context of the template down to a RenderFragment created via code


Solution

  • You are getting the error because you can't build component inheritance in the way you envisage. You can't call BuildRenderTree manually like this:

    base.BuildRenderTree(__builder);
    

    because you don't have a reference to the Renderer's RenderTreeBuilder.

    A RenderFragment is a delegate defined as:

    public delegate void RenderFragment(RenderTreeBuilder builder);
    

    It's run by the Renderer which invokes it and passes in the Renderer's instance of RenderTreeBuilder.

    All Razor pages are pre-compiled into C# classes. So this simple component DisplayDiv.razor:

    <h3>@Header</h3>
    
    @code {
        [Parameter] public string Header { get; set; } = "Hello Blazor";
    }
    

    gets pre-compiled by the Razor processor to something like this:

    public class DisplayDiv : ComponentBase
    {
        [Parameter] public string Header { get; set; } = "Hello Blazor";
    
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, "h3");
            builder.AddContent(1, this.Header);
            builder.CloseElement();
        }
    }
    

    BuildRenderTree gets added to ComponentBase's base RenderFragment which StateHasChanged passes to the Renderer when it's called to render the component.

    private readonly RenderFragment _renderFragment;
    
    public ComponentBase()
    {
        _renderFragment = builder =>
        {
            _hasPendingQueuedRender = false;
            _hasNeverRendered = false;
            BuildRenderTree(builder);
        };
    }
    

    You are trying to define multiple copies of BuildRenderTree, which you can't do within the razor page setting. The child BuildRenderTree method built by the Razor pre-compiler overrides that of the parent.

    Your SearchPage is a component which you then use in a page

    <SearchPage>
        <Columns>
            <CustomColumn FieldName="@nameof(Factory.Id)" Width="75px"/>
            <CustomColumn FieldName="@nameof(Factory.BusinessName)"/>
            <CustomColumn FieldName="@nameof(Factory.Address)"/>
        </Columns>
        <EditFormTemplate>
            <CustomEditForm Entity="editModel"/>
        </EditFormTemplate>
    </SearchPage>
    
    

    You can do inheritance in components, but your parent components need to be classes, not Razor components, and build out any markup directly in code.