Skip to content

BitNumberField and other inputs CancellationTokenSource disposal issues #12249

@Greexter

Description

@Greexter

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

When using BitNumberField (and similarly BitTimePicker, BitDatePicker, BitDateRangePicker) with an OnIncrement/OnDecrement/OnSelectTime callback that causes the component to be disposed (e.g., navigating away or conditionally rendering it out), clicking the increment or decrement button throws an ObjectDisposedException.

There are two related issues:

  1. Primary: HandleOnPointerDown awaits ChangeValueAndInvokeEvents (which invokes user callbacks like OnIncrement/OnDecrement or the value change event). If the callback disposes the component, DisposeAsync runs and calls _continuousChangeValueCts.Dispose(). When HandleOnPointerDown resumes, it calls ResetCts() => _cts.Cancel() on an already-disposed CancellationTokenSource => ObjectDisposedException.

  2. Secondary (all 5 components): DisposeAsync disposed the CTS without first cancelling it. Since IsCancellationRequested returns false on a disposed-but-uncancelled CTS, the ContinuousChangeValue/Time recursive loop continued running after component disposal, calling StateHasChanged() on a component that should be disposed indefinitely.

Expected Behavior

No exception should be thrown. After a value-change callback disposes the component, all continuations in the pointer-down handler should exit cleanly.

Steps To Reproduce

@page "/bug"

@if (_double == 1)
{
	<BitNumberField @bind-Value="_double" Mode="BitSpinButtonMode.Compact" />
}

@if (_time == TimeSpan.Zero)
{
	<BitTimePicker @bind-Value="_time" />
}

@* open the picker, then use the time spinners *@
@if (_date?.TimeOfDay == TimeSpan.Zero)
{
	<BitDatePicker @bind-Value="_date" ShowTimePicker="true" />
}

@* open the picker, then use the time spinners *@
@if (_dateRange?.StartDate?.TimeOfDay == TimeSpan.Zero)
{
	<BitDateRangePicker @bind-Value="_dateRange" ShowTimePicker="true" />
}

@if (_calDate?.TimeOfDay == TimeSpan.Zero)
{
	<BitCalendar @bind-Value="_calDate" ShowTimePicker="true" />
}

@code {
    double _double = 1;
    TimeSpan? _time = TimeSpan.Zero;
    DateTimeOffset? _date = DateTimeOffset.UtcNow.Date;
    BitDateRangePickerValue? _dateRange = new() { StartDate = DateTimeOffset.UtcNow.Date, EndDate = DateTimeOffset.UtcNow.Date.AddDays(3) };
    DateTimeOffset? _calDate = DateTimeOffset.UtcNow.Date;
}

Clicking up/down buttons for all components will trigger the bugs. For the datetime components, no exception is thrown, but they apear periodically because their value changed callbacks are still firing.

Exceptions (if any)

System.ObjectDisposedException: The CancellationTokenSource has been disposed.
   at System.Threading.CancellationTokenSource.Cancel()
   at Bit.BlazorUI.BitNumberField`1.ResetCts() in C:\Users\Jakub\source\repos\bitplatform-upstream\src\BlazorUI\Bit.BlazorUI\Components\Inputs\NumberField\BitNumberField.razor.cs:line 660
   at Bit.BlazorUI.BitNumberField`1.HandleOnPointerDown(Boolean isIncrement) in C:\Users\Jakub\source\repos\bitplatform-upstream\src\BlazorUI\Bit.BlazorUI\Components\Inputs\NumberField\BitNumberField.razor.cs:line 540
   at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)

for spin buttons on BitNumberField, other components will not throw because of synchronous value updates.

.NET Version

10.0.201

Anything else?

No response

Metadata

Metadata

Assignees

Type

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions