异步任务阻塞 UI
async Task blocking UI
我有以下场景:
在我使用 MVVM 的 WPF 应用程序中(我对 MVVM 还很陌生)我有一个包含 DataGrid 的 Window。加载 Window 后,我想使用 Entity Framework 使用数据库中的条目填充 DataGrid。此外,某个列应该在编辑模式下使用 ComboBox,它也从 DB 中填充(多对多关系)。这些项目也应该在 Window 加载时加载。哦,是的,当然,这应该异步完成,这样数据库查询就不会阻塞 UI.
我阅读了 Stephen Cleary 的这些优秀博客文章:https://t1p.de/c12y8 and https://t1p.de/xkdh .
我选择了同步构造我的 ViewModel 并从 Window-Loaded 事件调用异步 Initialize
方法的方法。 Initialize
方法然后触发两个查询。
视图模型:
public class ViewModel : ViewModelBase
{
// this uses a slightly modified class from the first blog post
private NotifyTaskCompletion databaseAction;
public NotifyTaskCompletion DatabaseAction
{
get => databaseAction;
private set
{
databaseAction = value;
NotifyPropertyChanged();
}
}
public ViewModel()
{
// nothinc asynchronous going on here
}
public void Initialize()
{
DatabaseAction = new NotifyTaskCompletion(InitializeAsync());
}
private async Task InitializeAsync()
{
List<Task> tasks = new List<Task>();
tasks.Add(FirstQueryAsync());
tasks.Add(SecondQueryAsync());
await Task.WhenAll(tasks);
}
private async Task FirstQueryAsync()
{
using (var context = new SampleContext())
{
var query = await context.Beds.ToListAsync();
if (query.Count > 0)
{
beds = new ObservableCollection<Bed>();
query.ForEach(bed => beds.Add(bed));
}
else
{
LoadBedsFromFile(ref beds);
foreach (var bed in beds)
{
context.Beds.Add(bed);
}
await context.SaveChangesAsync();
}
}
}
private void LoadBedsFromFile(ref ObservableCollection<Bed> list)
{
if (File.Exists("Beds.xml"))
{
FileStream fs = new FileStream("Beds.xml", FileMode.Open);
XmlSerializer serializer = new XmlSerializer(typeof(ObservableCollection<Bed>));
list = (ObservableCollection<Bed>)serializer.Deserialize(fs);
fs.Close();
}
}
private async Task SecondQueryAsync()
{
using (var context = new SampleContext())
{
var query = await context.Samples.Where(...)
.Include(...)
.ToListAsync();
foreach (Sample item in query)
{
// each entry is put into a ViewModel itself
SampleViewModel vm = new SampleViewModel(item);
// sampleClass.Samples is an ObservableCollection
sampleClass.Samples.Add(vm);
}
}
}
Window:
private async void Window_Loaded(object sender, RoutedEventArgs e)
{
ViewModel vm = this.TryFindResource("viewModel") as ViewModel;
if (vm != null)
vm.Initialize();
}
}
问题来了:
UI 没有响应并且在初始化完成之前不会更新。即使我使用 Task.Run(() => vm.Initialize());
。
奇怪的是:如果我将 Task.Delay()
放入 InitializeAsync
方法中,加载动画会按预期显示,而 UI 是响应式的。例如,如果我将延迟放入 SecondQueryAsync
,UI 会冻结几秒钟,然后加载动画会在延迟期间旋转。
我怀疑这可能是创建 DbContext 的一些问题,但我无法确定这一点。
我最终解决了这个问题。
TheodorZoulias 的评论和他发布到 的 link 给了我解决方案的提示。
不幸的是,用 new NotifyTaskCompletion(Task.Run(() => InitializeAsync()));
替换 new NotifyTaskCompletion(InitializeAsync());
引发了其他问题,因为我不能简单地从该任务的上下文中修改 ObservableCollection
。
我真的很喜欢用async-await写代码。问题是当它遇到非异步代码时。
因为我怀疑创建 DbContext 的同步调用阻塞了线程。
这就是我解决问题的方法 - 也许对某人有帮助:
首先,我使用工厂方法异步创建 DbContext:
public class SampleContext : DbContext
{
private SampleContext() : base()
{
...
}
public static async Task<SampleContext> CreateAsync()
{
return await Task.Run(() => { return new SampleContext(); }).ConfigureAwait(false);
}
}
然后我重写了查询数据库的方法,它们不修改 ObservableCollection
本身而是 return 编辑了一个列表。
第二种方法将 return 值作为参数并修改 ObservableCollection
。这样我就可以使用 ConfigureAwait
来配置上下文。
private async Task<List<Sample>> SecondQueryAsync()
{
var context = await SampleContext.CreateAsync().ConfigureAwait(false);
var query = await context.Samples.Where(...)
.Include(...)
.ToListAsync().ConfigureAwait(false);
context.Dispose();
return query;
}
private async Task InitializeAsync()
{
List<Task> tasks = new List<Task>();
var firstTask = FirstQueryAsync();
var secondTask = SecondQueryAsync();
tasks.Add(firstTask);
tasks.Add(secondTask);
await Task.WhenAll(tasks);
if (tasks.Any(t => t.IsFaulted))
{
Initialized = false;
}
else
{
Initialized = true;
FillFirstList(firstTask.Result);
FillSecondList(secondTask.Result);
}
}
我有以下场景: 在我使用 MVVM 的 WPF 应用程序中(我对 MVVM 还很陌生)我有一个包含 DataGrid 的 Window。加载 Window 后,我想使用 Entity Framework 使用数据库中的条目填充 DataGrid。此外,某个列应该在编辑模式下使用 ComboBox,它也从 DB 中填充(多对多关系)。这些项目也应该在 Window 加载时加载。哦,是的,当然,这应该异步完成,这样数据库查询就不会阻塞 UI.
我阅读了 Stephen Cleary 的这些优秀博客文章:https://t1p.de/c12y8 and https://t1p.de/xkdh .
我选择了同步构造我的 ViewModel 并从 Window-Loaded 事件调用异步 Initialize
方法的方法。 Initialize
方法然后触发两个查询。
视图模型:
public class ViewModel : ViewModelBase
{
// this uses a slightly modified class from the first blog post
private NotifyTaskCompletion databaseAction;
public NotifyTaskCompletion DatabaseAction
{
get => databaseAction;
private set
{
databaseAction = value;
NotifyPropertyChanged();
}
}
public ViewModel()
{
// nothinc asynchronous going on here
}
public void Initialize()
{
DatabaseAction = new NotifyTaskCompletion(InitializeAsync());
}
private async Task InitializeAsync()
{
List<Task> tasks = new List<Task>();
tasks.Add(FirstQueryAsync());
tasks.Add(SecondQueryAsync());
await Task.WhenAll(tasks);
}
private async Task FirstQueryAsync()
{
using (var context = new SampleContext())
{
var query = await context.Beds.ToListAsync();
if (query.Count > 0)
{
beds = new ObservableCollection<Bed>();
query.ForEach(bed => beds.Add(bed));
}
else
{
LoadBedsFromFile(ref beds);
foreach (var bed in beds)
{
context.Beds.Add(bed);
}
await context.SaveChangesAsync();
}
}
}
private void LoadBedsFromFile(ref ObservableCollection<Bed> list)
{
if (File.Exists("Beds.xml"))
{
FileStream fs = new FileStream("Beds.xml", FileMode.Open);
XmlSerializer serializer = new XmlSerializer(typeof(ObservableCollection<Bed>));
list = (ObservableCollection<Bed>)serializer.Deserialize(fs);
fs.Close();
}
}
private async Task SecondQueryAsync()
{
using (var context = new SampleContext())
{
var query = await context.Samples.Where(...)
.Include(...)
.ToListAsync();
foreach (Sample item in query)
{
// each entry is put into a ViewModel itself
SampleViewModel vm = new SampleViewModel(item);
// sampleClass.Samples is an ObservableCollection
sampleClass.Samples.Add(vm);
}
}
}
Window:
private async void Window_Loaded(object sender, RoutedEventArgs e)
{
ViewModel vm = this.TryFindResource("viewModel") as ViewModel;
if (vm != null)
vm.Initialize();
}
}
问题来了:
UI 没有响应并且在初始化完成之前不会更新。即使我使用 Task.Run(() => vm.Initialize());
。
奇怪的是:如果我将 Task.Delay()
放入 InitializeAsync
方法中,加载动画会按预期显示,而 UI 是响应式的。例如,如果我将延迟放入 SecondQueryAsync
,UI 会冻结几秒钟,然后加载动画会在延迟期间旋转。
我怀疑这可能是创建 DbContext 的一些问题,但我无法确定这一点。
我最终解决了这个问题。
TheodorZoulias 的评论和他发布到
不幸的是,用 new NotifyTaskCompletion(Task.Run(() => InitializeAsync()));
替换 new NotifyTaskCompletion(InitializeAsync());
引发了其他问题,因为我不能简单地从该任务的上下文中修改 ObservableCollection
。
我真的很喜欢用async-await写代码。问题是当它遇到非异步代码时。 因为我怀疑创建 DbContext 的同步调用阻塞了线程。
这就是我解决问题的方法 - 也许对某人有帮助:
首先,我使用工厂方法异步创建 DbContext:
public class SampleContext : DbContext
{
private SampleContext() : base()
{
...
}
public static async Task<SampleContext> CreateAsync()
{
return await Task.Run(() => { return new SampleContext(); }).ConfigureAwait(false);
}
}
然后我重写了查询数据库的方法,它们不修改 ObservableCollection
本身而是 return 编辑了一个列表。
第二种方法将 return 值作为参数并修改 ObservableCollection
。这样我就可以使用 ConfigureAwait
来配置上下文。
private async Task<List<Sample>> SecondQueryAsync()
{
var context = await SampleContext.CreateAsync().ConfigureAwait(false);
var query = await context.Samples.Where(...)
.Include(...)
.ToListAsync().ConfigureAwait(false);
context.Dispose();
return query;
}
private async Task InitializeAsync()
{
List<Task> tasks = new List<Task>();
var firstTask = FirstQueryAsync();
var secondTask = SecondQueryAsync();
tasks.Add(firstTask);
tasks.Add(secondTask);
await Task.WhenAll(tasks);
if (tasks.Any(t => t.IsFaulted))
{
Initialized = false;
}
else
{
Initialized = true;
FillFirstList(firstTask.Result);
FillSecondList(secondTask.Result);
}
}