Best Practices for Tabs with On-Demand Data, Unsaved Changes Tracking on tab navigation, and preserve grid references across tabs in Syncfusion Blazor

Image_8207_1756957267677

Requirements:

- Each tab contains editable items, for eg: a grid and a text area.

- User can edit grid cells (single-click edit), add rows, delete rows and edit the text area.

- On clicking the Save button in a tab, all changes for that tab (grid + text area) should be saved to the database, and the tab refreshed.

- Data for each tab should be loaded on demand (not all at once).

- If a user edits a tab (grid or text area) and tries to switch tabs, they should be warned about unsaved changes and given a choice to save or discard.

- Single-click editing should work for grids in all tabs (not just the first tab).

- I want to follow best practices for grid references, unsaved changes tracking, and tab data loading.


Current Issues:

- My single-click editing only works on the first tab; on other tabs, the grid reference is null so it doesn't work. 

- I’m not sure how to robustly track unsaved changes for both the grid and the text area in each tab, and how to prompt the user on tab switch.

- Is my approach for loading tab data on demand and managing grid references optimal? If not, what is the recommended pattern?


Minimal Repro (Runnable):

https://blazorplayground.syncfusion.com/rNVeZuZTMVHWLrRJ

Please help me implement my requirements in the most elegant, best practice way.


Thank you for your guidance!


2 Replies

AK Ashish Khanal September 4, 2025 03:45 AM UTC

Repro code:


<!--------------------------------------------------------__Index.razor------------------------------------------------------------------->

<SfButton OnClick="() => showTestDialog = true" CssClass="e-primary">Test Dialog Tabs</SfButton>

<TestDialogTabs IsOpen="@showTestDialog" OnClose="OnTestDialogClosed" />

@code {
    private bool showTestDialog = false;

    private Task OnTestDialogClosed()
    {
        showTestDialog = false;
        return Task.CompletedTask;
    }
}

<!--------------------------------------------------TestDialogTabs.razor------------------------------------------------------------->
@using Syncfusion.Blazor.Navigations
@using Syncfusion.Blazor.Popups
@using Syncfusion.Blazor.Grids
@using Syncfusion.Blazor.Buttons
@using Syncfusion.Blazor.Inputs

<div id="test-dialog-root">
    <SfDialog @bind-Visible="IsOpen"
              Width="80%"
              Height="500"
              IsModal="true"
              ShowCloseIcon="true"
              Target="#test-dialog-root">
        <DialogTemplates>
            <Header>Tabbed Dialog Repro</Header>
            <Content>
                @if (TabHeaders.Count > 0 && SelectedTabIndex >= 0)
                {
                    <SfTab @bind-SelectedItem="SelectedTabIndex">
                        <TabEvents Selected="OnTabSelected"></TabEvents>
                        <TabItems>
                            @foreach (var tab in TabHeaders)
                            {
                                <TabItem ID="@tab.TabKey">
                                    <ChildContent>
                                        <TabHeader Text="@tab.TabName"></TabHeader>
                                    </ChildContent>
                                    <ContentTemplate>
                                        <div class="tab-content">
                                            @if (SelectedTab is not null)
                                            {
                                                <SfGrid DataSource="@SelectedTab.GridData" @ref="@SelectedTab.GridRef">
                                                    <GridEditSettings AllowEditing="true"
                                                                      AllowAdding="true"
                                                                      AllowDeleting="true"
                                                                      Mode="Syncfusion.Blazor.Grids.EditMode.Batch"
                                                                      ShowConfirmDialog="false" />
                                                    <GridSelectionSettings Mode="Syncfusion.Blazor.Grids.SelectionMode.Both" />
                                                    <GridEvents CellSelected="CellSelectHandler" TValue="TestGridRow"></GridEvents>
                                                    <GridColumns>
                                                        <GridColumn Field=@nameof(TestGridRow.Id) IsPrimaryKey="true" Visible="false" />
                                                        <GridColumn Field=@nameof(TestGridRow.Name) HeaderText="Name" Width="200" />
                                                        <GridColumn Field=@nameof(TestGridRow.Value) HeaderText="Value" EditType="EditType.NumericEdit" Width="100" />
                                                    </GridColumns>
                                                </SfGrid>
                                                <SfTextArea @bind-Value="SelectedTab.Notes" Placeholder="Notes..." Rows="3" />
                                                <SfButton OnClick="() => SaveTabAsync(tab.TabKey)">Save</SfButton>
                                            }
                                            else
                                            {
                                                <div style="min-height:150px;display:flex;align-items:center;justify-content:center;">
                                                    Loading...
                                                </div>
                                            }
                                        </div>
                                    </ContentTemplate>
                                </TabItem>
                            }
                        </TabItems>
                    </SfTab>
                }
            </Content>
        </DialogTemplates>
        <DialogButtons>
            <DialogButton Content="Close" CssClass="e-flat e-outline" OnClick="@(async () => await OnClose.InvokeAsync())"></DialogButton>
        </DialogButtons>
    </SfDialog>
</div>

@code {
    [Parameter] public bool IsOpen { get; set; }
    [Parameter] public EventCallback OnClose { get; set; }

    private int SelectedTabIndex = -1;
    private List<TabHeader> TabHeaders = new();
    private TestTab? SelectedTab;

    protected override async Task OnParametersSetAsync()
    {
        if (!IsOpen) return;

        TabHeaders = await GetTabHeadersAsync();
        SelectedTabIndex = TabHeaders.FindIndex(t => t.IsActive);
        if (SelectedTabIndex < 0) SelectedTabIndex = 0;

        await LoadTabContentAsync(SelectedTabIndex);
    }

    private async Task<List<TabHeader>> GetTabHeadersAsync()
    {
        await Task.Delay(100); // Simulate async data fetch
        return new List<TabHeader>
        {
            new TabHeader { TabKey = "tab1", TabName = "Tab 1", IsActive = false },
            new TabHeader { TabKey = "tab2", TabName = "Tab 2", IsActive = true },
            new TabHeader { TabKey = "tab3", TabName = "Tab 3", IsActive = false }
        };
    }

    public async Task OnTabSelected(SelectEventArgs args)
    {
        // TODO: Check for dirty state and prompt user before switching
        await LoadTabContentAsync(args.SelectedIndex);
    }

    private async Task LoadTabContentAsync(int tabIndex)
    {
        var tabHeader = TabHeaders[tabIndex];
        SelectedTab = new TestTab
        {
            TabKey = tabHeader.TabKey,
            LastUpdatedBy = "User" + (tabIndex + 1),
            LastUpdatedDate = DateTime.Now.AddMinutes(-tabIndex * 10),
            GridData = Enumerable.Range(1, 3).Select(i => new TestGridRow
            {
                Id = i,
                Name = $"Item {i} (Tab {tabIndex + 1})",
                Value = 10 * (tabIndex + 1) + i
            }).ToList(),
            Notes = ""
        };
        await Task.CompletedTask;
    }

    // Single-click edit handler
    public async Task CellSelectHandler(CellSelectEventArgs<TestGridRow> args)
    {
        if (SelectedTab?.GridRef is null) return;
        var cellIndexes = await SelectedTab.GridRef.GetSelectedRowCellIndexesAsync();
        var rowIndex = cellIndexes[0].Item1;
        var colIndex = (int)cellIndexes[0].Item2;
        var fields = await SelectedTab.GridRef.GetColumnFieldNamesAsync();
        await SelectedTab.GridRef.EditCellAsync(rowIndex, fields[colIndex]);
    }

    // Save logic (simulate DB save)
    private async Task SaveTabAsync(string tabKey)
    {
        // Save SelectedTab.GridData and SelectedTab.Notes to DB
        // After save, reload tab data
        await LoadTabContentAsync(SelectedTabIndex);
    }

    public class TabHeader
    {
        public string TabKey { get; set; } = default!;
        public string TabName { get; set; } = default!;
        public bool IsActive { get; set; }
    }

    public class TestTab
    {
        public string TabKey { get; set; } = default!;
        public string LastUpdatedBy { get; set; } = default!;
        public DateTime LastUpdatedDate { get; set; }
        public List<TestGridRow> GridData { get; set; } = new();
        public SfGrid<TestGridRow>? GridRef { get; set; }
        public string Notes { get; set; } = "";
    }

    public class TestGridRow
    {
        public int Id { get; set; }
        public string Name { get; set; } = string.Empty;
        public int Value { get; set; }
    }
}


SS Saritha Sankar Syncfusion Team September 5, 2025 03:42 AM UTC

Hi Ashish Khanal,


A ticket has already been created for this query. Please track the ticket below for further details.

Ticket Link: [Ticket-764568] Best Practices for Tabs with On-Demand Data, Unsaved Changes Tracking on tab navigation, and preserve grid references across tabs in Syncfusion Blazor | Syncfusion

Regards,
Saritha S.


Loader.
Up arrow icon