This example introduces batch data editing support when using Microsoft Entity Framework Core in your DevExpress-powered Blazor app.
Our sample uses DbContext to obtain and update DevExpress Blazor TreeList data. When a user creates a new row or modifies/deletes an existing row, a DbContext instance tracks changes made to underlying data. End users can press Save to record all changes made within this context or press Cancel to dispose the context and discard accumulated changes.
The CustomizeElement event handler uses the DbContext.ChangeTracker property to identify and highlight modified cells.
Files to Review
Documentation
More Examples
- Getting Started with the Blazor TreeList
- How to bind the component to DevExtreme data source with Entity Framework Core
Does this example address your development requirements/objectives?
(you will be redirected to DevExpress.com to submit your response)
Example Code
Razor@page "/"
@using Microsoft.EntityFrameworkCore
@using Microsoft.EntityFrameworkCore.ChangeTracking
@using TreeListBatchEditing.Services;
@using TreeListBatchEditing.Data;
@implements IDisposable
@rendermode InteractiveServer
@inject CitiesService CitiesService
@inject IDbContextFactory<CitiesContext> CitiesContextFactory
<div>
<DxTreeList @ref="TreeList"
Data="@Data"
KeyFieldName="ID"
ParentKeyFieldName="ParentID"
HasChildrenFieldName="HasChildren"
AutoExpandAllNodes="true"
ValidationEnabled="false"
EditMode="TreeListEditMode.EditCell"
EditModelSaving="TreeList_EditModelSaving"
CustomizeElement="TreeList_CustomizeElement"
CustomizeEditModel="TreeList_CustomizeEditModel">
<Columns>
<DxTreeListDataColumn Caption="Location" FieldName="Name" />
<DxTreeListDataColumn FieldName="CityType" />
<DxTreeListDataColumn FieldName="Year" DisplayFormat="d" />
<DxTreeListDataColumn FieldName="RecordType" />
<DxTreeListDataColumn FieldName="Population" />
<DxTreeListCommandColumn Width="90px">
<HeaderTemplate>
<DxButton IconCssClass="treelist-icon treelist-icon-add"
RenderStyle="ButtonRenderStyle.Link"
aria-label="New"
Click="@(() => TreeList.StartEditNewRowAsync())" />
</HeaderTemplate>
<CellDisplayTemplate>
<div class="treelist-cell-align-center">
<DxButton IconCssClass="treelist-icon treelist-icon-add"
RenderStyle="ButtonRenderStyle.Link"
aria-label="New"
Click="@(() => TreeList.StartEditNewRowAsync(context.VisibleIndex))" />
<DxButton IconCssClass="treelist-icon treelist-icon-delete"
RenderStyle="ButtonRenderStyle.Link"
aria-label="Delete"
Click="@(() => Delete_Click((Location)context.DataItem))" />
</div>
</CellDisplayTemplate>
<CellEditTemplate>
<div class="treelist-cell-align-center">
<DxButton Enabled="false"
CssClass="treelist-disabled-button"
IconCssClass="treelist-icon treelist-icon-add"
aria-label="New"
RenderStyle="ButtonRenderStyle.Link" />
<DxButton Enabled="false"
CssClass="treelist-disabled-button"
IconCssClass="treelist-icon treelist-icon-delete"
aria-label="Delete"
RenderStyle="ButtonRenderStyle.Link" />
</div>
</CellEditTemplate>
</DxTreeListCommandColumn>
</Columns>
<ToolbarTemplate>
<DxToolbar ItemRenderStyleMode="ToolbarRenderStyleMode.Plain">
<DxToolbarItem Text="Save" Click="Save_Click" IconCssClass="treelist-toolbar-save" Enabled="BatchItemsEnabled" />
<DxToolbarItem Text="Cancel" Click="Cancel_Click" IconCssClass="treelist-toolbar-cancel" Enabled="BatchItemsEnabled" />
</DxToolbar>
</ToolbarTemplate>
</DxTreeList>
</div>
@code {
ITreeList TreeList { get; set; }
IList<Location> Data { get; set; }
CitiesContext DbContext { get; set; }
bool BatchItemsEnabled => DbContext.ChangeTracker.HasChanges() || TreeList.IsEditing();
protected override async Task OnInitializedAsync() {
await ReloadData();
}
void IDisposable.Dispose() {
DbContext?.Dispose();
}
async Task New_Click() => await TreeList.StartEditNewRowAsync();
async Task Save_Click() => await DbContext.SaveChangesAsync();
async Task Cancel_Click() => await ReloadData();
void Delete_Click(Location dataItem) {
DbContext.Remove(dataItem);
Data.Remove(dataItem);
TreeList.Reload();
}
void TreeList_CustomizeElement(TreeListCustomizeElementEventArgs e) {
if (e.ElementType == TreeListElementType.DataCell) {
var employee = (Location)TreeList.GetDataItem(e.VisibleIndex);
var column = (ITreeListDataColumn)e.Column;
if (TryGetChangedEntityEntry(employee, out var entityEntry)) {
if (entityEntry.State == EntityState.Added || entityEntry.Property(column.FieldName).IsModified)
e.CssClass = "treelist-modified-cell";
}
}
}
void TreeList_CustomizeEditModel(TreeListCustomizeEditModelEventArgs e) {
if (e.IsNew) {
var newLocation = (Location)e.EditModel;
newLocation.ID = Data.Max(x => x.ID) + 1;
if (e.ParentDataItem != null)
newLocation.ParentID = ((Location)e.ParentDataItem).ID;
}
else
e.EditModel = e.DataItem;
}
async Task TreeList_EditModelSaving(TreeListEditModelSavingEventArgs e) {
if (e.IsNew) {
var editModel = (Location)e.EditModel;
await DbContext.AddAsync(editModel);
Data.Add(editModel);
}
}
bool TryGetChangedEntityEntry<T>(T entity, out EntityEntry<T> entry) where T : class {
entry = null;
if (!DbContext.ChangeTracker.HasChanges())
return false;
entry = DbContext.ChangeTracker.Entries<T>().FirstOrDefault(x => x.Entity == entity);
return entry != null && entry.State != EntityState.Unchanged && entry.State != EntityState.Detached;
}
async Task ReloadData() {
DbContext?.Dispose();
DbContext = await CitiesContextFactory.CreateDbContextAsync();
Data = await DbContext.Locations.ToListAsync();
TreeList?.Reload();
}
}
CSS::deep .treelist-toolbar-save,
::deep .treelist-toolbar-cancel {
background-size: contain;
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
background-position: center center;
background-color: currentColor;
height: 16px;
width: 16px;
opacity: 0.7;
}
::deep .treelist-toolbar-save {
-webkit-mask-image: url("images/icons/save.svg");
mask-image: url("images/icons/save.svg");
}
::deep .treelist-toolbar-cancel {
-webkit-mask-image: url("images/icons/undo.svg");
mask-image: url("images/icons/undo.svg");
}
::deep .treelist-modified-cell {
background-color: rgba(var(--bs-primary-rgb), 0.15) !important;
}
::deep .treelist-icon {
display: inline-block;
width: 1rem;
height: 1rem;
min-width: 1rem;
min-height: 1rem;
background-size: contain;
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
background-position: center center;
background-color: currentColor;
vertical-align: middle;
}
::deep .treelist-icon-add {
-webkit-mask-image: url("../images/icons/add.svg");
mask-image: url("../images/icons/add.svg");
}
::deep .treelist-icon-delete {
-webkit-mask-image: url("../images/icons/delete.svg");
mask-image: url("../images/icons/delete.svg");
}
::deep .treelist-cell-align-center {
display: flex;
justify-content: center;
align-items: center;
margin-top: -4px;
margin-bottom: -4px;
}