Set AllowEdit="true" on MainGrid / SubGrid and a toggle labelled Editable appears in the gear-menu overflow. Flipping it on switches every declared column whose cell renderer isn't overridden into an inline editor — auto-dispatched from the column's Dataverse metadata — while a GridTemplateColumn's EditChildContent takes over from its read-mode ChildContent. Edits accumulate as pending row changes in the grid's transactional buffer and only land in Dataverse when the user saves.
Live Demo
A MainGrid over the contact table with AllowEdit="true". Click the gear icon on the toolbar overflow and flip Editable on. The Full Name column swaps its initials-avatar read template for a custom single-field editor that splits user input back into firstname + lastname. The bare Email, Phone, Age, and Account columns each light up with the editor matching their metadata type — TextEdit for the strings, NumberEdit for the integer, LookupEdit for the customer reference — without any per-column wiring.
React example
Blazor example
Anyone can view contacts in the grid below. Log in to create, view, and update your own contacts.
SaveRefresh
Page size
102050100
Editable
Full Name
Email
Phone
Age
Account
Metz, Barrows and Nicolas
Schmeler and Sons
Dooley - Nitzsche
Waelchi, Murazik and Considine
Bashirian - Weber
Adams - Torphy
Krajcik Group
Sanford - Littel
Kilback - Towne
Jacobson - Klein
React TypeScript
Razor
Enabling Editing
Set AllowEdit="true" on the grid. Editing is opt-in per grid — the toggle only appears when the parameter is set, and the toggle itself is only visible when at least one column is editable. While the toggle is off, the grid behaves as a read-only view; while it's on, cells render their inline editors and unsaved changes are tracked per row.
Inline edit changes the cell rendering for every visible row, which is a different read affordance than a static grid. The toggle lets users opt into the edit affordance when they need it (typing a value, tabbing across cells, etc.) and keep the cleaner read affordance the rest of the time. The grid remembers the toggle state for the session.
Automatic editor dispatch
Bare <GridColumn ColumnName="..." /> declarations with no ChildContent auto-dispatch the right inline editor while the grid is in edit mode. The framework reads the column's metadata and picks the editor subclass:
Supply ChildContent on a GridColumn to take over both the read-mode display AND the edit-mode editor for that column. Once ChildContent is set, the framework stops auto-dispatching the metadata-driven editor — render whichever editor you want inside the template and gate its visibility off the grid's Editable flag. Standard editors (TextEdit, NumberEdit, ColumnEdit) dropped into the template auto-wire to dirty-tracking and validation via the cascading row context.
React
{/* Custom display cell PLUS custom editor. React splits the two
halves into cellRenderer (read-only) and editRenderer (edit mode) —
use <GridTemplateColumn> so both renderers can coexist for the
same logical column. The dropped-in <TextEdit> self-registers for
dirty-tracking and validation via the cascading row context. */}
<GridTemplateColumn
displayName="Email"
dependsOn={['emailaddress1']}
sortBy="emailaddress1"
cellRenderer={({ record }) => {
const email = (record.properties?.emailaddress1 as { value?: string })?.value;
return <span>{email}</span>;
}}
editRenderer={() => (
<TextEdit columnName="emailaddress1" displayLabelWhenAvailable={false} />
)}
/>Blazor
<!-- Custom display cell PLUS custom editor. Once ChildContent is
supplied, the framework no longer auto-dispatches an inline
editor for this column, so render one yourself if you want
the cell editable. The dropped-in editor self-registers for
dirty-tracking and validation via the cascading row context. -->
<GridColumn ColumnName="emailaddress1">
<ChildContent>
@{
var email = context.PrimaryRecord
.GetValueOrDefault<StringValue>("emailaddress1")?.Value;
}
@if (this.Editable)
{
<TextEdit ColumnName="emailaddress1"
DisplayLabelWhenAvailable="false"
DisplayTooltipWhenAvailable="false" />
}
else
{
<span>@email</span>
}
</ChildContent>
</GridColumn>
Validation
Each row maintains its own EditContextValidator. Standard editors register with it on mount and surface their errors inline (red underline + tooltip on the cell). The grid's save button is disabled while any row has an unresolved validation error. Validation rules come from the column metadata:
RequiredLevel — the column is required, and an empty value blocks save.
Format on string columns — email, phone, URL formats enforce shape on top of the type.
MinValue / MaxValue on numeric columns — out-of-range values are flagged before save.
Custom validation — register additional rules per-form via the EditContextValidator API for cross-field invariants (e.g. "contact must have either email or phone").
Validation on custom widgets
Custom inputs rendered inside ChildContent / EditChildContent that don't extend BaseEdit (e.g. a raw FluentTextField or a third-party widget) don't auto-register with the row's validator. If you need them validated, either drop a standard editor instead of the raw widget, or run validation manually inside the widget's ValueChanged callback before calling editCtx.SetValue.
Save & Cancel
Edits accumulate as pending updates on the row — modified cells get a small "dirty" indicator, deleted rows are strikethrough, and pending-create rows show as new. The grid's toolbar surfaces Save and Cancel buttons whenever there are pending changes: Save commits the batch in one transactional pass; Cancel discards the buffer and reverts each row to its server state. Pair with NewRecordGridButton and DeleteRecordGridButton in the Buttons fragment to add row creation and deletion to the same edit transaction.
Pair a GridTemplateColumn's read-mode ChildContent with an EditChildContent to make a synthetic / composite column editable. The edit template receives a GridTemplateColumnEditContext — same row context as the read template (editCtx.Row.PrimaryRecord, the optional editCtx.Row.ParentRecord in a SubGrid, editCtx.DependsOnMetadata) plus an imperative editCtx.SetValue(columnName, value) helper.
EditChildContent fires only when the grid is editable (AllowEdit="true" AND the Editable toggle is on) and the row isn't pending-delete. Template columns that omit EditChildContent stay read-only even in edit mode — useful for actions columns that should always render the same buttons.
Pattern 1 — Drop-in standard editors
The most common case: the cell composes from multiple bound columns, and you want to let the user edit each one. Drop the matching <TextEdit> / <NumberEdit> / <ColumnEdit> editors inside EditChildContent — the framework cascades the row's primary record and matching EditContextValidator into that scope, so the editors self-register for dirty-tracking and validation. No extra wiring required.
React
<GridTemplateColumn
displayName="Full Name"
dependsOn={['firstname', 'lastname']}
sortBy="lastname"
cellRenderer={({ record }) => {
const first = (record.properties?.firstname as { value?: string })?.value ?? '';
const last = (record.properties?.lastname as { value?: string })?.value ?? '';
return <strong>{first} {last}</strong>;
}}
// Drop-in editors — TextEdit auto-wires to dirty-tracking
// and validation via the cascading row context.
editRenderer={() => (
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<TextEdit columnName="firstname" displayLabelWhenAvailable={false} />
<TextEdit columnName="lastname" displayLabelWhenAvailable={false} />
</div>
)}
/>Blazor
When the editor surface doesn't map 1:1 to a Dataverse column — a single "First Last" text box that parses into two columns, a slider tied to multiple percent fields, a custom date-range picker writing start + end — render your own input and call editCtx.SetValue(columnName, value) for each affected column. The framework routes the write to the correct AliasedTableRecord and fires ValueChanged so dirty-tracking picks the change up.
React
<GridTemplateColumn
displayName="Full Name"
dependsOn={['firstname', 'lastname']}
sortBy="lastname"
cellRenderer={({ record }) => {
const first = (record.properties?.firstname as { value?: string })?.value ?? '';
const last = (record.properties?.lastname as { value?: string })?.value ?? '';
return <strong>{first} {last}</strong>;
}}
editRenderer={({ record, setValue }) => {
const first = (record.properties?.firstname as { value?: string })?.value ?? '';
const last = (record.properties?.lastname as { value?: string })?.value ?? '';
const combined = [first, last].filter(Boolean).join(' ');
// Custom widget — split on space; consumer takes on
// validation since <Input> doesn't self-register.
return (
<Input
value={combined}
onChange={(_, data) => {
const next = data.value ?? '';
const idx = next.indexOf(' ');
const head = idx === -1 ? next : next.slice(0, idx);
const tail = idx === -1 ? '' : next.slice(idx + 1);
setValue('firstname', { $type: 'StringValue', value: head });
setValue('lastname', { $type: 'StringValue', value: tail || null });
}}
/>
);
}}
/>Blazor
<GridTemplateColumn Title="Full Name"
DependsOn="@(new[] { "firstname", "lastname" })"
SortBy="lastname">
<ChildContent Context="ctx">
@{
var first = ctx.Row.PrimaryRecord.GetValueOrDefault<StringValue>("firstname")?.Value;
var last = ctx.Row.PrimaryRecord.GetValueOrDefault<StringValue>("lastname")?.Value;
}
<strong>@first @last</strong>
</ChildContent>
<EditChildContent Context="editCtx">
@{
var combined =
(editCtx.Row.PrimaryRecord.GetValueOrDefault<StringValue>("firstname")?.Value ?? "")
+ " "
+ (editCtx.Row.PrimaryRecord.GetValueOrDefault<StringValue>("lastname")?.Value ?? "");
}
<!-- Custom widget — split on space; consumer takes on
validation since FluentTextField doesn't self-register. -->
<FluentTextField Value="@combined.Trim()" ValueChanged="@(next =>
{
var parts = next.Split(' ', 2);
editCtx.SetValue("firstname", new StringValue { Value = parts[0] });
editCtx.SetValue("lastname", new StringValue { Value = parts.Length > 1 ? parts[1] : null });
})" />
</EditChildContent>
</GridTemplateColumn>
Validation on the imperative path
Standard editors dropped into EditChildContent register with the row's EditContextValidator automatically and participate in the framework's validate-before-save gate. The editCtx.SetValue path bypasses that pipeline — values that didn't come from a registered editor aren't validated. If your custom widget needs to enforce constraints, run them inside the ValueChanged callback before calling SetValue, or fall back to the drop-in editor pattern.
Apply begins with filter on the following columns:
Account Email Phone
Apply begins with filter on the following columns: