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 aIEnumerable<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 BlazorRenderFragment
.- 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 theData
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.
- If Grid editing is enabled and the columns are created from the keys of the first data item, use the
- 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 inOnUpdate
(example below). - Nullable data,
LoadGroupsOnDemand
, and nested model properties require special handling. Check the notes below for additional information.
@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 missingMemberType
properties in theCompositeFilterDescriptor
s in theOnRead
event argument, namely inargs.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 columnsPropertyInt
andPropertyBool
in the example below.CheckboxList
filtering in non-nullable data requiresOnRead
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 withOnRead
event requires aFilterMenuTemplate
. You can use aTelerikCheckBoxListFilter
component with custom data. The following code snippet is part of the full example below. See columnPropertyBool
.<div>Bool FilterMenuTemplate</div> <TelerikCheckBoxListFilter Data="@BoolCheckBoxListData" Field="@nameof(BoolCheckBoxModel.PropertyBool)" @bind-FilterDescriptor="@context.FilterDescriptor" />
Grouping
-
Loading groups on demand requires you to bind the Grid with
OnRead
event. Then, set theMemberType
of the group-relatedFilterDescriptor
in theOnRead
handler. Otherwise the expanded group will show only one item. Also see the related public item for built-in support. The following code snippet is part of the full example below.private async Task OnGridRead(GridReadEventArgs args) { args.Request.Filters.OfType<FilterDescriptor>() .Each(x => x.MemberType = GetPropertyType(x.Member, true)); // ... }
Loading groups on demand doesn't support grouping by nullable property.
Nested Properties
You can use nested Grid model properties that are ExpandoObject
s. 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 Telerik.DataSource
@using Telerik.DataSource.Extensions
@using System.Dynamic
<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
}