Skip to content

Fix CancellationTokenSource disposal issues in multiple components (#12249)#12255

Open
msynk wants to merge 4 commits intobitfoundation:developfrom
msynk:12249-blazorui-inputs-cts-issues
Open

Fix CancellationTokenSource disposal issues in multiple components (#12249)#12255
msynk wants to merge 4 commits intobitfoundation:developfrom
msynk:12249-blazorui-inputs-cts-issues

Conversation

@msynk
Copy link
Copy Markdown
Member

@msynk msynk commented Apr 13, 2026

closes #12249

Summary by CodeRabbit

  • Bug Fixes
    • Fixed improper cleanup of asynchronous operations during component disposal across DataGrid, Calendar, DatePicker, DateRangePicker, NumberField, SearchBox, TimePicker, InfiniteScrolling, BasicList, and Tooltip components. Enhanced disposal behavior now properly cancels pending async work and prevents orphaned operations after component teardown.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 091e2edb-e7ce-45ed-8f20-f284e201f007

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The pull request addresses cancellation token source disposal issues across multiple Blazor UI components. Changes add explicit cancellation signaling before disposal and introduce disposal guards to prevent further execution after component disposal, ensuring clean teardown without exceptions.

Changes

Cohort / File(s) Summary
Disposal Cancellation
src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.razor.cs, src/BlazorUI/Bit.BlazorUI.Extras/Components/InfiniteScrolling/BitInfiniteScrolling.razor.cs, src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor.cs, src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/BitBasicList.razor.cs
Added explicit Cancel() calls to cancellation token sources before disposal in DisposeAsync, ensuring in-flight async operations are signaled rather than only terminated by source disposal.
Time-Increment Components with Disposal Guards
src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor.cs, src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor.cs, src/BlazorUI/Bit.BlazorUI/Components/Inputs/DateRangePicker/BitDateRangePicker.razor.cs, src/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.razor.cs
Added IsDisposed guards in time-change loops (ContinuousChangeTime, HandleOnPointerDown, ResetCts) to prevent state updates and further recursion after disposal; enhanced DisposeAsync to cancel token sources before disposing.
Numeric Input with Disposal Guards
src/BlazorUI/Bit.BlazorUI/Components/Inputs/NumberField/BitNumberField.razor.cs
Added IsDisposed checks in value-change flow (HandleOnPointerDown, ContinuousChangeValue, ResetCts) and explicit cancellation in DisposeAsync to prevent operations after component disposal.
Tooltip Cleanup
src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Tooltip/BitTooltip.razor.cs
Added new DisposeAsync override to explicitly cancel and dispose show/hide delay token sources before delegating to base class.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A hop through disposal we take,
Canceling tokens before we break,
Guards on disposal stand tall and true,
No more exceptions to chase us through!
Clean teardown dances, the loop set free,
Safe async work as it's meant to be!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: fixing CancellationTokenSource disposal issues across multiple Blazor UI components, directly matching the changeset content.
Linked Issues check ✅ Passed All code changes directly address the two primary issues in #12249: adding IsDisposed guards to prevent operations after disposal and explicitly canceling CTS before disposal to prevent ObjectDisposedException and continued loop execution.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to fixing CancellationTokenSource disposal and IsDisposed guard issues in the identified components; no unrelated modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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

Fixes a set of disposal-related race conditions across Bit.BlazorUI input components where CancellationTokenSource instances could be disposed while still in use (or not cancelled before disposal), leading to ObjectDisposedException and runaway “continuous change” loops after component disposal.

Changes:

  • Add IsDisposed short-circuits in pointer-down handlers and continuous-change loops to stop processing after user callbacks dispose the component.
  • Ensure CTS instances are cancelled before being disposed in DisposeAsync across multiple components.
  • Add disposal cleanup for tooltip delay CTSs and cancel pending data-load CTS in the DataGrid.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Tooltip/BitTooltip.razor.cs Cancels/disposes tooltip show/hide delay CTSs during disposal.
src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/BitBasicList.razor.cs Cancels global CTS before disposal to stop pending operations.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.razor.cs Adds IsDisposed guards and cancels CTS on dispose to stop continuous time changes safely.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor.cs Cancels CTS on dispose to stop pending suggestion-provider calls.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/NumberField/BitNumberField.razor.cs Prevents CTS usage after disposal and stops continuous increment/decrement loop cleanly.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/DateRangePicker/BitDateRangePicker.razor.cs Adds disposal guards and CTS cancellation to stop continuous time spinner loop.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor.cs Adds disposal guards and CTS cancellation to stop continuous time spinner loop.
src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor.cs Adds disposal guard in loop entry/reset and cancels CTS on dispose (but still has a remaining race; see comments).
src/BlazorUI/Bit.BlazorUI.Extras/Components/InfiniteScrolling/BitInfiniteScrolling.razor.cs Cancels global CTS before disposal to stop pending operations.
src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.razor.cs Cancels/disposes pending data-load CTS during disposal.
Comments suppressed due to low confidence (1)

src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor.cs:1081

  • ContinuousChangeTime calls ChangeTime(...) and then immediately calls StateHasChanged()/recurses without re-checking IsDisposed. In this component, ChangeTime updates CurrentValue, which kicks off SetCurrentValueAsync (fire-and-forget) that can invoke user callbacks and dispose the component before the subsequent StateHasChanged()/recursion runs. Add an IsDisposed check after ChangeTime (and similarly in HandleOnPointerDown right after ChangeTime and before ResetCts/starting the background loop) so the loop exits cleanly when a value-change callback removes the component.
    private async Task ContinuousChangeTime(bool isNext, bool isHour, CancellationTokenSource cts)
    {
        if (cts.IsCancellationRequested || IsDisposed) return;

        ChangeTime(isNext, isHour);

        StateHasChanged();

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor.cs (1)

656-664: ⚠️ Potential issue | 🟠 Major

Add cancellation and disposal guards to SearchItems() provider call.

Line 390 fire-and-forgets SearchItems(), and the SuggestItemsProvider await at line 532 receives a cancellation token that can be cancelled from both DisposeAsync (line 662) and subsequent keystroke events. If a provider properly honors the token by throwing OperationCanceledException, the unobserved task faults. Additionally, state is modified after the await without checking if the component was disposed or if the operation was cancelled. Wrap the provider call in try-catch for OperationCanceledException and add a stale-guard check before continuing into the callout path.

Suggested fix
         else if (SuggestItemsProvider is not null)
         {
             _cancellationTokenSource?.Cancel();
             _cancellationTokenSource?.Dispose();
-            _cancellationTokenSource = new();
-            _viewSuggestedItems = [.. (await SuggestItemsProvider(new(CurrentValue, MaxSuggestCount, _cancellationTokenSource.Token))).Take(MaxSuggestCount)];
+            var cts = _cancellationTokenSource = new();
+
+            try
+            {
+                var items = await SuggestItemsProvider(new(CurrentValue, MaxSuggestCount, cts.Token));
+                if (IsDisposed || cts.IsCancellationRequested || _cancellationTokenSource != cts) return;
+
+                _viewSuggestedItems = [.. items.Take(MaxSuggestCount)];
+            }
+            catch (OperationCanceledException) when (cts.IsCancellationRequested)
+            {
+                return;
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor.cs`
around lines 656 - 664, SearchItems() currently fire-and-forgets a call to the
SuggestItemsProvider which can be cancelled via DisposeAsync
(_cancellationTokenSource) or by new keystrokes; wrap the provider invocation in
a try/catch that catches OperationCanceledException and returns silently (to
avoid unobserved task faults), and before applying any state changes after the
await check both the provided CancellationToken (token.IsCancellationRequested)
and the component IsDisposed flag to guard against stale results; update the
code in SearchItems() (and any helper that awaits SuggestItemsProvider) to use
the same token from _cancellationTokenSource, catch OCE, and only call
StateHasChanged/update callout UI if not canceled and not disposed.
src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.razor.cs (1)

500-505: ⚠️ Potential issue | 🟠 Major

Handle OperationCanceledException from the items provider in the non-virtualized refresh path.

Line 345 awaits ResolveItemsRequestAsync(request) without catching OperationCanceledException, while Line 327 cancels the same CTS and DisposeAsync (lines 504-505) also cancels it. An ItemsProvider that propagates OperationCanceledException will throw unhandled instead of cleanly aborting, even though the check at line 346 suggests cancellation was anticipated. The same gap exists in ProvideVirtualizedItems at line 381.

Wrap the provider call in a try/catch for OperationCanceledException when thisLoadCts.IsCancellationRequested is true to treat cancellation as a normal exit path:

Suggested fix
-            var result = await ResolveItemsRequestAsync(request);
+            BitDataGridItemsProviderResult<TGridItem> result;
+            try
+            {
+                result = await ResolveItemsRequestAsync(request);
+            }
+            catch (OperationCanceledException) when (thisLoadCts.IsCancellationRequested)
+            {
+                return;
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.razor.cs`
around lines 500 - 505, Resolve unhandled OperationCanceledException from the
items provider by wrapping calls to ResolveItemsRequestAsync(request)
(non-virtualized refresh path) and the analogous call in ProvideVirtualizedItems
in a try/catch that catches OperationCanceledException; inside the catch, check
thisLoadCts.IsCancellationRequested (or the relevant CancellationToken) and
silently return/exit the method to treat cancellation as normal, and only
rethrow if the token was not cancelled; also ensure DisposeAsync cancels and
disposes _pendingDataLoadCancellationTokenSource as it already does so that
cancellations can occur safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/BlazorUI/Bit.BlazorUI.Extras/Components/InfiniteScrolling/BitInfiniteScrolling.razor.cs`:
- Around line 204-208: After cancelling and disposing _globalCts you must
prevent LoadMoreItems from unconditionally calling StateHasChanged during
teardown; update LoadMoreItems to check the cancellation state (e.g. inspect
_globalCts?.IsCancellationRequested or the active CancellationToken) and
early-return when cancellation has been requested or the token/source is null,
so StateHasChanged is not invoked after _globalCts is cancelled/disposed;
reference _globalCts, LoadMoreItems, and StateHasChanged to locate and apply the
guard.

In `@src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/BitBasicList.razor.cs`:
- Around line 278-282: PerformLoadMore() can still call the unconditional
StateHasChanged after _globalCts is Cancel()ed/Dispose()d; guard the trailing
render by checking the cancellation state before invoking StateHasChanged.
Update PerformLoadMore() to capture/inspect the same CTS (or its Token) used by
the teardown and only call StateHasChanged when the CTS is non-null and not
IsCancellationRequested (or when an _isDisposed flag is false); ensure the
cancellation branch in Dispose/teardown sets the same signal so no render runs
after _globalCts.Cancel()/Dispose().

---

Outside diff comments:
In `@src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.razor.cs`:
- Around line 500-505: Resolve unhandled OperationCanceledException from the
items provider by wrapping calls to ResolveItemsRequestAsync(request)
(non-virtualized refresh path) and the analogous call in ProvideVirtualizedItems
in a try/catch that catches OperationCanceledException; inside the catch, check
thisLoadCts.IsCancellationRequested (or the relevant CancellationToken) and
silently return/exit the method to treat cancellation as normal, and only
rethrow if the token was not cancelled; also ensure DisposeAsync cancels and
disposes _pendingDataLoadCancellationTokenSource as it already does so that
cancellations can occur safely.

In `@src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor.cs`:
- Around line 656-664: SearchItems() currently fire-and-forgets a call to the
SuggestItemsProvider which can be cancelled via DisposeAsync
(_cancellationTokenSource) or by new keystrokes; wrap the provider invocation in
a try/catch that catches OperationCanceledException and returns silently (to
avoid unobserved task faults), and before applying any state changes after the
await check both the provided CancellationToken (token.IsCancellationRequested)
and the component IsDisposed flag to guard against stale results; update the
code in SearchItems() (and any helper that awaits SuggestItemsProvider) to use
the same token from _cancellationTokenSource, catch OCE, and only call
StateHasChanged/update callout UI if not canceled and not disposed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a0e270b1-59ff-4592-a723-f7b49ddc1ed4

📥 Commits

Reviewing files that changed from the base of the PR and between 4cc435b and 7c0a149.

📒 Files selected for processing (10)
  • src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.razor.cs
  • src/BlazorUI/Bit.BlazorUI.Extras/Components/InfiniteScrolling/BitInfiniteScrolling.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/Calendar/BitCalendar.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/DatePicker/BitDatePicker.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/DateRangePicker/BitDateRangePicker.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/NumberField/BitNumberField.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Inputs/TimePicker/BitTimePicker.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/BitBasicList.razor.cs
  • src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Tooltip/BitTooltip.razor.cs

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

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

@Greexter
Copy link
Copy Markdown
Contributor

Thank you for the fix! I looked at the changes and everything seems alright. I tested the reported issues and all is working well now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BitNumberField and other inputs CancellationTokenSource disposal issues

4 participants