UWP Update UI From Async Worker

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

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

Inside of the MyStorageWrapper class:

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

The code for the time check I got from following this.

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

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?

In a Page class:

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

And in the Generator class:

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

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

Upvotes: 2

Views: 3111

Answers (1)

Martin Zikmund
Martin Zikmund

Reputation: 39072

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).

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.

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).

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

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.

In the constructor get reference to the UI thread Dispatcher:

private CoreDispatcher _dispatcher;

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

    ...
}

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.

Now rewrite UpdateUI as follows

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

Upvotes: 5

Related Questions