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 the Grid columns are created from the keys of first data item, they will disappear when adding a new item. This is because the newly added data item (
ExpandoObject
) has no properties and is prepended to theData
collection. To avoid this, either create columns from a non-first item, or useOnModelInit
to populate the newly added item with some default values.null
does not qualify as a default value, because it cannot help determine the property type. The example below includes anOnModelInit
handler. - 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). - Nested model properties and nullable types need special handling. Make sure to check the notes below the example.
In addition to the sample below, there is a complete runnable project in GitHub.
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="@FontIcon.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="@FontIcon.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
- You can also 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. - If any property in the
ExpandoObject
isnullable
, then do not setEditorType
for the column (this may be supported in the future). Instead, use a Grid columnEditorTemplate
, for example:
<GridColumn Field="@item.Key" FieldType="@(typeof(DateTime?))" @key="@item.Key">
<EditorTemplate>
@{
var editItem = (IDictionary<string, object>)context;
DateTime? dateValue = (DateTime?)(editItem[item.Key]);
<TelerikDatePicker Value="@dateValue"
ValueChanged="@( (DateTime? newValue) => editItem[item.Key] = newValue )" />
}
</EditorTemplate>
</GridColumn>