Search code examples
c#blazor.net-8.0blazor-component

Blazor 8.0 nested component not rendered


I tried to create a grid component by myself. I created component called DataTable and another called Column. Here is the code for DataTable:

public partial class DataTable<TEntity> : ComponentBase
{
    // [Parameter]
    // public RenderFragment Header { get; set; }

    // [Parameter]
    // public RenderFragment Columns { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public List<IColumn<TEntity>> DataColumns { get; set; } = new List<IColumn<TEntity>>();

    public List<TEntity> Data { get; set; } = new List<TEntity>();

    protected override async Task OnInitializedAsync()
    {
        // Get data
    }

    public void AddColumn(IColumn<TEntity> column)
    {
        DataColumns.Add(column);
    }
}

And the Razor part:

@typeparam TEntity
@attribute [CascadingTypeParameter(nameof(TEntity))]

<table>
    <thead>
        <tr>
            @foreach (IColumn<TEntity> column in DataColumns) {
                <th>A</th>
            }
        </tr>
    </thead>
    <tbody>
        <tr>
            foreach (TEntity entity in Data) {
                foreach (IColumn<TEntity> column in DataColumns) {
                    <td>@column.Value.Compile()(entity)</td>
                }
            }
        </tr>
    </tbody>
</table>

And this is the code for Column:

public partial class Column<TEntity, TValue> : ComponentBase, IColumn<TEntity>
{
    [CascadingParameter]
    private DataTable.DataTable<TEntity> Parent { get; set; }

    [Parameter]
    public Expression<Func<TEntity, TValue>> Value { get; set; }

    [Parameter]
    public Func<TValue, string> Format { get; set; }

    protected override void OnInitialized()
    {
        if (Parent == null)
            throw new ArgumentNullException(nameof(Parent), "");

        Parent.AddColumn(this);

        base.OnInitialized();
    }
}

And in HTML I call my component in this way:

<DataTable TEntity="User">
    @* <Header></Header>
    <Columns>
        <Column Value="x => x.Name"></Column>
        <Column Value="x => x.Surname"></Column>
        <Column Value="x => x.LastAccess" Format="@(x => x?.ToString("dd/MM/yyyy HH:mm"))"></Column>
    </Columns> *@
    <Column Value="x => x.Name"></Column>
    <Column Value="x => x.Surname"></Column>
    <Column Value="x => x.LastAccess" Format="@(x => x?.ToString("dd/MM/yyyy HH:mm"))"></Column>
</DataTable>

The problem is that method OnInitialized in Column component has never fired, so my DataColumns property remains empty. What I'm doing wrong? As you can see I tried to move columns creation outside Columns fragment but without success.


Solution

  • THe meziantou blog does mention and solve the problem. You seem to have omitted the 2 crucial elements that bring the columns to life:

    ChildContent contains the columndefinitions. You have to render it to actually create the columns.

    <CascadingValue IsFixed="true" Value="this">@ChildContent</CascadingValue>
    

    Note that the Column components do not have any markup, this will not render anything visual, it will just create the components.

    But the above line will create the columns too late for the first render of the table. So you will have to immediately request a second render:

    protected override void OnAfterRender(bool firstRender)
    {
      if (firstRender)
      {
        // The first render will instantiate the GridColumn defined in the ChildContent.
        // GridColumn calls AddColumn during its initialization. This means that until
        // the first render is completed, the columns collection is empty.
        // Calling StateHasChanged() will re-render the component,
        // so the second time it will know the columns
    
        StateHasChanged();
      }
    }