简体   繁体   English

异步任务阻塞 UI

[英]async Task blocking UI

I have the following scenario: In my WPF app using MVVM (I'm fairly new to MVVM) I have a Window that contains a DataGrid.我有以下情况:在我使用 MVVM 的 WPF 应用程序中(我对 MVVM 还很陌生)我有一个包含 DataGrid 的 Window。 When the Window is loaded I want to fill the DataGrid with entries from a Database using Entity Framework.加载 Window 时,我想使用实体框架使用数据库中的条目填充 DataGrid。 Additionally a certain column should use a ComboBox in edit mode which is also filled from the DB (many-to-many relation).此外,某个列应在编辑模式下使用 ComboBox,该列也从数据库中填充(多对多关系)。 These items should also be loaded when the Window loads.这些项目也应该在 Window 加载时加载。 Oh yes and, of course, this should be done async so that the database queries do not block the UI.哦,是的,当然,这应该异步完成,这样数据库查询就不会阻塞 UI。

I've read these excellent blog posts by Stephen Cleary: https://t1p.de/c12y8 and https://t1p.de/xkdh .我读过 Stephen Cleary 的这些优秀博客文章: https://t1p.de/c12y8https://t1p.de/xkdh

I have chosen the approach that my ViewModel is constructed synchronously and an asynchronous Initialize method is called from the Window-Loaded event.我选择了同步构造我的 ViewModel 并从 Window-Loaded 事件调用异步Initialize方法的方法。 The Initialize method then triggers the two queries. Initialize方法然后触发两个查询。

The ViewModel:视图模型:

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);
                }
            }
        }

The Window: Window:

private async void Window_Loaded(object sender, RoutedEventArgs e)
{
            ViewModel vm = this.TryFindResource("viewModel") as ViewModel;
            if (vm != null)
                vm.Initialize();
            }
}

Now here is the Issue: The UI is unresponsive and does not update until the initialization is finished.现在的问题是:UI 没有响应,并且在初始化完成之前不会更新。 Even if I use Task.Run(() => vm.Initialize());即使我使用Task.Run(() => vm.Initialize()); . . The strange thing ist this: If I put a Task.Delay() into the InitializeAsync method, the loading animation ist shown as intended und the UI is responsive.奇怪的是:如果我将Task.Delay()放入InitializeAsync方法中,加载 animation 会按预期显示,并且 UI 会响应。 If i put the Delay into SecondQueryAsync for example the UI freezes for a few seconds and afterwards the loading animation rotates for the duration of the delay.例如,如果我将 Delay 放入SecondQueryAsync中,UI 会冻结几秒钟,然后加载 animation 在延迟期间旋转。 I suspect this might be some issue with creating the DbContext but i cannot pinpoint this.我怀疑这可能是创建 DbContext 的一些问题,但我无法查明这一点。

I eventually solved this problem.我最终解决了这个问题。 TheodorZoulias' comment and the link he posted to await Task.Run vs await gave me a hint towards the solution. TheodorZoulias 的评论和他发布的await Task.Run vs await链接给了我解决方案的提示。
Replacing the new NotifyTaskCompletion(InitializeAsync());替换new NotifyTaskCompletion(InitializeAsync()); with new NotifyTaskCompletion(Task.Run(() => InitializeAsync()));使用new NotifyTaskCompletion(Task.Run(() => InitializeAsync())); unfortunately raised other problems because I could not simply modify the ObservableCollection from that Task's context.不幸的是引发了其他问题,因为我不能简单地从该任务的上下文中修改ObservableCollection

I really like writing code with async-await.我真的很喜欢用 async-await 编写代码。 The problem is when it hits code that is not async.问题是当它遇到非异步代码时。 As I suspected the syncronous call to create the DbContext was blocking the thread.因为我怀疑创建 DbContext 的同步调用阻塞了线程。

So this is how I solved it - maybe it helps someone:所以这就是我解决它的方法 - 也许它可以帮助某人:

First I used a factory method to create the DbContext asynchronous:首先,我使用工厂方法来异步创建 DbContext:

public class SampleContext : DbContext
{
        private SampleContext() : base()
        {
            ...
        }

        public static async Task<SampleContext> CreateAsync()
        {
            return await Task.Run(() => { return new SampleContext(); }).ConfigureAwait(false);
        }
}

Then I rewrote the methods that query the database in a way that they do not modify the ObservableCollection themselves but returned a list instead.然后我重写了查询数据库的方法,使它们不修改ObservableCollection本身而是返回一个列表。 A second method takes the return value as a parameter and modifies the ObservableCollection .第二种方法将返回值作为参数并修改ObservableCollection That way I can use ConfigureAwait to configure the context.这样我就可以使用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);
        }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM