Skip to content

fix(Table): use Equlas instead of Contains#7648

Merged
ArgoZhang merged 3 commits intomainfrom
fix-table
Feb 10, 2026
Merged

fix(Table): use Equlas instead of Contains#7648
ArgoZhang merged 3 commits intomainfrom
fix-table

Conversation

@ArgoZhang
Copy link
Copy Markdown
Member

@ArgoZhang ArgoZhang commented Feb 10, 2026

Link issues

fixes #7647

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

Bug Fixes:

  • Ensure table row deselection uses value equality when clearing previously selected rows from the header checkbox action to avoid incorrect selection state.

Copilot AI review requested due to automatic review settings February 10, 2026 03:04
@bb-auto bb-auto bot added the enhancement New feature or request label Feb 10, 2026
@bb-auto bb-auto bot added this to the v10.3.0 milestone Feb 10, 2026
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Feb 10, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Adjusts table header checkbox selection logic to use value equality rather than reference-based containment when clearing selected rows, with locale files touched but not functionally changed in this diff.

Class diagram for updated Table OnHeaderCheck selection logic

classDiagram
    class TableTItem {
        List~TItem~ SelectedRows
        IEnumerable~TItem~ Rows
        Func~TItem, bool~ ShowRowCheckboxCallback
        Task OnHeaderCheck(CheckboxState state, TItem val)
        CheckboxState HeaderCheckState()
    }

    class CheckboxState {
        <<enumeration>>
        Unchecked
        Checked
        Indeterminate
    }

    TableTItem --> CheckboxState
Loading

Flow diagram for updated header checkbox selection logic in Table

flowchart TD
    A[OnHeaderCheck called with state, val] --> B[Compute items = intersection of Rows and SelectedRows]
    B --> C["Remove from SelectedRows all i where any item in items satisfies Equals(item, i)"]
    C --> D{state == Checked?}
    D -- Yes --> E{ShowRowCheckboxCallback is null?}
    E -- Yes --> F[Add all Rows to SelectedRows]
    E -- No --> G[Filter Rows with ShowRowCheckboxCallback and add to SelectedRows]
    F --> H[End]
    G --> H[End]
    D -- No --> H[End]
Loading

File-Level Changes

Change Details Files
Update header checkbox selection clearing logic to use equality comparison instead of sequence Contains to correctly remove matching selected rows.
  • Replace RemoveAll call that used Rows.Intersect(SelectedRows).Contains with logic that first computes the intersection set, then removes selected rows whose values are equal to any item in that intersection using Equals
  • Preserve existing behavior of adding rows to SelectedRows when the header checkbox is checked, including filtering via ShowRowCheckboxCallback when applicable
src/BootstrapBlazor/Components/Table/Table.razor.Checkbox.cs
Locale JSON files were modified in the PR but the diff snippet does not show content changes, implying either formatting-only or unrelated adjustments.
  • Touch en-US locale file without visible semantic changes in this diff
  • Touch zh-CN locale file without visible semantic changes in this diff
src/BootstrapBlazor.Server/Locales/en-US.json
src/BootstrapBlazor.Server/Locales/zh-CN.json

Assessment against linked issues

Issue Objective Addressed Explanation
#7647 In the Table component’s header checkbox logic, replace the use of Contains with an equality comparison using Equals when removing intersecting selected rows.

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

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 1 issue, and left some high level feedback:

  • The new OnHeaderCheck implementation introduces an O(n²) pattern by calling items.Any inside RemoveAll; consider materializing items into a HashSet<TItem> (with the appropriate equality comparer) and using RemoveAll(i => set.Contains(i)) instead.
  • Since the intent is to use value equality, it may be clearer and safer to pass an explicit IEqualityComparer<TItem> (or use EqualityComparer<TItem>.Default) rather than relying on object.Equals in Equals(item, i).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `OnHeaderCheck` implementation introduces an O(n²) pattern by calling `items.Any` inside `RemoveAll`; consider materializing `items` into a `HashSet<TItem>` (with the appropriate equality comparer) and using `RemoveAll(i => set.Contains(i))` instead.
- Since the intent is to use value equality, it may be clearer and safer to pass an explicit `IEqualityComparer<TItem>` (or use `EqualityComparer<TItem>.Default`) rather than relying on `object.Equals` in `Equals(item, i)`.

## Individual Comments

### Comment 1
<location> `src/BootstrapBlazor/Components/Table/Table.razor.Checkbox.cs:108-109` </location>
<code_context>
     protected virtual async Task OnHeaderCheck(CheckboxState state, TItem val)
     {
-        SelectedRows.RemoveAll(Rows.Intersect(SelectedRows).Contains);
+        var items = Rows.Intersect(SelectedRows);
+        SelectedRows.RemoveAll(i => items.Any(item => Equals(item, i)));
         if (state == CheckboxState.Checked)
         {
</code_context>

<issue_to_address>
**issue (bug_risk):** The new intersection logic can still enumerate `SelectedRows` while mutating it and adds an O(n²) performance cost.

Because `Intersect` is lazy, the original `RemoveAll(Rows.Intersect(SelectedRows).Contains)` could enumerate `SelectedRows` while `RemoveAll` is mutating it, and `Contains` re-enumerates the sequence each time. The new version still closes over a lazy `items` and, for each element in `SelectedRows`, calls `items.Any(...)`, which again walks `SelectedRows`. This preserves the risk of `InvalidOperationException` (modifying a collection during enumeration) and adds an O(n²) cost.

A safer and faster approach is to materialize the intersection first and then remove by direct lookup:

```csharp
var intersected = Rows.Intersect(SelectedRows).ToHashSet();
SelectedRows.RemoveAll(i => intersected.Contains(i));
```

This avoids enumerating `SelectedRows` while mutating it and reduces the removal step to roughly O(n).
</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.

Comment thread src/BootstrapBlazor/Components/Table/Table.razor.Checkbox.cs
@ArgoZhang ArgoZhang merged commit f3f9ce7 into main Feb 10, 2026
4 checks passed
@ArgoZhang ArgoZhang deleted the fix-table branch February 10, 2026 03:07
@codecov
Copy link
Copy Markdown

codecov bot commented Feb 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (2079ddb) to head (5eb0a4e).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #7648   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          750       750           
  Lines        33185     33186    +1     
  Branches      4605      4605           
=========================================
+ Hits         33185     33186    +1     
Flag Coverage Δ
BB 100.00% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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 aims to adjust table header checkbox selection behavior to use the component’s equality semantics (instead of default Contains behavior), and adds a localized resource string used by the Table selection sample UI.

Changes:

  • Updates OnHeaderCheck removal logic for selected rows in the Table checkbox behavior.
  • Adds TablesSelectionCountText localization entries for the Table selection sample (zh-CN/en-US).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/BootstrapBlazor/Components/Table/Table.razor.Checkbox.cs Changes how selected rows are removed when toggling the header checkbox.
src/BootstrapBlazor.Server/Locales/zh-CN.json Adds zh-CN localized text for displaying selected-row count in the selection sample.
src/BootstrapBlazor.Server/Locales/en-US.json Adds en-US localized text for displaying selected-row count in the selection sample.

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

Comment on lines +108 to +109
var items = Rows.Intersect(SelectedRows);
SelectedRows.RemoveAll(i => items.Any(item => Equals(item, i)));
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

This predicate re-enumerates a deferred Intersect sequence for every element in SelectedRows, turning the removal into O(n*m) and repeatedly iterating SelectedRows while it is being mutated by RemoveAll. Materialize the rows-to-remove once (e.g., via ToList()/HashSet with an appropriate comparer) and then perform the removals against that snapshot.

Suggested change
var items = Rows.Intersect(SelectedRows);
SelectedRows.RemoveAll(i => items.Any(item => Equals(item, i)));
var items = new HashSet<TItem>(Rows.Intersect(SelectedRows));
SelectedRows.RemoveAll(i => items.Contains(i));

Copilot uses AI. Check for mistakes.
"TablesSearchTitle": "Table Search"
},
"BootstrapBlazor.Server.Components.Samples.Table.TablesSelection": {
"TablesSelectionCountText": "Count:{0}",
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The English string uses a full-width colon character (), which is inconsistent with the rest of en-US resources (e.g., "Keep State:") and may look incorrect in UI. Consider changing it to an ASCII colon and spacing (e.g., "Count: {0}").

Suggested change
"TablesSelectionCountText": "Count{0}",
"TablesSelectionCountText": "Count: {0}",

Copilot uses AI. Check for mistakes.
Comment on lines +108 to +109
var items = Rows.Intersect(SelectedRows);
SelectedRows.RemoveAll(i => items.Any(item => Equals(item, i)));
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

Rows.Intersect(SelectedRows) uses EqualityComparer<TItem>.Default and will ignore the table's model equality logic (ModelEqualityComparer / CustomKeyAttribute / DynamicContext.EqualityComparer). This means some selected rows may not be removed correctly, so the change may not actually fix the reported selection mismatch.

Consider building the removal set using the table's model comparer (e.g., new ModelHashSetComparer<TItem>(this)) and then removing via HashSet.Contains, or otherwise ensure the intersection/removal uses Table.Equals(TItem,TItem) semantics throughout.

Suggested change
var items = Rows.Intersect(SelectedRows);
SelectedRows.RemoveAll(i => items.Any(item => Equals(item, i)));
// Build removal set using the table's model equality comparer to ensure
// that intersection logic respects custom model equality semantics.
var removalSet = new HashSet<TItem>(new ModelHashSetComparer<TItem>(this));
foreach (var row in Rows)
{
removalSet.Add(row);
}
SelectedRows.RemoveAll(i => removalSet.Contains(i));

Copilot uses AI. Check for mistakes.
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.

fix(Table): use Equlas instead of Contains

2 participants