1- using System . Threading . Tasks ;
2- using System . Timers ;
3- using Microsoft . AspNetCore . Components ;
1+ using Microsoft . AspNetCore . Components ;
42using MudBlazor ;
3+ using MudBlazor . State ;
4+ using MudExtensions . Utilities ;
55
66namespace 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}
0 commit comments