C#中Action<T>事件触发异步HTTP请求的任务取消问题排查
解决ViewModel间异步触发HTTP请求时的任务取消问题
问题根源分析
你遇到的"任务已取消"异常,核心原因在于同步事件体系与异步方法的不兼容,具体两点:
async void的滥用:事件处理方法InitializeSearchView使用async void,除原生UI事件外,这种写法会导致异步任务的上下文管理失控——异常无法正常冒泡、任务完成状态无法被跟踪,极容易引发线程死锁或任务被提前终止。- 同步事件无法传递异步状态:原代码用
Action<Project>作为事件类型,它是无返回值的同步委托,无法让事件触发者等待异步处理完成,可能导致UI上下文被提前释放,进而卡住HttpClient的请求流程。
可行解决方案(无需MVVM Toolkit)
方案一:改用异步事件体系(推荐)
将同步事件替换为支持异步的委托类型,从命令触发到事件处理全程遵循异步原则,彻底避免上下文冲突。
1. 修改ProjectsViewModel(事件发起方)
把Action<Project>事件改为Func<Project, Task>,同时使用异步命令(如Prism的AsyncRelayCommand,或自定义支持异步的RelayCommand):
public class ProjectsViewModel : BindableBase { public ICommand SelectProjectCommand { get; private set; } // 替换为异步委托类型,支持返回Task public event Func<Project, Task> SelectProjectRequested = delegate { return Task.CompletedTask; }; public ProjectsViewModel() { // 使用AsyncRelayCommand处理异步逻辑(若使用Prism);自定义RelayCommand需确保支持异步委托 SelectProjectCommand = new AsyncRelayCommand<Project>(SelectProject); } private async Task SelectProject(Project project) { // 触发异步事件并等待所有处理完成 await SelectProjectRequested(project); } }
2. 修改主ViewModel(事件处理方)
将事件处理方法改为async Task,去掉async void:
public class ViewModel { public ViewModel() { _projectsViewModel.SelectProjectRequested += InitializeSearchView; } // 改为async Task,让异步状态可跟踪 private async Task InitializeSearchView(Project project) { if (project != null) { await SearchViewModel.GetAllMaterialsForProjectAsync(project); } } }
方案二:应急修复async void写法(不推荐,仅适配旧代码)
如果无法修改事件类型,可通过调整异步上下文避免死锁,但需额外处理UI线程切换:
1. 修改主ViewModel的事件处理方法
private async void InitializeSearchView(Project project) { try { if (project != null) { // 使用ConfigureAwait(false)避免捕获UI上下文,防止死锁 await SearchViewModel.GetAllMaterialsForProjectAsync(project).ConfigureAwait(false); } } catch (Exception ex) { // async void的异常不会自动冒泡,需手动捕获并切回UI线程显示 Application.Current.Dispatcher.Invoke(() => MessageBox.Show(ex.Message)); } }
2. 修改SearchViewModel的HTTP请求逻辑
同样添加ConfigureAwait(false),并在更新UI时切回主线程:
public async Task GetAllMaterialsForProjectAsync(Project project) { HttpRequestMessage msg = new HttpRequestMessage { Method = HttpMethod.Get, RequestUri = new Uri(HttpClientInstance.BaseUri + "materials/get-materials-for-project/") }; msg.Headers.Add("projectID", $"{project.Id}"); string responseString = null; try { // 跳过UI上下文捕获,避免死锁 HttpResponseMessage responseMessage = await HttpClientInstance.httpClient.SendAsync(msg).ConfigureAwait(false); responseMessage.EnsureSuccessStatusCode(); responseString = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); } catch (Exception ex) { // 切回UI线程显示错误 Application.Current.Dispatcher.Invoke(() => MessageBox.Show(ex.Message)); } if (responseString != null) { List<WindowEntity> windows = DeserializeMaterials(responseString); // 切回UI线程更新ObservableCollection(UI绑定属性必须在主线程修改) Application.Current.Dispatcher.Invoke(() => { Windows = new ObservableCollection<WindowEntity>(windows); }); } }
额外注意事项
- 确保
HttpClientInstance是单例且未被提前释放,HttpClient的实例复用是最佳实践,频繁创建可能导致连接耗尽。 - 检查请求URI是否正确,网络环境是否正常,这些外部因素也可能导致"任务已取消"异常。
内容的提问来源于stack exchange,提问作者Ryan Daley
相关产品推荐
相关产品推荐

