Skip to content

Commit dbba6f1

Browse files
Initial
1 parent 28fde7e commit dbba6f1

5 files changed

Lines changed: 633 additions & 79 deletions

File tree

Lines changed: 107 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,62 @@
1-
using System.Threading.Tasks;
2-
using System.Timers;
3-
using Microsoft.AspNetCore.Components;
1+
using Microsoft.AspNetCore.Components;
42
using MudBlazor;
3+
using MudBlazor.State;
4+
using MudExtensions.Utilities;
55

66
namespace MudExtensions
77
{
88
/// <summary>
9-
///
9+
/// An extended base class for designing input components which update after a delay.
1010
/// </summary>
11-
/// <typeparam name="T"></typeparam>
11+
/// <typeparam name="T">The type of object managed by this input.</typeparam>
1212
public abstract class MudDebouncedInputExtended<T> : MudBaseInputExtended<T>
1313
{
14-
private System.Timers.Timer? _timer;
15-
private double _debounceInterval;
14+
private DebounceDispatcher? _debouncer;
1615

1716
/// <summary>
18-
/// Interval to be awaited in milliseconds before changing the Text value
17+
/// Initializes a new instance of the <see cref="MudDebouncedInputExtended{T}"/> class.
1918
/// </summary>
20-
[Parameter]
21-
[Category(CategoryTypes.FormComponent.Behavior)]
22-
public double DebounceInterval
19+
protected MudDebouncedInputExtended()
2320
{
24-
get => _debounceInterval;
25-
set
26-
{
27-
if (DoubleEpsilonEqualityComparer.Default.Equals(_debounceInterval, value))
28-
return;
29-
_debounceInterval = value;
30-
if (_debounceInterval == 0)
31-
{
32-
// not debounced, dispose timer if any
33-
ClearTimer(suppressTick: false);
34-
return;
35-
}
36-
SetTimer();
37-
}
21+
using var registerScope = CreateRegisterScope();
22+
registerScope.RegisterParameter<double>(nameof(DebounceInterval))
23+
.WithParameter(() => DebounceInterval)
24+
.WithComparer(DoubleEpsilonEqualityComparer.Default)
25+
.WithChangeHandler(OnDebounceIntervalChangedAsync);
3826
}
3927

28+
[Inject]
29+
private TimeProvider TimeProvider { get; set; } = null!;
30+
4031
/// <summary>
41-
/// callback to be called when the debounce interval has elapsed
42-
/// receives the Text as a parameter
32+
/// The number of milliseconds to wait before updating the <see cref="MudBaseInput{T}.Text"/> value.
4333
/// </summary>
44-
[Parameter] public EventCallback<string> OnDebounceIntervalElapsed { get; set; }
34+
[Parameter, ParameterState(ParameterUsage = ParameterUsageOptions.None)]
35+
[Category(CategoryTypes.FormComponent.Behavior)]
36+
public double DebounceInterval { get; set; }
4537

4638
/// <summary>
47-
///
39+
/// callback to be called when the debounce interval has elapsed
40+
/// receives the Text as a parameter
4841
/// </summary>
49-
/// <returns></returns>
50-
protected Task OnChanged()
42+
[Parameter]
43+
public EventCallback<string> OnDebounceIntervalElapsed { get; set; }
44+
45+
/// <inheritdoc />
46+
protected override Task UpdateTextPropertyAsync(bool updateValue)
5147
{
52-
if (DebounceInterval > 0 && _timer != null)
53-
{
54-
_timer.Stop();
55-
return base.UpdateValuePropertyAsync(false);
56-
}
48+
// Don't update text if we're debouncing and the value hasn't actually changed
49+
var suppressTextUpdate = !updateValue
50+
&& DebounceInterval > 0
51+
&& _debouncer is not null
52+
&& _debouncer.IsPending;
5753

58-
return Task.CompletedTask;
54+
return suppressTextUpdate
55+
? Task.CompletedTask
56+
: base.UpdateTextPropertyAsync(updateValue);
5957
}
6058

61-
/// <summary>
62-
///
63-
/// </summary>
64-
/// <param name="updateText"></param>
65-
/// <returns></returns>
59+
/// <inheritdoc />
6660
protected override Task UpdateValuePropertyAsync(bool updateText)
6761
{
6862
// This method is called when Value property needs to be refreshed from the current Text property, so typically because Text property has changed.
@@ -73,70 +67,105 @@ protected override Task UpdateValuePropertyAsync(bool updateText)
7367
// we have a change coming not from the Text setter, no debouncing is needed
7468
return base.UpdateValuePropertyAsync(updateText);
7569
}
76-
// if debounce interval is 0 we update immediately
77-
if (DebounceInterval <= 0 || _timer == null)
70+
// if debounce interval is 0 or no debouncer, we update immediately
71+
if (DebounceInterval <= 0 || _debouncer is null)
72+
{
7873
return base.UpdateValuePropertyAsync(updateText);
79-
// If a debounce interval is defined, we want to delay the update of Value property.
80-
_timer.Stop();
81-
// restart the timer while user is typing
82-
_timer.Start();
74+
}
75+
76+
// Debounce the update - use fire-and-forget pattern to match the old Timer implementation.
77+
_ = _debouncer.DebounceAsync(OnDebouncedUpdate);
8378
return Task.CompletedTask;
8479
}
8580

86-
/// <summary>
87-
///
88-
/// </summary>
81+
/// <inheritdoc />
82+
protected override async Task ValidateValue()
83+
{
84+
if (await SynchronizePendingValueForValidationAsync())
85+
{
86+
return;
87+
}
88+
89+
await base.ValidateValue();
90+
}
91+
92+
/// <inheritdoc />
8993
protected override void OnParametersSet()
9094
{
9195
base.OnParametersSet();
9296
// if input is to be debounced, makes sense to bind the change of the text to oninput
9397
// so we set Immediate to true
9498
if (DebounceInterval > 0)
99+
{
100+
// TODO: Don't write to parameter directly
95101
Immediate = true;
102+
}
96103
}
97104

98-
private void SetTimer()
105+
private async Task OnDebounceIntervalChangedAsync(ParameterChangedEventArgs<double> args)
99106
{
100-
if (_timer == null)
107+
if (args.Value <= 0)
101108
{
102-
_timer = new System.Timers.Timer();
103-
_timer.Elapsed += OnTimerTick;
104-
_timer.AutoReset = false;
109+
// not debounced, dispose debouncer if any
110+
_debouncer?.Dispose();
111+
_debouncer = null;
112+
return;
105113
}
106-
_timer.Interval = DebounceInterval;
107-
}
108114

109-
private void OnTimerTick(object? sender, ElapsedEventArgs e)
110-
{
111-
InvokeAsync(OnTimerTickGuiThread).CatchAndLog();
115+
// Create debouncer if we don't have one
116+
if (_debouncer is null)
117+
{
118+
_debouncer = new DebounceDispatcher(TimeSpan.FromMilliseconds(args.Value), false, TimeProvider);
119+
}
120+
else
121+
{
122+
// Only update interval if it has meaningfully changed
123+
// Use DoubleEpsilonEqualityComparer to avoid unnecessary updates due to floating-point precision
124+
if (!DoubleEpsilonEqualityComparer.Default.Equals(args.LastValue, args.Value))
125+
{
126+
await _debouncer.UpdateIntervalAsync(TimeSpan.FromMilliseconds(args.Value));
127+
}
128+
}
112129
}
113130

114-
private async Task OnTimerTickGuiThread()
131+
private async Task<bool> SynchronizePendingValueForValidationAsync()
115132
{
116-
await base.UpdateValuePropertyAsync(false);
117-
await OnDebounceIntervalElapsed.InvokeAsync(ReadText);
133+
if (DebounceInterval <= 0 || _debouncer is null || !_debouncer.IsPending)
134+
{
135+
return false;
136+
}
137+
138+
var pendingValue = ConvertGet(ReadText);
139+
var pendingValueChanged = !EqualityComparer<T?>.Default.Equals(ReadValue, pendingValue);
140+
141+
await _debouncer.CancelAsync();
142+
143+
if (!pendingValueChanged)
144+
{
145+
return false;
146+
}
147+
148+
// SetValueAndUpdateTextAsync already triggers FieldChanged and BeginValidateAsync,
149+
// so the synced validation happens there and this call can stop.
150+
await SetValueAndUpdateTextAsync(pendingValue, updateText: false);
151+
return true;
118152
}
119153

120-
private void ClearTimer(bool suppressTick = false)
154+
private Task OnDebouncedUpdate()
121155
{
122-
if (_timer == null)
123-
return;
124-
var wasEnabled = _timer.Enabled;
125-
_timer.Stop();
126-
_timer.Elapsed -= OnTimerTick;
127-
_timer.Dispose();
128-
_timer = null;
129-
if (wasEnabled && !suppressTick)
130-
OnTimerTickGuiThread().CatchAndLog();
156+
return InvokeAsync(async () =>
157+
{
158+
await base.UpdateValuePropertyAsync(false);
159+
await OnDebounceIntervalElapsed.InvokeAsync(ReadText);
160+
});
131161
}
132162

133-
/// <summary>
134-
///
135-
/// </summary>
163+
/// <inheritdoc />
136164
protected override async ValueTask DisposeAsyncCore()
137165
{
138166
await base.DisposeAsyncCore();
139-
ClearTimer(suppressTick: true);
167+
_debouncer?.Dispose();
168+
_debouncer = null;
140169
}
141170
}
142171
}

src/CodeBeam.MudBlazor.Extensions/Components/TextFieldExtended/MudTextFieldExtended.razor

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
OnInput="@OnInput"
4848
OnBlur="@OnBlurredAsync"
4949
OnKeyDown="@InvokeKeyDownAsync"
50-
OnInternalInputChanged="OnChanged"
5150
OnKeyUp="@InvokeKeyUpAsync"
5251
OnBeforeInput="@InvokeBeforeInputAsync"
5352
KeyDownPreventDefault="KeyDownPreventDefault"

0 commit comments

Comments
 (0)