Grid Buttons

Grid toolbar buttons control how users interact with records in a MainGrid or SubGrid. Buttons are placed inside the Buttons render fragment and automatically appear in the grid's toolbar.

Button Categories

There are three categories of grid buttons based on how they handle record operations:

  • Dialog Buttons — Open a form in a dialog to create or edit records inline without leaving the page.
  • Navigation Buttons — Navigate to a separate page URL for creating or editing records.
  • Action Buttons — Perform operations like deleting, linking, or unlinking records directly.

Choosing the right approach

Pick the button shape based on how much context the user needs and how much new state is involved. The three buckets below cover the typical cases.

  • Dialog (inline) — Use NewRecordGridButton / OpenRecordGridButton when the form fits a single dialog and the user benefits from staying on the current page (e.g. adding a contact to an account record without losing the account's other unsaved edits). Combine with Behavior="GridActionBehavior.WithGridContext" (the default) to fold the new record's save into the parent context's transactional commit.
  • Navigate (full page) — Use NavigateNewRecordGridButton / NavigateOpenRecordGridButton when the create or edit needs a richer surface than a dialog can comfortably offer: many fields, multiple tabs, related records of its own, attachments, or shareable URLs. The route owns its own RecordContext, so the parent grid doesn't need to be aware of the form's complexity.
  • Wizard (multi-step dialog) — Use NewRecordGridButton FormType="FormType.WizardForm" when the form has enough fields that one screen feels crowded, but the inputs are interdependent enough that splitting across pages is worth the navigation cost. Common shape: page 1 captures identity / classification, page 2 captures details that depend on page 1's choices.

Save flow & behavior modes

Every dialog-based grid button (NewRecordGridButton, OpenRecordGridButton, the M2M link / unlink pair) accepts a Behavior parameter that controls when the underlying Dataverse call fires. The two values map to materially different request sequences:

Behavior = Immediately

The dialog's Save button issues the create / update / associate / disassociate request straight to Dataverse via ExecuteMultipleAsync, refreshes the grid, then closes the dialog. The surrounding MainContext / RecordContext (if any) doesn't see the change — it's already committed.

  1. User clicks Save in the dialog.
  2. Dialog validates, then ships one or more OrganizationRequests through ExecuteMultipleAsync.
  3. Server returns; the grid refreshes; the dialog closes.

Pick this when: the dialog stands alone (no surrounding page-level Save button), or the user genuinely wants each row-level action to be its own commit. Each Save here is independent — partial completion of a multi-row workflow stays on the server.

Behavior = WithGridContext (default)

The dialog's Save button stages the request on the grid's pending queue (_rowsToCreate / _rowsToUpdate in Blazor, the React equivalent in useGridContext()) and closes the dialog. The actual Dataverse call doesn't fire until the surrounding MainContext / RecordContext's Save button is clicked.

  1. User clicks Save in the dialog → record (or update / associate / disassociate) is queued on the grid.
  2. Dialog closes. The page's parent context flips to IsDirty=true; the page-level Save button enables.
  3. User clicks the page-level Save → every queued grid change + the parent record's own update + every other descendant's pending requests get batched into a single ExecuteMultipleAsync.
  4. Server returns; the page refreshes; queues clear; IsDirty flips back.

Pick this when: the page has a parent RecordContext with its own Save button — the user expects "save the whole page" semantics, and a partial commit (parent saved, child rows not) would be a worse outcome than rolling back everything together. This is the default for that reason.

Default is WithGridContext

WithGridContext is the default for every dialog-based button — Immediately is opt-in. If your grid lives outside a parent context (a standalone listing page with no record above it), the queue has nowhere to drain to and the dialog auto-falls-back to Immediately semantics.

NewRecordGridButton

Opens a dialog form to create a new record. Requires a TForm type parameter specifying the Razor component to render as the form.

Use Location to control where the dialog appears: DialogLocation.Center (default) or DialogLocation.Right (side panel).

Use FormType to choose between a standard form (FormType.Form) or a multi-step wizard (FormType.WizardForm).

Use Behavior to control when the record is created: GridActionBehavior.Immediately saves to Dataverse right away, while GridActionBehavior.WithGridContext (default) defers the save until the parent context is committed.

<!-- Center dialog with standard form -->
<NewRecordGridButton TForm="NewContactForm" />

<!-- Side panel with standard form -->
<NewRecordGridButton TForm="NewContactForm"
                     Location="DialogLocation.Right" />

<!-- Center dialog with wizard form -->
<NewRecordGridButton TForm="NewContactForm"
                     Location="DialogLocation.Center"
                     FormType="FormType.WizardForm" />

<!-- Save immediately instead of deferring -->
<NewRecordGridButton TForm="NewContactForm"
                     Behavior="GridActionBehavior.Immediately" />

Standard Form Example

The TForm type parameter specifies a Razor component that defines the form layout. A standard form is simply a Razor component containing editor components. It can include tabs, sections, or any layout you need.

<!-- EditContactForm.razor -->
<FluentTabs Style="width: 100%">
    <FluentTab Label="General">
        <TextEdit ColumnName="firstname" />
        <TextEdit ColumnName="middlename" />
        <TextEdit ColumnName="lastname" />
    </FluentTab>
    <FluentTab Label="Other">
        <MoneyEdit ColumnName="annualincome" />
        <TextEdit ColumnName="telephone1"
                  TextFieldType="TextFieldType.Tel" />
    </FluentTab>
</FluentTabs>

Wizard Form Example

A wizard form splits the creation process into multiple steps. Define each step using WizardRecordPage components. Use FormType="FormType.WizardForm" on the button to enable wizard mode.

<!-- NewContactForm.razor -->
<WizardRecordPage>
    <TextEdit ColumnName="firstname" />
    <TextEdit ColumnName="middlename" />
    <TextEdit ColumnName="lastname" />
</WizardRecordPage>
<WizardRecordPage>
    <MoneyEdit ColumnName="annualincome" />
    <TextEdit ColumnName="telephone1"
              TextFieldType="TextFieldType.Tel" />
</WizardRecordPage>

Note

When using a wizard form, set FormType="FormType.WizardForm" on the NewRecordGridButton. The wizard displays Back/Next navigation and validates each page before advancing.

Per-page validation

Each WizardRecordPage defaults to ForceSuccessfulValidationBeforeSave="true": the wizard's Next button runs the active page's validators before advancing and cancels the transition if any required field is empty / invalid. Set false on pages that only carry optional fields so the user can skip them. The final-page Finish button always validates regardless of this flag — server-side rejection of the create is the only way past it.

<WizardRecordPage>
    <!-- Required fields here — user can't advance until they're filled -->
    <TextEdit ColumnName="firstname" />
    <TextEdit ColumnName="lastname" />
</WizardRecordPage>
<WizardRecordPage ForceSuccessfulValidationBeforeSave="false">
    <!-- Optional fields — user can advance even with validation errors -->
    <MoneyEdit ColumnName="annualincome" />
</WizardRecordPage>

Shared record across pages

All WizardRecordPage components inside one NewRecordGridButton share the same underlying TableRecord object — editing firstname on page 1 and annualincome on page 2 ends up in a single create payload at the end. Per-page RecordContexts bind to the same record instance, so navigation between steps preserves in-progress edits.

OpenRecordGridButton

Opens a dialog form to edit the selected record(s). Like NewRecordGridButton, it requires a TForm type parameter. When multiple records are selected, the form shows shared fields and applies changes to all selected records.

The edit button is automatically triggered when a row is double-clicked in the grid. Set AllowNavigateOnRowDoubleClick="false" on the grid to suppress the double-click handler.

By default the table's primary-name column also renders as a hyperlink in every row, and clicking the link triggers the same edit action as the double-click. Set AllowNavigateOnPrimaryNameClick="false" on the grid to suppress the hyperlink and render the primary-name cell as plain text. The hyperlink only appears when an OpenRecordGridButton or NavigateOpenRecordGridButton is registered, so grids without an edit button are unaffected.

<!-- Center dialog (default) -->
<OpenRecordGridButton TForm="EditContactForm" />

<!-- Side panel -->
<OpenRecordGridButton TForm="EditContactForm"
                      Location="DialogLocation.Right" />

<!-- Save immediately -->
<OpenRecordGridButton TForm="EditContactForm"
                      Behavior="GridActionBehavior.Immediately" />

<!-- Suppress the primary-name hyperlink (row double-click still triggers edit) -->
<MainGrid TableName="contact"
          AllowNavigateOnPrimaryNameClick="false">
    <Buttons>
        <OpenRecordGridButton TForm="EditContactForm" />
    </Buttons>
</MainGrid>

<!-- Suppress the row double-click handler (primary-name hyperlink still works) -->
<MainGrid TableName="contact"
          AllowNavigateOnRowDoubleClick="false">
    <Buttons>
        <OpenRecordGridButton TForm="EditContactForm" />
    </Buttons>
</MainGrid>

NavigateNewRecordGridButton

Navigates to a URL to create a new record. Set the Url parameter to the target page. When used in a SubGrid, the parent record's relationship context is automatically appended as query string parameters.

Use the OnClick callback to dynamically set the URL based on the grid context. This is useful for multi-table grids where the URL depends on the selected view's table.

<!-- Static URL -->
<NavigateNewRecordGridButton Url="/contacts/new" />

<!-- Dynamic URL based on selected view -->
<NavigateNewRecordGridButton OnClick="OnNewClick" />

@code {
    private async Task OnNewClick(NavigateGridButtonContext ctx)
    {
        ctx.Url = ctx.GridContext.SelectedView.TableName switch
        {
            "contact" => "/contacts/new",
            "account" => "/accounts/new",
            _ => throw new Exception("Unknown table"),
        };
    }
}

NavigateOpenRecordGridButton

Navigates to a URL to edit the selected record. The Url parameter supports {0} as a placeholder for the selected record's ID.

<!-- {0} is replaced with the selected record's ID -->
<NavigateOpenRecordGridButton Url="/contacts/edit?contactId={0}" />

<!-- Dynamic URL -->
<NavigateOpenRecordGridButton OnClick="OnEditClick" />

@code {
    private async Task OnEditClick(NavigateGridButtonContext ctx)
    {
        ctx.Url = ctx.GridContext.SelectedView.TableName switch
        {
            "contact" => "/contacts/edit?contactId={0}",
            "account" => "/accounts/edit?accountId={0}",
            _ => throw new Exception("Unknown table"),
        };
    }
}

NavigateRecordGridButton

A general-purpose navigation button with a custom label, icon, and URL. Use this for custom navigation actions that don't fit the new/edit pattern.

<NavigateRecordGridButton Title="View Details"
                          Url="/records/details?id={0}"
                          ButtonEnabledBehavior="GridButtonBehavior.WhenOneSelected" />

DeleteRecordGridButton

Deletes the selected records after prompting for confirmation. Use Mode to control whether records are deleted BulkOperationMode.Individually (one by one with progress) or in a single batch.

<!-- Delete one by one with progress -->
<DeleteRecordGridButton Mode="BulkOperationMode.Individually" />

<!-- Delete in a single batch -->
<DeleteRecordGridButton Mode="BulkOperationMode.Batch" />

<!-- Delete immediately without deferring -->
<DeleteRecordGridButton Behavior="GridActionBehavior.Immediately" />

LinkExistingRecordGridButton

Opens a lookup dialog to find and associate existing records via a many-to-many relationship. Only applicable in SubGrid with N:N relationships.

<LinkExistingRecordGridButton />

<!-- Associate immediately -->
<LinkExistingRecordGridButton Behavior="GridActionBehavior.Immediately" />

UnlinkExistingRecordGridButton

Disassociates the selected records from a many-to-many relationship after prompting for confirmation. Only applicable in SubGrid with N:N relationships.

<UnlinkExistingRecordGridButton />

<!-- Disassociate immediately -->
<UnlinkExistingRecordGridButton Behavior="GridActionBehavior.Immediately" />

GridButton

A fully custom button with an OnClick callback that receives the current GridContext. Use this to implement custom toolbar actions.

<SubGrid RelationshipName="contact_customer_accounts">
    <Buttons>
        <NewRecordGridButton TForm="NewContactForm" />
        <OpenRecordGridButton TForm="EditContactForm" />
        <DeleteRecordGridButton />

        <!-- Custom button -->
        <GridButton Label="Export"
                    Icon="@(new Icons.Regular.Size20.ArrowDownload())"
                    IsButtonEnabled="DefaultGridButtonBehavior.GetBehavior(GridButtonBehavior.WhenOneOrMoreSelected)"
                    OnClick="OnExportClick" />
    </Buttons>
</SubGrid>

@code {
    private async Task OnExportClick(GridContext context)
    {
        var selectedRecords = context.SelectedRecords;
        // Custom logic — export, print, send email, etc.
    }
}

Common Parameters

  • Behavior — Controls whether the operation is executed immediately (GridActionBehavior.Immediately) or deferred until the parent context saves (GridActionBehavior.WithGridContext).
  • Mode — For delete/link/unlink buttons, controls whether bulk operations run individually with progress feedback or in a single batch request.
  • IsButtonEnabled / IsButtonVisible — Predicates that control button state based on the current row selection.
Blazor

GridButton Class

Parameters

Name
Type
Default
Description
Enabledbool?
True
Overrides the enabled state of the button regardless of the GridButton.IsButtonEnabled predicate result.
IconIcon?
An optional icon displayed on the button.
IsButtonEnabledFunc<IEnumerable<GridRowContext>, bool>
A predicate evaluated against the current row selection to determine whether the button is enabled.
IsButtonVisibleFunc<IEnumerable<GridRowContext>, bool>
A predicate evaluated against the current row selection to determine whether the button is visible.
IsOpenRecordButtonbool
False
Specifies that this is the 'Edit' button for the grid. Only one button should have this property set to 'true' in a grid. This button's event handler is called when a row is 'double-clicked' in the grid.
Labelstring?
The text label displayed on the button.
Tooltipstring?
An optional tooltip shown when the user hovers over the button.
Name: Enabled
Type: bool?
Default: True
Description: Overrides the enabled state of the button regardless of the GridButton.IsButtonEnabled predicate result.
Name: Icon
Type: Icon?
Description: An optional icon displayed on the button.
Name: IsButtonEnabled
Type: Func<IEnumerable<GridRowContext>, bool>
Description: A predicate evaluated against the current row selection to determine whether the button is enabled.
Name: IsButtonVisible
Type: Func<IEnumerable<GridRowContext>, bool>
Description: A predicate evaluated against the current row selection to determine whether the button is visible.
Name: IsOpenRecordButton
Type: bool
Default: False
Description: Specifies that this is the 'Edit' button for the grid. Only one button should have this property set to 'true' in a grid. This button's event handler is called when a row is 'double-clicked' in the grid.
Name: Label
Type: string?
Description: The text label displayed on the button.
Name: Tooltip
Type: string?
Description: An optional tooltip shown when the user hovers over the button.

Events

Name
Type
Description
OnClickEventCallback<GridContext>
Fires when the button is clicked, providing the current Components.GridContext to the handler.
Name: OnClick
Type: EventCallback<GridContext>
Description: Fires when the button is clicked, providing the current Components.GridContext to the handler.
React