Skip to content

Commit 4d9816e

Browse files
authored
Merge pull request #111 from CommunityToolkit/dev/async-command-concurrency-notify
Fix AsyncRelayCommand with allowConcurrentExecutions == false not raising CanExecuteChanged
2 parents 5dcc52b + 105e31a commit 4d9816e

File tree

4 files changed

+225
-16
lines changed

4 files changed

+225
-16
lines changed

CommunityToolkit.Mvvm/Input/AsyncRelayCommand.cs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -265,21 +265,33 @@ public Task ExecuteAsync(object? parameter)
265265
{
266266
if (CanExecute(parameter))
267267
{
268-
// Non cancelable command delegate
268+
Task executionTask;
269+
269270
if (this.execute is not null)
270271
{
271-
return ExecutionTask = this.execute();
272+
// Non cancelable command delegate
273+
executionTask = ExecutionTask = this.execute();
272274
}
275+
else
276+
{
277+
// Cancel the previous operation, if one is pending
278+
this.cancellationTokenSource?.Cancel();
279+
280+
CancellationTokenSource cancellationTokenSource = this.cancellationTokenSource = new();
273281

274-
// Cancel the previous operation, if one is pending
275-
this.cancellationTokenSource?.Cancel();
282+
PropertyChanged?.Invoke(this, IsCancellationRequestedChangedEventArgs);
276283

277-
CancellationTokenSource cancellationTokenSource = this.cancellationTokenSource = new();
284+
// Invoke the cancelable command delegate with a new linked token
285+
executionTask = ExecutionTask = this.cancelableExecute!(cancellationTokenSource.Token);
286+
}
278287

279-
PropertyChanged?.Invoke(this, IsCancellationRequestedChangedEventArgs);
288+
// If concurrent executions are disabled, notify the can execute change as well
289+
if (!this.allowConcurrentExecutions)
290+
{
291+
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
292+
}
280293

281-
// Invoke the cancelable command delegate with a new linked token
282-
return ExecutionTask = this.cancelableExecute!(cancellationTokenSource.Token);
294+
return executionTask;
283295
}
284296

285297
return Task.CompletedTask;

CommunityToolkit.Mvvm/Input/AsyncRelayCommand{T}.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -269,21 +269,34 @@ public Task ExecuteAsync(T? parameter)
269269
{
270270
if (CanExecute(parameter))
271271
{
272-
// Non cancelable command delegate
272+
Task executionTask;
273+
274+
273275
if (this.execute is not null)
274276
{
275-
return ExecutionTask = this.execute(parameter);
277+
// Non cancelable command delegate
278+
executionTask = ExecutionTask = this.execute(parameter);
276279
}
280+
else
281+
{
282+
// Cancel the previous operation, if one is pending
283+
this.cancellationTokenSource?.Cancel();
284+
285+
CancellationTokenSource cancellationTokenSource = this.cancellationTokenSource = new();
277286

278-
// Cancel the previous operation, if one is pending
279-
this.cancellationTokenSource?.Cancel();
287+
PropertyChanged?.Invoke(this, AsyncRelayCommand.IsCancellationRequestedChangedEventArgs);
280288

281-
CancellationTokenSource cancellationTokenSource = this.cancellationTokenSource = new();
289+
// Invoke the cancelable command delegate with a new linked token
290+
executionTask = ExecutionTask = this.cancelableExecute!(parameter, cancellationTokenSource.Token);
291+
}
282292

283-
PropertyChanged?.Invoke(this, AsyncRelayCommand.IsCancellationRequestedChangedEventArgs);
293+
// If concurrent executions are disabled, notify the can execute change as well
294+
if (!this.allowConcurrentExecutions)
295+
{
296+
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
297+
}
284298

285-
// Invoke the cancelable command delegate with a new linked token
286-
return ExecutionTask = this.cancelableExecute!(parameter, cancellationTokenSource.Token);
299+
return executionTask;
287300
}
288301

289302
return Task.CompletedTask;

tests/CommunityToolkit.Mvvm.UnitTests/Test_AsyncRelayCommand.cs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,4 +325,97 @@ async void TestCallback(Action throwAction, Action completeAction)
325325

326326
Assert.IsTrue(success);
327327
}
328+
329+
// See https://github.com/CommunityToolkit/dotnet/issues/108
330+
[TestMethod]
331+
public void Test_AsyncRelayCommand_ExecuteDoesNotRaiseCanExecuteChanged()
332+
{
333+
TaskCompletionSource<object?> tcs = new();
334+
335+
AsyncRelayCommand command = new(() => tcs.Task, allowConcurrentExecutions: true);
336+
337+
(object? Sender, EventArgs? Args) args = default;
338+
339+
command.CanExecuteChanged += (s, e) => args = (s, e);
340+
341+
Assert.IsTrue(command.CanExecute(null));
342+
343+
command.Execute(null);
344+
345+
Assert.IsNull(args.Sender);
346+
Assert.IsNull(args.Args);
347+
348+
Assert.IsTrue(command.CanExecute(null));
349+
350+
tcs.SetResult(null);
351+
}
352+
353+
[TestMethod]
354+
public void Test_AsyncRelayCommand_ExecuteWithoutConcurrencyRaisesCanExecuteChanged()
355+
{
356+
TaskCompletionSource<object?> tcs = new();
357+
358+
AsyncRelayCommand command = new(() => tcs.Task, allowConcurrentExecutions: false);
359+
360+
(object? Sender, EventArgs? Args) args = default;
361+
362+
command.CanExecuteChanged += (s, e) => args = (s, e);
363+
364+
Assert.IsTrue(command.CanExecute(null));
365+
366+
command.Execute(null);
367+
368+
Assert.AreSame(command, args.Sender);
369+
Assert.AreSame(EventArgs.Empty, args.Args);
370+
371+
Assert.IsFalse(command.CanExecute(null));
372+
373+
tcs.SetResult(null);
374+
}
375+
376+
[TestMethod]
377+
public void Test_AsyncRelayCommand_ExecuteDoesNotRaiseCanExecuteChanged_WithCancellation()
378+
{
379+
TaskCompletionSource<object?> tcs = new();
380+
381+
AsyncRelayCommand command = new(token => tcs.Task, allowConcurrentExecutions: true);
382+
383+
(object? Sender, EventArgs? Args) args = default;
384+
385+
command.CanExecuteChanged += (s, e) => args = (s, e);
386+
387+
Assert.IsTrue(command.CanExecute(null));
388+
389+
command.Execute(null);
390+
391+
Assert.IsNull(args.Sender);
392+
Assert.IsNull(args.Args);
393+
394+
Assert.IsTrue(command.CanExecute(null));
395+
396+
tcs.SetResult(null);
397+
}
398+
399+
[TestMethod]
400+
public void Test_AsyncRelayCommand_ExecuteWithoutConcurrencyRaisesCanExecuteChanged_WithToken()
401+
{
402+
TaskCompletionSource<object?> tcs = new();
403+
404+
AsyncRelayCommand command = new(token => tcs.Task, allowConcurrentExecutions: false);
405+
406+
(object? Sender, EventArgs? Args) args = default;
407+
408+
command.CanExecuteChanged += (s, e) => args = (s, e);
409+
410+
Assert.IsTrue(command.CanExecute(null));
411+
412+
command.Execute(null);
413+
414+
Assert.AreSame(command, args.Sender);
415+
Assert.AreSame(EventArgs.Empty, args.Args);
416+
417+
Assert.IsFalse(command.CanExecute(null));
418+
419+
tcs.SetResult(null);
420+
}
328421
}

tests/CommunityToolkit.Mvvm.UnitTests/Test_AsyncRelayCommand{T}.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,95 @@ async void TestCallback(Action throwAction, Action completeAction)
223223

224224
Assert.IsTrue(success);
225225
}
226+
227+
public void Test_AsyncRelayCommand_ExecuteDoesNotRaiseCanExecuteChanged()
228+
{
229+
TaskCompletionSource<object?> tcs = new();
230+
231+
AsyncRelayCommand<string> command = new(s => tcs.Task, allowConcurrentExecutions: true);
232+
233+
(object? Sender, EventArgs? Args) args = default;
234+
235+
command.CanExecuteChanged += (s, e) => args = (s, e);
236+
237+
Assert.IsTrue(command.CanExecute(""));
238+
239+
command.Execute("");
240+
241+
Assert.IsNull(args.Sender);
242+
Assert.IsNull(args.Args);
243+
244+
Assert.IsTrue(command.CanExecute(""));
245+
246+
tcs.SetResult(null);
247+
}
248+
249+
[TestMethod]
250+
public void Test_AsyncRelayCommand_ExecuteWithoutConcurrencyRaisesCanExecuteChanged()
251+
{
252+
TaskCompletionSource<object?> tcs = new();
253+
254+
AsyncRelayCommand<string> command = new(s => tcs.Task, allowConcurrentExecutions: false);
255+
256+
(object? Sender, EventArgs? Args) args = default;
257+
258+
command.CanExecuteChanged += (s, e) => args = (s, e);
259+
260+
Assert.IsTrue(command.CanExecute(""));
261+
262+
command.Execute("");
263+
264+
Assert.AreSame(command, args.Sender);
265+
Assert.AreSame(EventArgs.Empty, args.Args);
266+
267+
Assert.IsFalse(command.CanExecute(""));
268+
269+
tcs.SetResult(null);
270+
}
271+
272+
[TestMethod]
273+
public void Test_AsyncRelayCommand_ExecuteDoesNotRaiseCanExecuteChanged_WithCancellation()
274+
{
275+
TaskCompletionSource<object?> tcs = new();
276+
277+
AsyncRelayCommand<string> command = new((s, token) => tcs.Task, allowConcurrentExecutions: true);
278+
279+
(object? Sender, EventArgs? Args) args = default;
280+
281+
command.CanExecuteChanged += (s, e) => args = (s, e);
282+
283+
Assert.IsTrue(command.CanExecute(""));
284+
285+
command.Execute("");
286+
287+
Assert.IsNull(args.Sender);
288+
Assert.IsNull(args.Args);
289+
290+
Assert.IsTrue(command.CanExecute(""));
291+
292+
tcs.SetResult(null);
293+
}
294+
295+
[TestMethod]
296+
public void Test_AsyncRelayCommand_ExecuteWithoutConcurrencyRaisesCanExecuteChanged_WithToken()
297+
{
298+
TaskCompletionSource<object?> tcs = new();
299+
300+
AsyncRelayCommand<string> command = new((s, token) => tcs.Task, allowConcurrentExecutions: false);
301+
302+
(object? Sender, EventArgs? Args) args = default;
303+
304+
command.CanExecuteChanged += (s, e) => args = (s, e);
305+
306+
Assert.IsTrue(command.CanExecute(""));
307+
308+
command.Execute("");
309+
310+
Assert.AreSame(command, args.Sender);
311+
Assert.AreSame(EventArgs.Empty, args.Args);
312+
313+
Assert.IsFalse(command.CanExecute(""));
314+
315+
tcs.SetResult(null);
316+
}
226317
}

0 commit comments

Comments
 (0)