来自异步工作人员的UWP更新UI [英] UWP Update UI From Async Worker

查看:76
本文介绍了来自异步工作人员的UWP更新UI的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试实施一个长期运行的后台进程,该进程会定期报告其进度,以更新UWP应用程序中的UI.我该怎么做?我看到了几个有用的主题,但是没有一个完整的主题,而且我无法将它们全部组合在一起.

I am trying to implement a long-running background process, that periodically reports on its progress, to update the UI in a UWP app. How can I accomplish this? I have seen several helpful topics, but none have all of the pieces, and I have been unable to put them all together.

例如,考虑一个用户选择了一个非常大的文件,而该应用正在读取文件中的数据和/或对其进行操作.用户单击一个按钮,该按钮将使用用户选择的文件中的数据填充页面上存储的列表.

For example, consider a user who picks a very large file, and the app is reading in and/or operating on the data in the file. The user clicks a button, which populates a list stored on the page with data from the file the user picks.

PART 1

页面和按钮的click事件处理程序如下所示:

The page and button's click event handler look something like this:

public sealed partial class MyPage : Page
{
    public List<DataRecord> DataRecords { get; set; }

    private DateTime LastUpdate;

    public MyPage()
    {
        this.InitializeComponent();

        this.DataRecords = new List<DataRecord>();
        this.LastUpdate = DateTime.Now;

        // Subscribe to the event handler for updates.
        MyStorageWrapper.MyEvent += this.UpdateUI;
    }

    private async void LoadButton_Click(object sender, RoutedEventArgs e)
    {
        StorageFile pickedFile = // … obtained from FileOpenPicker.

        if (pickedFile != null)
        {
            this.DataRecords = await MyStorageWrapper.GetDataAsync(pickedFile);
        }
    }

    private void UpdateUI(long lineCount)
    {
        // This time check prevents the UI from updating so frequently
        //    that it becomes unresponsive as a result.
        DateTime now = DateTime.Now;
        if ((now - this.LastUpdate).Milliseconds > 3000)
        {
            // This updates a textblock to display the count, but could also
            //    update a progress bar or progress ring in here.
            this.MessageTextBlock.Text = "Count: " + lineCount;

            this.LastUpdate = now;
        }
    }
}

MyStorageWrapper类内部:

public static class MyStorageWrapper
{
    public delegate void MyEventHandler(long lineCount);
    public static event MyEventHandler MyEvent;

    private static void RaiseMyEvent(long lineCount)
    {
        // Ensure that something is listening to the event.
        if (MyStorageWrapper.MyEvent!= null)
        {
            // Call the listening event handlers.
            MyStorageWrapper.MyEvent(lineCount);
        }
    }

    public static async Task<List<DataRecord>> GetDataAsync(StorageFile file)
    {
        List<DataRecord> recordsList = new List<DataRecord>();

        using (Stream stream = await file.OpenStreamForReadAsync())
        {
            using (StreamReader reader = new StreamReader(stream))
            {
                while (!reader.EndOfStream)
                {
                    string line = reader.ReadLine();

                    // Does its parsing here, and constructs a single DataRecord …

                    recordsList.Add(dataRecord);

                    // Raises an event.
                    MyStorageWrapper.RaiseMyEvent(recordsList.Count);
                }
            }
        }

        return recordsList;
    }
}

我从以下按照编写的方式,此代码使应用程序无法响应大文件(我在大约850万行的文本文件上进行了测试).我以为在GetDataAsync()调用中添加asyncawait可以防止这种情况?除了UI线程外,这是否对线程不起作用?通过Visual Studio中的调试"模式,我已验证程序正在按预期方式进行...它只是占用了UI线程,使应用程序无响应(请参阅

As written, this code makes the app unresponsive with a large file (I tested on a text file on the order of about 8.5 million lines). I thought adding async and await to the GetDataAsync() call would prevent this? Does this not do its work on a thread aside from the UI thread? Through Debug mode in Visual Studio, I have verified the program is progressing as expected... it is just tying up the UI thread, making the app unresponsive (see this page from Microsoft about the UI thread and asynchronous programming).

PART 2

我已经成功地实现了在单独线程上运行的异步,长期运行的进程之前,并且仍会定期更新UI ...但是此解决方案不允许返回值-特别是第1部分中的行:

I have successfully implemented before an asynchronous, long-running process that runs on a separate thread AND still updates the UI periodically... but this solution does not allow for the return value - specifically the line from PART 1 that says:

this.DataRecords = await MyStorageWrapper.GetDataAsync(pickedFile);

接下来是我先前成功的实现(为简洁起见,大多数主体被删去了).有没有办法对此进行调整以允许返回值?

My previous, successful implementation follows (most of the bodies cut out for brevity). Is there a way to adapt this to allow for return values?

Page类中:

public sealed partial class MyPage : Page
{
    public Generator MyGenerator { get; set; }

    public MyPage()
    {
        this.InitializeComponent();

        this.MyGenerator = new Generator();
    }

    private void StartButton_Click(object sender, RoutedEventArgs e)
    {
        this.MyGenerator.ProgressUpdate += async (s, f) => await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, delegate ()
        {
            // Updates UI elements on the page from here.
        }

        this.MyGenerator.Start();
    }

    private void StopButton_Click(object sender, RoutedEventArgs e)
    {
        this.MyGenerator.Stop();
    }
}

Generator类中:

public class Generator
{
    private CancellationTokenSource cancellationTokenSource;

    public event EventHandler<GeneratorStatus> ProgressUpdate;

    public Generator()
    {
        this.cancellationTokenSource = new CancellationTokenSource();
    }

    public void Start()
    {
        Task task = Task.Run(() =>
        {
            while(true)
            {
                // Throw an Operation Cancelled exception if the task is cancelled.
                this.cancellationTokenSource.Token.ThrowIfCancellationRequested();

                // Does stuff here.

                // Finally raise the event (assume that 'args' is the correct args and datatypes).
                this.ProgressUpdate.Raise(this, new GeneratorStatus(args));
            }
        }, this.cancellationTokenSource.Token);
    }

    public void Stop()
    {
        this.cancellationTokenSource.Cancel();
    }
}

最后,ProgressUpdate事件有两个支持类:

Finally, there are two supporting classes for the ProgressUpdate event:

public class GeneratorStatus : EventArgs
{
    // This class can contain a handful of properties; only one shown.
    public int number { get; private set; }

    public GeneratorStatus(int n)
    {
        this.number = n;
    }
}

static class EventExtensions
{
    public static void Raise(this EventHandler<GeneratorStatus> theEvent, object sender, GeneratorStatus args)
    {
        theEvent?.Invoke(sender, args);
    }
}

推荐答案

关键是要了解async/await不会直接说等待的代码将在其他线程上运行.当您执行await GetDataAsync(pickedFile);时,执行将仍然在UI线程上输入GetDataAsync方法,并继续执行直到达到await file.OpenStreamForReadAsync()-这是 only 操作,实际上将在其他线程上异步运行线程(因为file.OpenStreamForReadAsync实际上是通过这种方式实现的.)

It is key to understand that async/await does not directly say the awaited code will run on a different thread. When you do await GetDataAsync(pickedFile); the execution enters the GetDataAsync method still on the UI thread and continues there until await file.OpenStreamForReadAsync() is reached - and this is the only operation that will actually run asynchronously on a different thread (as file.OpenStreamForReadAsync is actually implemented this way).

但是,一旦OpenStreamForReadAsync完成(这将是非常快的),await确保执行返回到它在其上开始的同一线程-这意味着 UI线程.因此,您代码的实际昂贵部分(在while中读取文件)在UI线程上运行.

However, once OpenStreamForReadAsync is completed (which will be really quick), await makes sure the execution returns to the same thread it started on - which means UI thread. So the actual expensive part of your code (reading the file in while) runs on UI thread.

您可以通过使用reader.ReadLineAsync稍微改善这一点,但仍然会在每个await之后返回UI线程.

You could marginally improve this by using reader.ReadLineAsync, but still, you will be returning to UI thread after each await.

您要介绍的解决此问题的第一个技巧是ConfigureAwait(false).

The first trick you want to introduce to resolve this problem is ConfigureAwait(false).

在异步调用上调用它可以告诉运行时执行不必返回到最初调用异步方法的线程-因此可以避免将执行返回给UI线程.放置在您的情况下的好地方是OpenStreamForReadAsyncReadLineAsync调用:

Calling this on an asynchronous call tells the runtime that the execution does not have to return to the thread that originally called the asynchronous method - hence this can avoid returning execution to the UI thread. Great place to put it in your case is OpenStreamForReadAsync and ReadLineAsync calls:

public static async Task<List<DataRecord>> GetDataAsync(StorageFile file)
{
    List<DataRecord> recordsList = new List<DataRecord>();

    using (Stream stream = await file.OpenStreamForReadAsync().ConfigureAwait(false))
    {
        using (StreamReader reader = new StreamReader(stream))
        {
            while (!reader.EndOfStream)
            {
                string line = await reader.ReadLineAsync().ConfigureAwait(false);

                // Does its parsing here, and constructs a single DataRecord …

                recordsList.Add(dataRecord);

                // Raises an event.
                MyStorageWrapper.RaiseMyEvent(recordsList.Count);
            }
        }
    }

    return recordsList;
}

调度程序

现在,您释放了UI线程,但是进度报告又引入了另一个问题.因为现在MyStorageWrapper.RaiseMyEvent(recordsList.Count)在不同的线程上运行,所以您不能直接在UpdateUI方法中更新UI ,因为从非UI线程访问UI元素会引发同步异常.而是必须使用UI线程Dispatcher来确保代码在正确的线程上运行.

Dispatcher

Now you freed up your UI thread, but introduced yet another problem with the progress reporting. Because now MyStorageWrapper.RaiseMyEvent(recordsList.Count) runs on a different thread, you cannot update the UI in the UpdateUI method directly, as accessing UI elements from non-UI thread throws synchronization exception. Instead, you must use UI thread Dispatcher to make sure the code runs on the right thread.

在构造函数中获取对UI线程Dispatcher的引用:

In the constructor get reference to the UI thread Dispatcher:

private CoreDispatcher _dispatcher;

public MyPage()
{
    this.InitializeComponent();
    _dispatcher = Window.Current.Dispatcher;

    ...
}

提前这样做的原因是,再次只能从UI线程访问Window.Current,但是页面构造函数肯定在此运行,因此它是理想的使用位置.

Reason to do it ahead is that Window.Current is again accessible only from the UI thread, but the page constructor definitely runs there, so it is the ideal place to use.

现在如下重写UpdateUI

private async void UpdateUI(long lineCount)
{
    await _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
       // This time check prevents the UI from updating so frequently
       //    that it becomes unresponsive as a result.
       DateTime now = DateTime.Now;
       if ((now - this.LastUpdate).Milliseconds > 3000)
       {
           // This updates a textblock to display the count, but could also
           //    update a progress bar or progress ring in here.
           this.MessageTextBlock.Text = "Count: " + lineCount;

           this.LastUpdate = now;
       }
    });
}

这篇关于来自异步工作人员的UWP更新UI的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆