Search code examples
blazorblazor-server-sideblazor-client-side

Blazor Grid Bound Column by Field Name


I'm studying some paid grid components and they have a pretty cool "Bound Column" technique:

<TelerikGrid Data=@GridData>
    <GridColumns>
        <GridColumn Field="TemperatureC" Title="Temp. C" />
        <GridColumn Field="Summary" />
    </GridColumns>
    <GridToolBar>
        <GridCommandButton Command="Add" Icon="add">Add Forecast</GridCommandButton>
    </GridToolBar>
</TelerikGrid>

<DxDataGrid Data="@DataSource">
    <DxDataGridColumn Field="@nameof(ProductFlat.Id)">
    </DxDataGridColumn>
    <DxDataGridColumn Field="@nameof(ProductFlat.Category)">
    </DxDataGridColumn>
</DxDataGrid>

how can I achieve this? I want to create a very basic table:

<MyGrid DataSource=@MySource>
  <MyColumn Field="Id" Name="My Id" />
</MyGrid>

to render:

<table class="table">
  <thead>
    <tr>
      <th scope="col">My Id</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>#1</td>
    </tr>
    <tr>
      <td>#2</td>
    </tr>
  </tbody>
</table>

Solution

  • It's not a trivial solution, you should to understand how Blazor works with components. Basically, the way, is to register children MyColumn "templates" on outer MyGrid component, then, render grid dynamically with a bit of reflection. Below I post the recipe ready for copy-paste, test and learn.

    Step 0: Create the templated blazor demo:

    dotnet new blazorserver
    

    Step 1: Create Pages/MyColumn.razor component:

    @code {
        [Parameter]
        public string Field { get; set; }
    
        [CascadingParameter]
        private MyGrid Parent { get; set; }    
    
        protected override void OnInitialized()
        {
            base.OnInitialized();        
            Parent.AddColumn(this); //register child into parent
        }
    }
    

    Step 2: Create Pages/MyGrid.razor component:

    <CascadingValue Value="this">
    <table class="table">
        <tbody>
            @ChildContent
            @foreach (var item in DataSource)
            {
                @RowItem(item) 
            }
        </tbody>
    </table>
    </CascadingValue>
    
    @code {
        [Parameter]
        public RenderFragment ChildContent { get; set; }
    
        [Parameter]
        public IEnumerable<object> DataSource { get; set; }
    
        List<MyColumn> Columns = new List<MyColumn>();
        public void AddColumn(MyColumn column)
        {
            Columns.Add(column);
            StateHasChanged();
        }
    

    // Here the function to create each row dynamically:

        private RenderFragment RowItem(object row)
        {   
            int i = 0;
            return b =>          // create dynamically
            {
                b.OpenElement(++i, "tr");
                foreach(var column in Columns)
                {
                    b.OpenElement(++i, "td");
                    var val = row.GetType().GetProperty(column.Field)
                              .GetValue(row, null).ToString();                
                    b.AddContent(++i,val);
                    b.CloseElement();
                }
                b.CloseElement();
            };
        }
    }
    

    Step 3: Use your grid on Pages/FetchData.razor

    @page "/fetchdata"
    
    @using s.Data
    @inject WeatherForecastService ForecastService
    
    <h1>Weather forecast</h1>
    
    <p>This component demonstrates fetching data from a service.</p>
    
    @if (forecasts == null)
    {
        <p><em>Loading...</em></p>
    }
    else
    {
        <MyGrid DataSource=@forecasts>
           <MyColumn Field="Date"/>
           <MyColumn Field="Summary"/>
        </MyGrid>
    }
    
    @code {
        private WeatherForecast[] forecasts;
    
        protected override async Task OnInitializedAsync()
        {
            forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
        }
    }
    

    Result:

    enter image description here