New to Telerik UI for Blazor? Download free 30-day trial

Bind Grid to Expando Object

Environment

Product Grid for Blazor,
TreeList for Blazor

Description

How to bind Grid to Expando object?

How to use the Grid with dynamic model type and dynamic columns?

How to edit nullable properties in the ExpandoObject?

Solution

The key points in the required implementation are:

  • Set the Grid Data parameter to a IEnumerable<ExpandoObject>.
  • Set the FieldType parameter of all bound columns.
  • If you need autogenerated Grid columns, then define them in a loop inside the <GridColumns> tag, which is a standard Blazor RenderFragment.
    • If Grid editing is enabled and the columns are created from the keys of the first data item, use the OnModelInit event to populate each newly added item with some default values. Otherwise the columns will disappear because the new data item (ExpandoObject) has no properties and is prepended to the Data collection. Another option is to create columns from a non-first item. null does not qualify as a default value, because it cannot help determine the property type.
  • The Grid clones data items during editing. When editing, you can store the original item in the Grid OnEdit handler. This will make the retrieval of the original item easier later in OnUpdate (example below).
  • Nullable data, LoadGroupsOnDemand, and nested model properties require special handling. Check the notes below for additional information.

Grid data operations with ExpandoObject

@using System.Dynamic

<TelerikGrid Data="@GridData"
             Pageable="true"
             Sortable="true"
             FilterMode="@GridFilterMode.FilterRow"
             EditMode="@GridEditMode.Incell"
             OnEdit="@OnGridEdit"
             OnUpdate="@OnGridUpdate"
             OnCreate="@OnGridCreate"
             OnModelInit="@OnGridModelInit"
             OnDelete="@OnGridDelete">
    <GridToolBarTemplate>
        <GridCommandButton Command="Add" Icon="@SvgIcon.Plus">Add Item</GridCommandButton>
    </GridToolBarTemplate>
    <GridColumns>
        @{
            @* dynamic columns *@

            if (GridData != null && GridData.Any())
            {
                // The first data item should always contain non-null values for the property types to be determined.
                // Use another data item or OnModelInit to populate default values for newly added items.
                var firstDataItem = (IDictionary<string, object>)GridData.First();

                foreach (var item in firstDataItem)
                {
                    if (item.Key != "Id")
                    {
                        <GridColumn Field="@item.Key" FieldType="@item.Value.GetType()" @key="@item.Key">
                        </GridColumn>
                    }
                }
            }
        }

        @* or static columns *@

        @*<GridColumn Field="PropertyInt" Title="Int Column" FieldType="@typeof(int)" />
        <GridColumn Field="PropertyString" Title="String Column" FieldType="@typeof(string)" />
        <GridColumn Field="PropertyDate" Title="DateTime Column" FieldType="@typeof(DateTime)" />*@

        <GridCommandColumn @key="@( "command-column" )">
            <GridCommandButton Command="Delete" Icon="@SvgIcon.Trash">Delete</GridCommandButton>
        </GridCommandColumn>
    </GridColumns>
</TelerikGrid>

@code {
    private List<ExpandoObject> GridData { get; set; } = new List<ExpandoObject>();

    private IDictionary<string, object> GridEditItem { get; set; }

    private int LastId { get; set; }

    private async Task OnGridEdit(GridCommandEventArgs args)
    {
        GridEditItem = (IDictionary<string, object>)args.Item;
    }

    private async Task OnGridUpdate(GridCommandEventArgs args)
    {
        var item = (IDictionary<string, object>)args.Item;

        // There are two ways to update the data item:
        // Store its instance in OnEdit, or find it in the Grid Data via search by Id.

        @*IDictionary<string, object> originalItem = GridData.Find(x =>
            {
                return ((IDictionary<string, object>)x)["Id"] == item["Id"];
            });*@

        // In cell editing - update one property
        @*originalItem[args.Field] = item[args.Field];*@
        GridEditItem[args.Field] = item[args.Field];

        // Inline or popup editing - update all properties
        @*foreach (string key in item.Keys)
            {
                //originalItem[key] = item[key];
                GridEditItem[key] = item[key];
            }*@
    }

    private ExpandoObject OnGridModelInit()
    {
        // Use OnModelInit to populate default values in newly created rows.
        // This is optional for the editing experience,
        // but required if the Grid columns are generated from the first data item.
        // The default values cannot be null, otherwise the property type cannot be determined.
        dynamic expando = new ExpandoObject();

        expando.Id = new int();
        expando.PropertyInt = new int();
        expando.PropertyString = String.Empty;
        expando.PropertyDate = DateTime.Now;

        return expando;
    }

    private async Task OnGridCreate(GridCommandEventArgs args)
    {
        var item = args.Item as ExpandoObject;

        ((IDictionary<string, object>)item)["Id"] = ++LastId;

        GridData.Insert(0, item);
    }

    private async Task OnGridDelete(GridCommandEventArgs args)
    {
        var item = args.Item as ExpandoObject;

        GridData.Remove(item);
    }

    protected override async Task OnInitializedAsync()
    {
        LastId = 15;

        for (int i = 1; i <= LastId; i++)
        {
            dynamic expando = new ExpandoObject();

            expando.Id = i;
            expando.PropertyInt = i;
            expando.PropertyString = "String " + i;
            expando.PropertyDate = DateTime.Now.AddMonths(-i);

            GridData.Add(expando);
        }
    }
}

Notes

The following list contains information about requirements and limitations when binding the Grid to ExpandoObject. They are related to using nullable data, editing, filtering, grouping, and nested model properties. A runnable example is available later in the article.

Editing

If any non-string property in the ExpandoObject is nullable, then do not set EditorType for the column. Instead, use a Grid column EditorTemplate. The following code snippet is part of the full example below.

<GridColumn Field="PropertyDate"
            FieldType="@GetPropertyType("PropertyDate")"
            Title="Date declared">
    <EditorTemplate>
        @{
            var editItem = (IDictionary<string, object>)context;
            DateTime? dateValue = (DateTime?)(editItem["PropertyDate"]);
            <TelerikDatePicker Value="@dateValue"
                                ValueChanged="@( (DateTime? newValue) => editItem["PropertyDate"] = newValue )"
                                DebounceDelay="0" />
        }
    </EditorTemplate>
</GridColumn>

Filtering

  • Filtering in nullable data requires you to use OnRead data binding. Populate the missing MemberType properties in the CompositeFilterDescriptors in the OnRead event argument, namely in args.Request.Filters. Also see the related public item. The following code snippet is part of the full example below.

    private async Task OnGridRead(GridReadEventArgs args)
    {
        args.Request.Filters.OfType<CompositeFilterDescriptor>()
            .Each(x =>
            {
                x.FilterDescriptors.OfType<FilterDescriptor>()
                    .Each(y => y.MemberType = GetPropertyType(y.Member, true));
            });
    
        // ...
    }
    
  • Row filtering in nullable data requires FilterCellTemplate for numeric and boolean properties. See columns PropertyInt and PropertyBool in the example below.

  • CheckboxList filtering in non-nullable data requires OnRead data binding and the same approach as in the first bullet item above.

  • The built-in CheckboxList filtering doesn't support nullable data. Instead, use the default filter menu or filter row. Using a filter menu template with a custom checkbox list is possible.

  • CheckboxList filtering in grouped data with OnRead event requires a FilterMenuTemplate. You can use a TelerikCheckBoxListFilter component with custom data. The following code snippet is part of the full example below. See column PropertyBool.

    <div>Bool FilterMenuTemplate</div>
    <TelerikCheckBoxListFilter Data="@BoolCheckBoxListData"
                                Field="@nameof(BoolCheckBoxModel.PropertyBool)"
                                @bind-FilterDescriptor="@context.FilterDescriptor" />
    

Grouping

Nested Properties

You can use nested Grid model properties that are ExpandoObjects. However, data operations like filtering, sorting, and grouping are not supported for those nested properties. Disable these features per column if they are enabled for the Grid.

Example

The following example demonstrates how the Grid can edit, filter, and group nullable ExpandoObject data. The example covers multiple different scenarios at the same time, so remove any code and settings that you don't need.

Using ExpandoObject with nullable data, CheckBoxList, and LoadGroupsOnDemand

<TelerikToolBar>
    <ToolBarButtonGroup>
        <ToolBarToggleButton Selected="@( GridFilterMode == GridFilterMode.FilterRow )"
                             SelectedChanged="@( (bool useFilterRow) =>
                                                 OnGridFilterModeChanged(useFilterRow) )">
            Row Filtering
        </ToolBarToggleButton>
        <ToolBarToggleButton Selected="@( GridFilterMode == GridFilterMode.FilterMenu )"
                             SelectedChanged="@( (bool useFilterMenu) =>
                                                 OnGridFilterModeChanged(!useFilterMenu) )">
            Menu Filtering
        </ToolBarToggleButton>
    </ToolBarButtonGroup>
    <ToolBarButtonGroup>
        <ToolBarToggleButton Selected="@( GridFilterMenuType == FilterMenuType.Menu )"
                             SelectedChanged="@( (bool useFilterMenu) =>
                                                 OnGridFilterMenuTypeChanged(useFilterMenu) )">
            Default Filter Menu
        </ToolBarToggleButton>
        <ToolBarToggleButton Selected="@( GridFilterMenuType == FilterMenuType.CheckBoxList )"
                             SelectedChanged="@( (bool useCheckBoxList) =>
                                                 OnGridFilterMenuTypeChanged(!useCheckBoxList) )">
            CheckBox List Filter Menu
        </ToolBarToggleButton>
    </ToolBarButtonGroup>
    <ToolBarButtonGroup>
        <ToolBarToggleButton Selected="@( !GridLoadGroupsOnDemand )"
                             SelectedChanged="@( (bool useDefaultGroupMode) =>
                                                 OnGridGroupModeChanged(useDefaultGroupMode) )">
            Load All Groups
        </ToolBarToggleButton>
        <ToolBarToggleButton Selected="@( GridLoadGroupsOnDemand )"
                             SelectedChanged="@( (bool useLoadOnDemand) =>
                                                 OnGridGroupModeChanged(!useLoadOnDemand) )">
            Load Groups On Demand
        </ToolBarToggleButton>
    </ToolBarButtonGroup>
    <ToolBarToggleButton Selected="@( UseNullableData )"
                         SelectedChanged="@OnUseNullableDataChanged">
        Use Nullable Data
    </ToolBarToggleButton>
</TelerikToolBar>

@if (ShowGrid)
{
    <TelerikGrid @ref="@GridRef"
                 OnRead="@OnGridRead"
                 TItem="@ExpandoObject"
                 Pageable="true"
                 Sortable="true"
                 FilterMode="@GridFilterMode"
                 FilterMenuType="@GridFilterMenuType"
                 Groupable="true"
                 LoadGroupsOnDemand="@GridLoadGroupsOnDemand"
                 EditMode="@GridEditMode.Incell"
                 OnUpdate="@OnGridUpdate"
                 OnStateInit="@OnGridStateInit">
        <GridToolBarTemplate>
            <GridSearchBox />
        </GridToolBarTemplate>
        <GridColumns>
            @{
                if (GridData != null && GridData.Any())
                {
                    if (!UseNullableData && GridFilterMenuType != FilterMenuType.CheckBoxList)
                    {
                        // FieldType can also be set with FieldType="@item.Value.GetType()", but
                        // only if the iterated data item (firstDataItem) contains non-null values.
                        var firstDataItem = GridData.First() as IDictionary<string, object>;

                        foreach (var item in firstDataItem)
                        {
                            if (item.Key != "Id")
                            {
                                <GridColumn Field="@item.Key"
                                            FieldType="@GetPropertyType(item.Key)"
                                            Title="@( $"{item.Key.Replace("Property", string.Empty)} foreached" )"
                                            @key="@item.Key">
                                </GridColumn>
                            }
                        }
                    }
                    else
                    {
                        <GridColumn Field="PropertyInt"
                                    FieldType="@GetPropertyType("PropertyInt")"
                                    Title="@( $"Int declared" )">
                            <FilterCellTemplate>
                                <TelerikNumericTextBox Value="@IntColumnFilterValue"
                                                       ValueChanged="@( (int? newValue) => IntFilterValueChanged(newValue, context) )" />
                                <TelerikButton Icon="@SvgIcon.FilterClear"
                                               OnClick="@( async () => {
                                                                       IntColumnFilterValue = null;
                                                                       await context.ClearFilterAsync();
                                                                   } )"></TelerikButton>
                            </FilterCellTemplate>
                            <EditorTemplate>
                                @{
                                    var editItem = (IDictionary<string, object>)context;
                                    int? intValue = (int?)(editItem["PropertyInt"]);
                                    <TelerikNumericTextBox Value="@intValue"
                                                           ValueChanged="@( (int? newValue) => editItem["PropertyInt"] = newValue )" />
                                }
                            </EditorTemplate>
                        </GridColumn>
                        <GridColumn Field="PropertyString"
                                    FieldType="@GetPropertyType("PropertyString")"
                                    Title="@( $"String declared" )" />
                        <GridColumn Field="PropertyGroup"
                                    FieldType="@GetPropertyType("PropertyGroup")"
                                    Title="@( $"Group string declared" )" />
                        <GridColumn Field="PropertyDate"
                                    FieldType="@GetPropertyType("PropertyDate")"
                                    Title="Date declared">
                            <EditorTemplate>
                                @{
                                    var editItem = (IDictionary<string, object>)context;
                                    DateTime? dateValue = (DateTime?)(editItem["PropertyDate"]);
                                    <TelerikDatePicker Value="@dateValue"
                                                       ValueChanged="@( (DateTime? newValue) => editItem["PropertyDate"] = newValue )"
                                                       DebounceDelay="0" />
                                }
                            </EditorTemplate>
                        </GridColumn>
                        <GridColumn Field="PropertyBool"
                                    FieldType="@GetPropertyType("PropertyBool")"
                                    Title="@( $"Bool declared" )"
                                    FilterMenuTemplate="@GetGridBoolFilterMenuTemplate()">
                            <FilterCellTemplate>
                                <TelerikDropDownList Data="@( new List<bool>() { true, false } )"
                                                     DefaultText="(All)"
                                                     Value="@BoolColumnFilterValue"
                                                     ValueChanged="@( (bool? newValue) => BoolFilterValueChanged(newValue, context) )" />
                                <TelerikButton Icon="@SvgIcon.FilterClear"
                                               OnClick="@( async () => {
                                                                       BoolColumnFilterValue = null;
                                                                       await context.ClearFilterAsync();
                                                                   } )"></TelerikButton>
                            </FilterCellTemplate>
                            <EditorTemplate>
                                @{
                                    var editItem = (IDictionary<string, object>)context;
                                    bool? boolValue = (bool?)(editItem["PropertyBool"]);
                                    <TelerikCheckBox Value="@boolValue"
                                                     ValueChanged="@( (bool? newValue) => editItem["PropertyBool"] = newValue )" />
                                }
                            </EditorTemplate>
                        </GridColumn>
                    }
                }
            }
        </GridColumns>
    </TelerikGrid>
}

@code {
    private TelerikGrid<ExpandoObject> GridRef { get; set; } = null!;

    private List<ExpandoObject> GridData { get; set; } = new List<ExpandoObject>();

    private bool ShowGrid { get; set; } = true;

    #region Grid Data Property Types

    private Dictionary<string, Type> GridPropertyTypes { get; set; } = new Dictionary<string, Type>() {
        { "Id", typeof(int) },
        { "PropertyInt", typeof(int) },
        { "PropertyString", typeof(string) },
        { "PropertyGroup", typeof(string) },
        { "PropertyDate", typeof(DateTime) },
        { "PropertyBool", typeof(bool) }
    };
    private Dictionary<string, Type> GridPropertyTypesNullable { get; set; } = new Dictionary<string, Type>() {
        { "Id", typeof(int) },
        { "PropertyInt", typeof(int?) },
        { "PropertyString", typeof(string) },
        { "PropertyGroup", typeof(string) },
        { "PropertyDate", typeof(DateTime?) },
        { "PropertyBool", typeof(bool?) }
    };

    private Type GetPropertyType(string columnName, bool allowNullable = false)
    {
        if (allowNullable && UseNullableData)
        {
            // for filtering in nullable data
            return GridPropertyTypesNullable[columnName];
        }
        else
        {
            // for columns and filtering in non-nullable data
            return GridPropertyTypes[columnName];
        }
    }

    #endregion Grid Data Property Types

    #region FilterCellTemplates

    private int? IntColumnFilterValue { get; set; }
    private FilterOperator IntColumnFilterOperator { get; set; } = FilterOperator.IsEqualTo;
    private bool? BoolColumnFilterValue { get; set; }

    private async Task IntFilterValueChanged(int? newValue, FilterCellTemplateContext context)
    {
        IntColumnFilterValue = newValue;

        var fd = (FilterDescriptor)context.FilterDescriptor.FilterDescriptors.First();
        // Hard-coded IsEqualTo operator.
        // You can change it here or enable users to select it from the FilterCellTemplate.
        fd.Operator = IntColumnFilterOperator;
        fd.Value = newValue;

        await context.FilterAsync();
    }

    private async Task BoolFilterValueChanged(bool? newValue, FilterCellTemplateContext context)
    {
        BoolColumnFilterValue = newValue;

        var fd = (FilterDescriptor)context.FilterDescriptor.FilterDescriptors.First();
        fd.Operator = FilterOperator.IsEqualTo;
        fd.Value = newValue;

        await context.FilterAsync();
    }

    #endregion FilterCellTemplates

    #region FilterMenuTemplates

    private RenderFragment<FilterMenuTemplateContext> GridBoolFilterMenuTemplateContent { get; set; } = context => __builder =>
    {
        <div>Bool FilterMenuTemplate</div>
        <TelerikCheckBoxListFilter Data="@BoolCheckBoxListData"
                                   Field="@nameof(BoolCheckBoxModel.PropertyBool)"
                                   @bind-FilterDescriptor="@context.FilterDescriptor" />
    };

    private RenderFragment<FilterMenuTemplateContext> GetGridBoolFilterMenuTemplate()
    {
        if (GridFilterMenuType == FilterMenuType.CheckBoxList)
        {
            return GridBoolFilterMenuTemplateContent;
        }
        else
        {
            return null;
        }
    }

    private static List<BoolCheckBoxModel> BoolCheckBoxListData { get; set; } = new List<BoolCheckBoxModel>() {
        new BoolCheckBoxModel() { PropertyBool = true },
        new BoolCheckBoxModel() { PropertyBool = false }
    };

    public class BoolCheckBoxModel
    {
        public bool PropertyBool { get; set; }
    }

    #endregion FilterMenuTemplates

    #region Editing

    private IDictionary<string, object> GridEditItem { get; set; }

    private async Task OnGridUpdate(GridCommandEventArgs args)
    {
        var item = (IDictionary<string, object>)args.Item;

        var originalItem = GridData.First(x =>
            {
                return (x as IDictionary<string, object>)["Id"] == item["Id"];
            }) as IDictionary<string, object>;

        originalItem[args.Field] = item[args.Field];
    }

    #endregion Editing

    #region Grid Data Binding

    private async Task OnGridRead(GridReadEventArgs args)
    {
        // for filtering
        args.Request.Filters.OfType<CompositeFilterDescriptor>()
            .Each(x =>
            {
                x.FilterDescriptors.OfType<FilterDescriptor>()
                    .Each(y => y.MemberType = GetPropertyType(y.Member, true));
            });

        // for data of one group when LoadGroupsOnDemand = true
        args.Request.Filters.OfType<FilterDescriptor>()
        .Each(x => x.MemberType = GetPropertyType(x.Member, true));

        await Task.Delay(100); // simulate network delay

        var result = GridData.ToDataSourceResult(args.Request);

        args.Data = result.Data;
        args.Total = result.Total;
        args.AggregateResults = result.AggregateResults;
    }

    #endregion Grid Data Binding

    #region Example Helpers

    private GridFilterMode GridFilterMode { get; set; } = GridFilterMode.FilterRow;

    private FilterMenuType GridFilterMenuType { get; set; } = FilterMenuType.Menu;

    private bool GridLoadGroupsOnDemand { get; set; }

    private bool UseNullableData { get; set; } = true;

    private void OnGridStateInit(GridStateEventArgs<ExpandoObject> args)
    {
        args.GridState.GroupDescriptors.Add(new GroupDescriptor()
        {
            Member = "PropertyGroup",
            MemberType = typeof(string)
        });
    }

    private async Task OnGridFilterModeChanged(bool useFilterRow)
    {
        // recreate Grid to reassign the columns and filter templates
        ShowGrid = false;
        await Task.Delay(100);

        GridFilterMode = useFilterRow ? GridFilterMode.FilterRow : GridFilterMode.FilterMenu;

        ShowGrid = true;
    }

    private async Task OnGridFilterMenuTypeChanged(bool useDefaultFilterMenu)
    {
        // recreate Grid to reassign the columns and filter templates
        ShowGrid = false;
        await Task.Delay(100);

        GridFilterMode = GridFilterMode.FilterMenu;
        GridFilterMenuType = useDefaultFilterMenu ? FilterMenuType.Menu : FilterMenuType.CheckBoxList;

        if (!useDefaultFilterMenu)
        {
            UseNullableData = false;
        }

        GenerateData();

        ShowGrid = true;
    }

    private void OnGridGroupModeChanged(bool useDefaultGrouping)
    {
        GridLoadGroupsOnDemand = !useDefaultGrouping;
        GenerateData();
        GridRef.Rebind();
    }

    private void OnUseNullableDataChanged(bool useNullableData)
    {
        UseNullableData = useNullableData;

        GenerateData();
        GridRef.Rebind();
    }

    #endregion Example Helpers

    #region Data Generation

    protected override void OnInitialized()
    {
        GenerateData();
    }

    private void GenerateData()
    {
        GridData = new List<ExpandoObject>();

        for (int i = 1; i <= 25; i++)
        {
            dynamic expando = new ExpandoObject();

            expando.Id = i;

            if (i % 6 == 2 && UseNullableData)
                expando.PropertyInt = null;
            else
                expando.PropertyInt = i * 11;

            if (i % 6 == 1 && UseNullableData)
                expando.PropertyString = null;
            else
                expando.PropertyString = $"String {i}";

            if (i % 6 == 0 && UseNullableData && !GridLoadGroupsOnDemand)
                expando.PropertyGroup = null;
            else
                expando.PropertyGroup = $"Group {(i % 3 + 1)}";

            if (i % 6 == 4 && UseNullableData)
                expando.PropertyDate = null;
            else
                expando.PropertyDate = DateTime.Now.AddMonths(-i).AddDays(-i * 2);

            if (i % 6 == 5 && UseNullableData)
                expando.PropertyBool = null;
            else
                expando.PropertyBool = i % 2 == 0;

            GridData.Add(expando);
        }
    }

    #endregion Data Generation
}

See Also

In this article