Skip to content

refactor(Table): updating column state persistence#7954

Merged
ArgoZhang merged 3 commits into
mainfrom
refactor-table-order
May 5, 2026
Merged

refactor(Table): updating column state persistence#7954
ArgoZhang merged 3 commits into
mainfrom
refactor-table-order

Conversation

@ArgoZhang
Copy link
Copy Markdown
Member

@ArgoZhang ArgoZhang commented May 5, 2026

Link issues

fixes #7953

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

Summary by Sourcery

Refine table column persistence, sizing, and drag behavior, and streamline how visible columns are built and rendered.

Bug Fixes:

  • Prevent all table columns from becoming invisible by forcing at least the first column to remain visible when toggling visibility.

Enhancements:

  • Load client-side column state earlier in the lifecycle and rebuild visible columns from cache or current configuration as appropriate.
  • Simplify and centralize visible-column list rendering and ordering, removing the explicit Order field from ColumnVisibleItem and using list position instead.
  • Ensure drag-and-drop column reordering and column resizing update the persisted client-side state only when client persistence is enabled, and always trigger a table re-render.
  • Standardize width calculations in the table JavaScript helpers using getWidth to avoid direct offsetWidth access and bitwise casts, improving consistency and robustness.
  • Rename and clarify localStorage helpers for saving visible column lists and refine auto-fit and tooltip behavior based on computed widths.

Copilot AI review requested due to automatic review settings May 5, 2026 07:31
@bb-auto bb-auto Bot added the enhancement New feature or request label May 5, 2026
@bb-auto bb-auto Bot added this to the v10.6.0 milestone May 5, 2026
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented May 5, 2026

Reviewer's Guide

Refactors the Blazor Table component’s column visibility/width persistence and drag/resize behavior, centralizing column list rendering, simplifying visibility state, and making the JS width handling more robust and consistent while wiring it tightly to client-side local storage.

Sequence diagram for column drag and visibility persistence

sequenceDiagram
    actor User
    participant Browser as Browser_Table_UI
    participant JS as Table_razor_js
    participant DotNet as Blazor_Table_component
    participant LS as LocalStorage

    User->>Browser: Drag column header
    Browser->>JS: dragstart / dragover / drop events
    JS->>JS: setDraggable registers drop handler
    JS->>DotNet: invokeMethodAsync(dragColumnCallback, originIndex, currentIndex)

    DotNet->>DotNet: DragColumnCallback(originIndex, currentIndex)
    DotNet->>DotNet: Reorder _columnVisibleItems
    alt ClientTableName is set
        DotNet->>DotNet: Update _tableColumnStateCache.ColumnVisibleStates
    end
    DotNet->>DotNet: _resetColumns = true, _invoke = true
    DotNet->>DotNet: StateHasChanged()

    Note over DotNet,JS: Table rerenders with new column order

    JS->>JS: resetColumns(table, options)
    alt tableName and visibleColumns provided
        JS->>LS: saveColumnVisibleList(tableName, visibleColumns)
    end
Loading

Sequence diagram for column resize and width persistence

sequenceDiagram
    actor User
    participant Browser as Browser_Table_UI
    participant JS as Table_razor_js
    participant DotNet as Blazor_Table_component
    participant LS as LocalStorage

    User->>Browser: Drag column resize handle
    Browser->>JS: mousedown / mousemove / mouseup
    JS->>JS: setResizeListener / setColumnResizingListen
    JS->>JS: getWidth(th), getWidth(table)

    JS->>DotNet: invokeMethodAsync(resizeColumnCallback, fieldOrIndex, columnWidth, tableWidth)

    DotNet->>DotNet: ResizeColumnCallback(name, columnWidth, tableWidth)
    alt ClientTableName is set
        DotNet->>DotNet: Find ColumnWidthItem by Name
        DotNet->>DotNet: Update Width and TableWidth in _tableColumnStateCache.ColumnWidthState
    end
    DotNet->>DotNet: Invoke resize callbacks (if any)

    JS->>JS: saveColumnWidth(table)
    JS->>LS: setItem(bb-table-column-width-*, widthState)
Loading

Class diagram for updated Table column state management

classDiagram
    class Table~TItem~ {
        -string ClientTableName
        -TableColumnLocalstorageStatus _tableColumnStateCache
        -List~ColumnVisibleItem~ _columnVisibleItems
        +Task OnAfterRenderAsync(bool firstRender)
        -Task ReloadColumnStatesFromBrowserAsync()
        -void BuildTableColumns()
        -void RebuildTableColumnFromCache(List~ITableColumn~ cols)
        -List~ColumnVisibleItem~ GetColumnVisibleItems(List~ITableColumn~ cols)
        -Task OnTableRenderAsync(bool firstRender)
        +Task DragColumnCallback(int originIndex, int currentIndex)
        +Task ResizeColumnCallback(string name, int columnWidth, int tableWidth)
        -RenderFragment RenderColumnList()
        -void RebuildVisibleColumnsCache()
    }

    class ColumnVisibleItem {
        +string Name
        +string DisplayName
        +bool Visible
    }

    class TableColumnLocalstorageStatus {
        +ColumnWidthState ColumnWidthState
        +List~ColumnVisibleItem~ ColumnVisibleStates
    }

    class ColumnWidthState {
        +List~ColumnWidthItem~ ColumnWidths
        +int TableWidth
    }

    class ColumnWidthItem {
        +string Name
        +int Width
    }

    class ITableColumn {
        <<interface>>
        +string GetFieldName()
        +string GetDisplayName()
        +bool GetIgnore()
        +bool GetVisible()
        +bool? Visible
        +int? Width
    }

    Table~TItem~ --> TableColumnLocalstorageStatus : uses
    TableColumnLocalstorageStatus --> ColumnWidthState : has
    ColumnWidthState --> ColumnWidthItem : contains
    Table~TItem~ --> ColumnVisibleItem : maintains
    Table~TItem~ --> ITableColumn : builds from
Loading

File-Level Changes

Change Details Files
Refine column persistence loading and rebuilding logic to better use cached state and simplify visible column item generation.
  • Move ReloadColumnStatesFromBrowserAsync earlier in lifecycle and ensure column cache initialization with non-null width/visible collections.
  • Update RebuildTableColumnFromCache to only apply cache when client persistence is enabled, avoid unnecessary loops, and set column widths/visibility directly from cached state.
  • Replace in-place mutation of _columnVisibleItems with regeneration via a new GetColumnVisibleItems helper that either uses cached visible states or derives them from current columns when no cache exists.
src/BootstrapBlazor/Components/Table/Table.razor.cs
Improve drag/resize callbacks to keep client-side and cached column state consistent and ensure table re-rendering when structure changes.
  • On column drag end, synchronize _tableColumnStateCache.ColumnVisibleStates with current _columnVisibleItems, then mark columns for reset and trigger StateHasChanged.
  • Guard ResizeColumnCallback’s cache updates behind a ClientTableName check and update both column width and overall table width only when a cached entry is present.
src/BootstrapBlazor/Components/Table/Table.razor.cs
Centralize column list rendering in a reusable RenderFragment and use it from the Razor view to avoid duplicated foreach logic.
  • Introduce RenderColumnList RenderFragment that loops over _columnVisibleItems and renders each via RenderColumnListItem.
  • Replace duplicated foreach blocks in Table.razor’s column-list UI with calls to @RenderColumnList().
src/BootstrapBlazor/Components/Table/Table.razor
src/BootstrapBlazor/Components/Table/Table.razor.cs
Clean up ColumnVisibleItem model and visible columns cache ordering logic now that ordering is driven by list position.
  • Remove the Order property from ColumnVisibleItem since ordering is implied by list index.
  • Update RebuildVisibleColumnsCache to iterate _columnVisibleItems in list order instead of sorting by Order.
src/BootstrapBlazor/Components/Table/ColumnVisibleItem.cs
src/BootstrapBlazor/Components/Table/Table.razor.Toolbar.cs
Make table JavaScript width calculations more robust and consistently use a shared getWidth helper instead of raw offsetWidth and bitwise coercion.
  • Use getWidth in multiple places (resetTableWidth, resize listener, resizing of fixed columns, tooltip width display, cell width calculation, column width state capture) instead of accessing offsetWidth and bitwise OR with 0.
  • Ensure tableWidth and column widths are read via getWidth on the correct elements (th, table container, columns) to avoid NaN and inconsistent sizing.
src/BootstrapBlazor/Components/Table/Table.razor.js
Refine JS event handling for drag and auto-fit behaviors and align localStorage keys with visibility semantics.
  • Adjust drop handler in setDraggable to compute and pass explicit original/current indices to the dragColumnCallback .NET method.
  • In autoFitColumnWidth, remove integer bitwise coercion and pass maxWidth and widthState directly to resizeColumnCallback.
  • Rename saveColumnList to saveColumnVisibleList and update callers, clarifying the purpose of persisted data.
src/BootstrapBlazor/Components/Table/Table.razor.js
Minor Razor and import adjustments to table rendering and visibility toggling behavior.
  • Move BuildTableColumns and RebuildVisibleColumnsCache calls out of an inline Razor code block into the main else branch before rendering to ensure they run consistently.
  • Relocate the Virtualization using from Table.razor to the shared _Imports.razor so it’s available project-wide.
  • Add a safeguard in TriggerSelectInvertColumnList to ensure at least one column remains visible after toggling inverted selection.
src/BootstrapBlazor/Components/Table/Table.razor
src/BootstrapBlazor/_Imports.razor
src/BootstrapBlazor/Components/Table/Table.razor.Checkbox.cs

Assessment against linked issues

Issue Objective Addressed Explanation
#7953 Refactor and redesign the Table component’s JavaScript logic (column width calculation, resize, drag, and persistence) to improve behavior and consistency.
#7953 Update the Table component’s C# and Razor code to integrate with the redesigned JavaScript (state caching, column visibility ordering, callbacks, and rendering).

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ArgoZhang ArgoZhang changed the title refactor(Table): redesign table javascript refactor(Table): updating column state persistence May 5, 2026
@ArgoZhang ArgoZhang merged commit 309b2a4 into main May 5, 2026
5 of 6 checks passed
@ArgoZhang ArgoZhang deleted the refactor-table-order branch May 5, 2026 07:34
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In RebuildTableColumnFromCache/GetColumnVisibleItems, _tableColumnStateCache is accessed without a null check; if ClientTableName is empty or no state is loaded from local storage, this can throw, so consider initializing the cache to a non-null default or guarding those accesses.
  • In resetTableWidth (Table.razor.js), the index constant computed inside the group.children loop is never used and can be removed to avoid unnecessary work.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `RebuildTableColumnFromCache`/`GetColumnVisibleItems`, `_tableColumnStateCache` is accessed without a null check; if `ClientTableName` is empty or no state is loaded from local storage, this can throw, so consider initializing the cache to a non-null default or guarding those accesses.
- In `resetTableWidth` (Table.razor.js), the `index` constant computed inside the `group.children` loop is never used and can be removed to avoid unnecessary work.

## Individual Comments

### Comment 1
<location path="src/BootstrapBlazor/Components/Table/Table.razor.js" line_range="684" />
<code_context>

         setTableDefaultWidth(table);
         const widthState = getColumnWidthStateObject(table);
-        await table.invoke.invokeMethodAsync(table.options.resizeColumnCallback, index, maxWidth | 0, widthState)
+        await table.invoke.invokeMethodAsync(table.options.resizeColumnCallback, index, maxWidth, widthState)

         resetColumnWidthTips(table, col);
</code_context>
<issue_to_address>
**issue (bug_risk):** Using the column index instead of the field name for `resizeColumnCallback` may not match the C# callback signature.

The C# callback is `ResizeColumnCallback(string name, int columnWidth, int tableWidth)`, but here the first arg is the column index and the third is a width state object. If this is meant to call the same .NET handler used in `setResizeListener`, these mismatched argument types/semantics could cause incorrect column resolution or binding issues. Please either align the JS arguments with the C# signature (field name + numeric table width) or call a separate .NET handler that accepts index + width state.
</issue_to_address>

### Comment 2
<location path="src/BootstrapBlazor/Components/Table/Table.razor.js" line_range="423-425" />
<code_context>
             let width = 0;
             [...group.children].forEach(col => {
                 let colWidth = parseInt(col.style.width);
+                const index = [...group.children].indexOf(col);
                 if (isNaN(colWidth)) {
-                    colWidth = 100;
</code_context>
<issue_to_address>
**nitpick (performance):** Unused `index` variable and repeated array materialization in `resetTableWidth`.

`index` is unused and `[...group.children]` is created twice here (for `forEach` and `indexOf`), which is unnecessary work for large tables. Either drop `index` or materialize `children` once and reuse it (e.g., `const children = [...group.children]; children.forEach((col, index) => { ... });`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.


setTableDefaultWidth(table);
const widthState = getColumnWidthStateObject(table);
await table.invoke.invokeMethodAsync(table.options.resizeColumnCallback, index, maxWidth | 0, widthState)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Using the column index instead of the field name for resizeColumnCallback may not match the C# callback signature.

The C# callback is ResizeColumnCallback(string name, int columnWidth, int tableWidth), but here the first arg is the column index and the third is a width state object. If this is meant to call the same .NET handler used in setResizeListener, these mismatched argument types/semantics could cause incorrect column resolution or binding issues. Please either align the JS arguments with the C# signature (field name + numeric table width) or call a separate .NET handler that accepts index + width state.

Comment on lines 423 to +425
[...group.children].forEach(col => {
let colWidth = parseInt(col.style.width);
const index = [...group.children].indexOf(col);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (performance): Unused index variable and repeated array materialization in resetTableWidth.

index is unused and [...group.children] is created twice here (for forEach and indexOf), which is unnecessary work for large tables. Either drop index or materialize children once and reuse it (e.g., const children = [...group.children]; children.forEach((col, index) => { ... });).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the Table component’s client-side behavior and column-state persistence, aiming to “redesign table javascript” (issue #7953) while also simplifying column visibility ordering by removing the ColumnVisibleItem.Order concept.

Changes:

  • Refactors table column persistence (visibility/width) handling between .NET and JavaScript, including renaming the JS localStorage helper for visible columns.
  • Updates column list rendering and visible-column cache rebuilding to rely on _columnVisibleItems ordering directly (removing the Order field).
  • Adjusts resize/drag JavaScript logic to use getWidth(...) rather than offsetWidth | 0 in multiple places.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/BootstrapBlazor/Components/Table/Table.razor.Toolbar.cs Updates visible-columns cache rebuild to use _columnVisibleItems order directly.
src/BootstrapBlazor/Components/Table/Table.razor.js Refactors resize/drag/width persistence logic; renames visible-column persistence helper.
src/BootstrapBlazor/Components/Table/Table.razor.cs Refactors column-state reload and rebuild logic; changes how _columnVisibleItems is rebuilt from cache.
src/BootstrapBlazor/Components/Table/Table.razor.Checkbox.cs Adds safeguard intent comment for “invert selection” column visibility behavior.
src/BootstrapBlazor/Components/Table/Table.razor Moves BuildTableColumns() / RebuildVisibleColumnsCache() to an earlier render location; uses a RenderFragment for column list rendering.
src/BootstrapBlazor/Components/Table/ColumnVisibleItem.cs Removes Order property from persisted column visibility model.
src/BootstrapBlazor/_Imports.razor Moves Virtualization using directive to shared imports.
Comments suppressed due to low confidence (1)

src/BootstrapBlazor/Components/Table/Table.razor.Checkbox.cs:203

  • TriggerSelectInvertColumnList can throw when _columnVisibleItems is empty: All(...) will return true for an empty sequence, then _columnVisibleItems.First() throws. Add a guard (e.g., check Count > 0) before forcing the first column visible.
        // 如果全部列都不可见了,则至少显示第一列
        if (_columnVisibleItems.All(i => i.Visible == false))
        {
            _columnVisibleItems.First().Visible = true;
        }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 422 to 428
let width = 0;
[...group.children].forEach(col => {
let colWidth = parseInt(col.style.width);
const index = [...group.children].indexOf(col);
if (isNaN(colWidth)) {
colWidth = 100;
colWidth = getWidth(col);
}
Comment on lines 573 to 577
const field = col.getAttribute('data-bb-field');
const th = col.closest('th')
const width = getWidth(th) | 0;
const tableWidth = table.tables[0].offsetWidth | 0;
const width = getWidth(th);
const tableWidth = getWidth(table.tables[0]);
table.invoke.invokeMethodAsync(table.options.resizeColumnCallback, field, width, tableWidth);
Comment on lines 930 to 936
const getColumnWidthStateObject = table => {
const cols = table.columns
const tableWidth = table.tables[0].offsetWidth | 0;
const tableWidth = getWidth(table.tables[0]);
return {
"cols": cols.map(col => {
return { "width": col.closest('th').offsetWidth | 0, "name": col.getAttribute('data-bb-field') }
return { "width": getWidth(col.closest('th')), "name": col.getAttribute('data-bb-field') }
}),
Comment on lines 682 to 685
setTableDefaultWidth(table);
const widthState = getColumnWidthStateObject(table);
await table.invoke.invokeMethodAsync(table.options.resizeColumnCallback, index, maxWidth | 0, widthState)
await table.invoke.invokeMethodAsync(table.options.resizeColumnCallback, index, maxWidth, widthState)

Comment on lines +830 to +832
const orginIndex = index;
const currentIndex = table.dragColumns.indexOf(col);
table.invoke.invokeMethodAsync(table.options.dragColumnCallback, orginIndex, currentIndex);
Comment on lines +1370 to +1378
// 开启客户端持久化后未设置列状态的列默认使用组件参数值
return _tableColumnStateCache.ColumnVisibleStates.Count != 0
? _tableColumnStateCache.ColumnVisibleStates
: [.. cols.Where(i => !i.GetIgnore()).Select(i => new ColumnVisibleItem()
{
Name = i.GetFieldName(),
Visible = i.GetVisible(),
DisplayName = i.GetDisplayName()
})];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(Table): redesign table javascript

2 participants