Understanding QuickGrid internals: "Defer hack"

363 Views Asked by At

I am studying the source code of the QuickGrid from Blazor (ASP.NET Core 8). The implementation leverages some internal knowledge on how Blazor handles the actual rendering in order to collect all ColumnBase child components. It does so by initiating and ending a "collecting session" and during this session all ColumnBase child components attach themselves to the cascaded grid context.

<CascadingValue TValue="InternalGridContext<TGridItem>" IsFixed="true" Value="@_internalGridContext">
    @{ StartCollectingColumns(); }
    @ChildContent
    <Defer>
        @{ FinishCollectingColumns(); }
        <ColumnsCollectedNotifier TGridItem="TGridItem" />

        @* HTML table... *@
    </Defer>
</CascadingValue>

The ColumnBase components inside the ChildContent execute the following code in their BuildRenderTree method:

InternalGridContext.Grid.AddColumn(this, InitialSortDirection, IsDefaultSortColumn);

The Defer component ist built like this:

// This is used by QuickGrid to move its body rendering to the end of the render queue so we can collect
// the list of child columns first. It has to be public only because it's used from .razor logic.
public sealed class Defer : ComponentBase
{
    [Parameter] public RenderFragment? ChildContent { get; set; }

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        builder.AddContent(0, ChildContent);
    }
}

There is also a comment in the Defer component explaining what it does which I do understand. However, I do not exactly understand how and why this works. Can someone explain to me the details on how and why this works?

It somehow suggests that RenderFragments are delayed when rendering. But thats not really intuitive to me. I am thinking of the rendering as some sort of a left-order tree traversal of the nodes including the RenderFragments. But it almost looks like RenderFragments are not traversed initially.

2

There are 2 best solutions below

0
On BEST ANSWER

I'm assuming you understand:

  1. What a RenderFragment really is - a delegate.
  2. That Razor files are compiled into C# classes.
  3. That Razor markup is compiled into a set of RenderTreeBuilder instructions.

When the renderer steps through the generated RenderTreeBuilder C# code it instantiates, injects, attaches and calls SetParametersAsync on each column component as it comes to it. In the column code there's no async yielding code involved, so everything happens in sequence, including stacking the render fragments for each column on the Renderer's Queue. The Blazor UI runs in a Synchronisation Context which ensures a single thread of execution.

So each component's RenderFragment - the registration process - is run in sequence and all the registrations are complete before we get to the last component Defer. It's ChildContent is provided by QuickGrid.

You can see that by placing the render fragment that actually builds the grid inside a subordinate component - Defer - you delay the rendering of that fragment until the processes it depends on - the column registrations - are complete.

You can use a similar process for other types of composite controls.

1
On

I am thinking of the rendering as some sort of a left-order tree traversal of the nodes including the RenderFragments.

The traversal is not really depth-first but more like breadth-first. The child components are created when the parent is rendering, and all render after the parent.

But it almost looks like RenderFragments are not traversed initially.

This is not about RenderFragments but about child components. As far as I can tell the real purpose of <Defer> is the @{ FinishCollectingColumns(); } line.
Without <Defer> the columns would not yet be registered at that point. Because the columns would have been created but none would have rendered and thus not have had a chance to call AddColumn(this,).