TaskCanceledException is the expected exception type when a Task that observes a CancellationToken is canceled. In the posted code, cancellation is already handled correctly inside the task (catch (OperationCanceledException)), but the outer await of _runningTask in StartButton_Click is missing. Because StartButton_Click is async void, any TaskCanceledException that bubbles out of _runningTask (or from Task.Run) can appear as an unhandled exception in the synchronization context.
To avoid the console output and keep cancellation fully controlled, use these patterns:
- Return
Task from the event handler and await _runningTask
Change the event handler signature and await the task so that cancellation is observed and handled:
private async Task StartButton_Click(object sender, RoutedEventArgs e)
{
if (_runningTask != null && !_runningTask.IsCompleted)
{
AppendLog("已有任务正在运行,请先取消");
return;
}
_cancellationTokenSource = new CancellationTokenSource();
var token = _cancellationTokenSource.Token;
StartButton.IsEnabled = false;
CancelButton.IsEnabled = true;
StatusText.Text = "任务运行中...";
StatusText.Foreground = System.Windows.Media.Brushes.Orange;
AppendLog("========== 开始执行任务 ==========");
_runningTask = Task.Run(async () =>
{
try
{
for (int i = 1; i <= 10; i++)
{
token.ThrowIfCancellationRequested();
await Task.Delay(500, token);
Dispatcher.Invoke(() =>
{
AppendLog($"步骤 {i}/10 完成");
});
}
Dispatcher.Invoke(() =>
{
StatusText.Text = "任务完成";
StatusText.Foreground = System.Windows.Media.Brushes.Green;
AppendLog("========== 任务成功完成 ==========");
});
}
catch (OperationCanceledException)
{
Dispatcher.Invoke(() =>
{
StatusText.Text = "任务已取消";
StatusText.Foreground = System.Windows.Media.Brushes.Red;
AppendLog("========== 任务已取消 ==========");
});
}
catch (Exception ex)
{
Dispatcher.Invoke(() =>
{
StatusText.Text = "任务出错";
StatusText.Foreground = System.Windows.Media.Brushes.Red;
AppendLog($"任务执行出错: {ex.Message}");
});
}
finally
{
Dispatcher.Invoke(() =>
{
StartButton.IsEnabled = true;
CancelButton.IsEnabled = false;
});
}
}, token);
try
{
await _runningTask; // Observe cancellation here
}
catch (OperationCanceledException)
{
// Already handled inside; swallow if desired
}
}
If the UI framework requires void for the event handler, keep async void but still await _runningTask inside the handler; the key is that the TaskCanceledException must be awaited and optionally caught.
- Do not pass the token to
Task.Run if cancellation is handled inside
Currently, Task.Run is called with token:
_runningTask = Task.Run(async () => { ... }, token);
This allows the task to be canceled before the delegate runs, causing Task.Run itself to throw TaskCanceledException. Since cancellation is already handled inside the delegate (ThrowIfCancellationRequested and Task.Delay(…, token)), remove the token from Task.Run:
_runningTask = Task.Run(async () =>
{
// same body as before
});
Then only the inner code throws OperationCanceledException, which is caught and handled, and _runningTask will complete in the Canceled state without surfacing an unhandled TaskCanceledException.
- General guidance from task cancellation pattern
The recommended pattern is:
- Use
CancellationTokenSource.Cancel to request cancellation.
- Inside the task, periodically call
token.ThrowIfCancellationRequested() or use APIs that accept the token (such as Task.Delay(…, token)).
- Catch
OperationCanceledException (or use ThrowIfCancellationRequested) so the task transitions to TaskStatus.Canceled instead of RanToCompletion.
- Ensure callers
await the task so that cancellation and exceptions are observed rather than treated as unhandled.
This matches the documented pattern for task cancellation and async return types: methods that can throw should return Task/Task<TResult> and be awaited, not void.
References: