Skip to content

Commit df91883

Browse files
Add test and fix behavior for when switching Debounce mode from Trailing to Leading for a DispatcherQueueTimer
1 parent 8632ff8 commit df91883

File tree

2 files changed

+66
-0
lines changed

2 files changed

+66
-0
lines changed

components/Extensions/src/Dispatcher/DispatcherQueueTimerExtensions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace CommunityToolkit.WinUI;
1717
/// </summary>
1818
public static class DispatcherQueueTimerExtensions
1919
{
20+
// TODO: We should use a WeakReference here...
2021
private static ConcurrentDictionary<DispatcherQueueTimer, Action> _debounceInstances = new ConcurrentDictionary<DispatcherQueueTimer, Action>();
2122

2223
/// <summary>
@@ -52,8 +53,20 @@ public static void Debounce(this DispatcherQueueTimer timer, Action action, Time
5253
timer.Tick -= Timer_Tick;
5354
timer.Interval = interval;
5455

56+
// Ensure we haven't been misconfigured and won't execute more times than we expect.
57+
timer.IsRepeating = false;
58+
5559
if (immediate)
5660
{
61+
// If we have a _debounceInstance queued, then we were running in trailing mode,
62+
// so if we now have the immediate flag, we should ignore this timer, and run immediately.
63+
if (_debounceInstances.ContainsKey(timer))
64+
{
65+
timeout = false;
66+
67+
_debounceInstances.Remove(timer, out var _);
68+
}
69+
5770
// If we're in immediate mode then we only execute if the timer wasn't running beforehand
5871
if (!timeout)
5972
{

components/Extensions/tests/DispatcherQueueTimerExtensionTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,59 @@ public async Task DispatcherQueueTimer_Debounce_Immediate_Interrupt()
167167
Assert.AreEqual(1, triggeredCount, "Expected 2nd request to be ignored.");
168168
}
169169

170+
/// <summary>
171+
/// Tests the scenario where we flip from wanting trailing to leading edge invocaton.
172+
/// For instance, this could be for a case where a user has cleared the textbox, so you
173+
/// want to immediately return new results vs. waiting for further input.
174+
/// </summary>
175+
/// <returns></returns>
176+
[TestCategory("DispatcherQueueTimerExtensions")]
177+
[UIThreadTestMethod]
178+
public async Task DispatcherQueueTimer_Debounce_Trailing_Switch_Leading_Interrupt()
179+
{
180+
var debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
181+
182+
var triggeredCount = 0;
183+
string? triggeredValue = null;
184+
185+
var value = "Hello";
186+
debounceTimer.Debounce(
187+
() =>
188+
{
189+
triggeredCount++;
190+
triggeredValue = value;
191+
},
192+
TimeSpan.FromMilliseconds(100), false); // Start off waiting
193+
194+
// Intentional pause to mimic reality
195+
await Task.Delay(TimeSpan.FromMilliseconds(30));
196+
197+
Assert.AreEqual(true, debounceTimer.IsRunning, "Expected time to be running.");
198+
Assert.AreEqual(0, triggeredCount, "Function shouldn't have run yet.");
199+
Assert.IsNull(triggeredValue, "Function shouldn't have run yet.");
200+
201+
// Now interrupt with a scenario we want processed immediately, i.e. user started typing something new
202+
var value2 = "He";
203+
debounceTimer.Debounce(
204+
() =>
205+
{
206+
triggeredCount++;
207+
triggeredValue = value2;
208+
},
209+
TimeSpan.FromMilliseconds(100), true);
210+
211+
Assert.AreEqual(true, debounceTimer.IsRunning, "Expected timer should still be running.");
212+
Assert.AreEqual(1, triggeredCount, "Function should now have run immediately.");
213+
Assert.AreEqual(value2, triggeredValue, "Function should have set value to 'He'");
214+
215+
// Wait to where all should be done
216+
await Task.Delay(TimeSpan.FromMilliseconds(120));
217+
218+
Assert.AreEqual(false, debounceTimer.IsRunning, "Expected to stop the timer.");
219+
Assert.AreEqual(value2, triggeredValue, "Expected value to remain the same.");
220+
Assert.AreEqual(1, triggeredCount, "Expected to interrupt execution and ignore initial queued exectution.");
221+
}
222+
170223
/// <summary>
171224
/// Tests where we start with immediately processing a delay, but then want to switch to processing after.
172225
/// For instance, maybe we want to ensure we start processing the first letter of a search query to filter initial results.

0 commit comments

Comments
 (0)