0

So I'm basically trying to delay the invocation of filter process by 1.5 seconds to allow user to type multiple keystrokes in case they want to. If a new keystroke is typed, previously waiting task is cancelled and a new one starts waiting:

System.Threading.CancellationTokenSource token = new System.Threading.CancellationTokenSource(); private async void MyTextBox_TextChanged(object sender, TextChangedEventArgs e) { token.Cancel(); await System.Threading.Tasks.Task.Delay(1500, token.Token); this.filterText = (sender as TextBox).Text; (this.Resources["CVS"] as CollectionViewSource).View.Refresh(); //Earlier I had tried this variant too: //System.Threading.Tasks.Task.Delay(500, token.Token).ContinueWith(_ => //{ // this.filterText = (sender as TextBox).Text; // (this.Resources["CVS"] as CollectionViewSource).View.Refresh(); //}); } 

But the filter process (View.Refresh() line) hits immediately on first keystroke without waiting. My impression was that calling Cancel on the token would kill Delay() and thereby the continuation task too, before planting the next one, but apparently this scheme doesn't work.

What am I missing?

10
  • 3
    You cancelled the token that is used to cancel delaying. So no delay occurred. Commented Jan 25, 2022 at 13:38
  • Yeah, but is a token single-use thing? If not, see that i'm starting the new task AFTER cancelling the previous one. Commented Jan 25, 2022 at 13:39
  • 2
    After canceling, you have a canceled Token. You need a new CancellationTokenSource. Commented Jan 25, 2022 at 13:40
  • Your ContinueWith doesn't use the token so it has no influence on it. Use a ContinueWith overload that actually uses the token. Commented Jan 25, 2022 at 13:41
  • 1
    token.Token <- you confuse yourself. token is not a token, it's a CancellationTokenSource. use proper naming, so it's clear what it is and what it does. And note, does CancellationTokenSource have a Reset() or UnCancel() method? No. You need a new one. Edit: I just see it has a TryReset. Still, might not be the best use-case. Commented Jan 25, 2022 at 13:42

2 Answers 2

1

The proper way to handle this is not with Task.Delay and exceptions (as exceptions are for exceptional circumstances), but using a Timer with the Timer.Elapsed event.

E.g.

using Timer = System.Windows.Forms.Timer; private readonly Timer timer = new Timer(); private static string newText = ""; public Form1() { timer.Interval = 1500; timer.Tick += OnTimedEvent; } private void MyTextBox_TextChanged(object sender, EventArgs e) { timer.Stop(); // sets the time back to 0 newText = (sender as TextBox).Text; // sets new text timer.Start(); // restarts the timer } private void OnTimedEvent(Object source, EventArgs e) { filterText = newText; (Resources["CVS"] as CollectionViewSource).View.Refresh(); } 

(Not sure this is 100% correct, but you get the gist.)


Old snippet relating to the discussions in the comments.

As the post says: this is not needed, as Task.Delay will link a listener to the CancellationToken, thus .Cancel() will block until all listeners have heard it.

using System.Threading; using System.Threading.Tasks; private CancellationTokenSource cts = new CancellationTokenSource(); private Task delayTask; private async void TenantsFilter_TextChanged(object sender, TextChangedEventArgs e) { cts.Cancel(); if (delayTask != null) { try{await delayTask;} catch(TaskCanceledException){} } cts = new CancellationTokenSource(); try { delayTask = Task.Delay(1500, cts.Token); await delayTask; this.filterText = (sender as TextBox).Text; (this.Resources["CVS"] as CollectionViewSource).View.Refresh(); } catch(TaskCanceledException) { } } 
Sign up to request clarification or add additional context in comments.

11 Comments

OK. I spent some more time reading about how CancellationTokenSource works. If my understanding is correct, cts.Cancel call in our code is blocking and waits for all registered callbacks to complete. Therefore, there shouldn't ideally be a race condition here.
@dotNET wow, great find. I learned something important today. I'll remove this answer tomorrow
Why delayTask.Wait(); instead of await delayTask;?
So you think that each await inside an async method results in an additional state machine? That's not how it works. Each async method results in one state machine, regardless of how many awaits are inside the async method. Refraining from using await for this reason, and risking to block the UI thread by calling .Wait() on a task that may have not completed yet, is not justified.
@TheodorZoulias ah thanks. Pitifully I don't have that much experience with async/await, as the technical lead at my company banned them (he doesn't know them, doesn't trust them and doesn't want to learn them... he's weird)
|
1

If this helps anyone, the following is correctly working for me. My mistake was that I incorrectly assumed that CancellationTokenSource is a signaling device and could be used multiple times. That is apparently not the case:

private System.Threading.CancellationTokenSource cts = new System.Threading.CancellationTokenSource(); private async void TenantsFilter_TextChanged(object sender, TextChangedEventArgs e) { cts.Cancel(); cts = new System.Threading.CancellationTokenSource(); try { await System.Threading.Tasks.Task.Delay(1500, cts.Token); this.filterText = (sender as TextBox).Text; (this.Resources["CVS"] as CollectionViewSource).View.Refresh(); } catch(System.Threading.Tasks.TaskCanceledException ee) { } } 

Posting it here for my own record and just to let others check I'm still not doing anything wrong.

5 Comments

Thinking about this, I'm not sure this is 100% thread safe. cts is being referenced by another task/thread when you replace it. If the other thread has not seen the cancellation yet (which can happen, as scheduling is not e certainty), there might be a data race. You need to be sure of the other task is done before replacing it.
@JHBonarius: Hmm... Moreover, this could be called in quick successions by the user typing fast. What do u suggest? A monitor or mutex maybe?
I'm just thinking aloud, because it's not a trivial thing. But maybe assign the Task.Delay object to a private field before you await it. You can then .Wait() on it.
@JHBonarius - I'm not sure what you think that extra assignment does. By the time await is applied, the call to Task.Delay was already invoked, assessing the value of cts.Token and storing that internally. await is just working with whatever awaitable the Task.Delay method returned.
For the lazy future reader, I'm pasting the same comment here too. OK. I spent some more time reading about how CancellationTokenSource works. If my understanding is correct, cts.Cancel call in our code is blocking and waits for all registered callbacks to complete. Therefore, there shouldn't ideally be a race condition here.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.