在我的 MVVM 应用程序中,我的 View 模型调用了 3 种不同的服务方法,将每个方法的数据转换为通用格式,然后使用属性通知/可观察集合等更新 UI。
服务层中的每个方法都会启动一个新的Task
并将Task
返回给 View 模型。这是我的一种服务方法的示例。
public class ResourceService
{
internal static Task LoadResources(Action<IEnumerable<Resource>> completedCallback, Action<Exception> errorCallback)
{
var t = Task.Factory.StartNew(() =>
{
//... get resources from somewhere
return resources;
});
t.ContinueWith(task =>
{
if (task.IsFaulted)
{
errorCallback(task.Exception);
return;
}
completedCallback(task.Result);
}, TaskScheduler.FromCurrentSynchronizationContext());
return t;
}
}
这是 View 模型的调用代码和其他相关部分...
private ObservableCollection<DataItem> Data = new ObservableCollection<DataItem>();
public ICollectionView DataView
{
get { return _dataView; }
set
{
if (_dataView != value)
{
_dataView = value;
RaisePropertyChange(() => DataView);
}
}
}
private void LoadData()
{
SetBusy("Loading...");
Data.Clear();
Task[] tasks = new Task[3]
{
LoadTools(),
LoadResources(),
LoadPersonel()
};
Task.WaitAll(tasks);
DataView = CollectionViewSource.GetDefaultView(Data);
DataView.Filter = FilterTimelineData;
IsBusy = false;
}
private Task LoadResources()
{
return ResourceService.LoadResources(resources =>
{
foreach(var r in resources)
{
var d = convertResource(r);
Data.Add(d);
}
},
error =>
{
// do some error handling
});
}
这几乎可以工作,但有几个小问题。
第 1 条:在开始调用 SetBusy
时,在开始任何任务之前和调用 WaitAll
之前,我设置了 IsBusy
属性为真。这应该会更新 UI 并显示 BusyIndicator 控件,但它不起作用。我也试过添加简单的字符串属性并绑定(bind)它们,但它们也没有更新。 IsBusy 功能是基类的一部分,在我没有运行多个任务的其他 View 模型中工作,因此我认为 XAML 中的属性通知或数据绑定(bind)没有问题。
所有数据绑定(bind)似乎都在整个方法完成后更新。我在输出窗口中没有看到任何“第一次异常”或绑定(bind)错误,这让我相信 UI 线程在调用 WaitAll 之前以某种方式被阻塞。
第 2 点:我似乎从服务方法中返回了错误的任务。我希望 WaitAll
之后的所有内容在 View 模型转换回调中所有服务方法的所有结果后运行。但是,如果我从服务方法返回延续任务,则永远不会调用延续任务,WaitAll
将永远等待。奇怪的是绑定(bind)到 ICollectionView 的 UI 控件实际上正确地显示了所有内容,我认为这是因为数据是一个可观察的集合并且 CollectionViewSource 知道集合更改事件。
最佳答案
您可以使用 TaskFactory.ContinueWhenAll构建一个在输入任务全部完成时运行的延续。
Task[] tasks = new Task[3]
{
LoadTools(),
LoadResources(),
LoadPersonel()
};
Task.Factory.ContinueWhenAll(tasks, t =>
{
DataView = CollectionViewSource.GetDefaultView(Data);
DataView.Filter = FilterTimelineData;
IsBusy = false;
}, CancellationToken.None, TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
请注意,如果您使用 C# 5 的 await
,这会变得更简单/async
语法:
private async void LoadData()
{
SetBusy("Loading...");
Data.Clear();
Task[] tasks = new Task[3]
{
LoadTools(),
LoadResources(),
LoadPersonel()
};
await Task.WhenAll(tasks);
DataView = CollectionViewSource.GetDefaultView(Data);
DataView.Filter = FilterTimelineData;
IsBusy = false;
}
However if I return the continuation task from the service method the continuation never gets called and WaitAll waits forever
问题是您的延续任务需要 UI 线程,而您在 WaitAll
中阻塞了 UI 线程称呼。这会造成无法解决的死锁。
修复上述问题应该更正此问题 - 您需要将 Continuation 作为任务返回,因为这是您需要等待完成的内容 - 但通过使用 TaskFactory.ContinueWhenAll
您释放了 UI 线程,以便它可以处理这些延续。
请注意,这是 C# 5 简化的另一件事。您可以将其他方法编写为:
internal static async Task LoadResources(Action<IEnumerable<Resource>> completedCallback, Action<Exception> errorCallback)
{
try
{
await Task.Run(() =>
{
//... get resources from somewhere
return resources;
});
}
catch (Exception e)
{
errorCallback(task.Exception);
}
completedCallback(task.Result);
}
也就是说,编写返回 Task<T>
的方法通常更好。而不是提供回调,因为这简化了使用的两端。
关于c# - 如何在不阻塞 UI 线程的情况下在执行多个任务后继续?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18091780/