异步任务阻塞 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);
        }
}