【问题标题】:UWP Update UI From Async Worker来自异步工作器的 UWP 更新 UI
【发布时间】:2019-04-27 02:01:01
【问题描述】:

我正在尝试实现一个长时间运行的后台进程,它会定期报告其进度,以更新 UWP 应用中的 UI。我怎样才能做到这一点?我看到了几个有用的主题,但没有一个包含所有部分,我无法将它们全部放在一起。

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

第 1 部分

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

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

我从关注this获得的时间检查代码。

如所写,此代码使应用程序对大文件无响应(我在大约 850 万行的文本文件上进行了测试)。我认为将asyncawait 添加到GetDataAsync() 调用会阻止这种情况?这不是在 UI 线程之外的线程上工作吗?通过 Visual Studio 中的调试模式,我已验证程序正在按预期进行......它只是占用了 UI 线程,使应用程序无响应(请参阅this page from Microsoft about the UI thread and asynchronous programming)。

第 2 部分

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

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

我之前的成功实施如下(为简洁起见,大部分正文都被删掉了)。有没有办法调整它以允许返回值?

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 事件有两个支持类:

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

【问题讨论】:

    标签: c# multithreading user-interface uwp async-await


    【解决方案1】:

    了解async/await 并没有直接说等待的代码将在不同的线程上运行是关键。当您执行 await GetDataAsync(pickedFile); 时,执行仍会在 UI 线程上进入 GetDataAsync 方法并继续在那里直到达到 await file.OpenStreamForReadAsync() - 这是 only 操作实际上将在不同的线程上异步运行线程(因为file.OpenStreamForReadAsync实际上是这样实现的)。

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

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

    ConfigureAwait(false)

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

    在异步调用上调用 this 会告诉运行时执行不必返回到最初调用异步方法的线程 - 因此这可以避免将执行返回到 UI 线程。将它放在您的案例中的好地方是 OpenStreamForReadAsyncReadLineAsync 调用:

    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 来确保代码在正确的线程上运行。

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

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

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

    现在重写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;
           }
        });
    }
    

    【讨论】:

    • 我实现了这个并且效果很好!然而,一个小的编辑:Window.Current.Dispatcher 的数据类型是 CoreDispatcher,而不是 Dispatcher(也许这是版本或其他东西的变化)
    • 你是对的,我的错:-)。我写那部分主要是凭记忆:-D。很高兴它有帮助,编码愉快!
    • UpdateUI 内部有一个await。这意味着一个线程被阻塞,直到返回RunAsync。这可能会引入死锁。但在这种情况下,这样做是没有意义的。有可能在构造函数中获取SynchronizationContext.Current 并使用它的Post 方法来实现fire-and-forget。不过,我不知道是否可以将CoreDispatcherPost 结合起来。
    猜你喜欢
    • 1970-01-01
    • 2015-07-26
    • 1970-01-01
    • 2016-06-01
    • 2018-09-30
    • 2017-01-21
    • 1970-01-01
    • 2020-11-13
    • 2013-06-08
    相关资源
    最近更新 更多