来自异步工作人员的UWP更新UI

时间:2018-11-25 01:30:50

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

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

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

PART 1

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

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万行的文本文件上进行了测试)。我以为在async调用中添加awaitGetDataAsync()可以防止这种情况?除了UI线程外,这是否对线程不起作用?通过Visual Studio中的“调试”模式,我已验证程序正在按预期方式进行...它只是占用了UI线程,使应用程序无响应(请参阅this page from Microsoft about the UI thread and asynchronous programming)。

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

1 个答案:

答案 0 :(得分:3)

关键是要了解<svg viewBox="0 0 100 100"> <defs> <symbol id="A" viewBox="0 0 100 100"> <g> <g transform="translate(50 50) scale(0.69 0.69) rotate(0) translate(-50 -50)" > <svg viewBox="0 0 50 50" enable-background="new 0 0 50 50" xml:space="preserve"> <rect width="100" height="100" /> </svg> </g> </g> </symbol> <symbol id="B" viewBox="0 0 100 100"> <rect width="100" height="100" /> </symbol> </defs> <use xlink:href="#B" style="fill:rgb(0,0,0);stroke-width:3;stroke:rgb(0,0,0)" x="5" y="5" width="90" height="90"/> <use width="40" height="40" x="50" y="50" xlink:href="#B" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)"/> </svg>不会直接说等待的代码将在另一个线程上运行。当您执行async/await时,执行将继续在UI线程上输入await GetDataAsync(pickedFile);方法,然后继续执行直到到达GetDataAsync为止-这是 only 操作,实际上在另一个线程上异步运行(因为await file.OpenStreamForReadAsync()实际上是通过这种方式实现的。)

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

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

await

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

在异步调用上调用它可以告诉运行时执行不必返回到最初调用异步方法的线程-因此可以避免将执行返回给UI线程。适合您的情况的好地方是ConfigureAwait(false)OpenStreamForReadAsync呼叫:

ReadLineAsync

调度程序

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

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

Dispatcher

提前这样做的原因是private CoreDispatcher _dispatcher; public MyPage() { this.InitializeComponent(); _dispatcher = Window.Current.Dispatcher; ... } 再次只能从UI线程访问,但是页面构造函数肯定在那里运行,因此它是理想的使用位置。

现在如下重写Window.Current

UpdateUI