Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Show a modal UI in the middle of background operation and continue

I have a WPF application running a background task which uses async/await. The task is updating the app's status UI as it progresses. During the process, if a certain condition has been met, I am required to show a modal window to make the user aware of such event, and then continue processing, now also updating the status UI of that modal window.

This is a sketch version of what I am trying to achieve:

async Task AsyncWork(int n, CancellationToken token)
{
    // prepare the modal UI window
    var modalUI = new Window();
    modalUI.Width = 300; modalUI.Height = 200;
    modalUI.Content = new TextBox();

    using (var client = new HttpClient())
    {
        // main loop
        for (var i = 0; i < n; i++)
        {
            token.ThrowIfCancellationRequested();

            // do the next step of async process
            var data = await client.GetStringAsync("http://www.bing.com/search?q=item" + i);

            // update the main window status
            var info = "#" + i + ", size: " + data.Length + Environment.NewLine;
            ((TextBox)this.Content).AppendText(info); 

            // show the modal UI if the data size is more than 42000 bytes (for example)
            if (data.Length < 42000)
            {
                if (!modalUI.IsVisible)
                {
                    // show the modal UI window 
                    modalUI.ShowDialog();
                    // I want to continue while the modal UI is still visible
                }
            }

            // update modal window status, if visible
            if (modalUI.IsVisible)
                ((TextBox)modalUI.Content).AppendText(info);
        }
    }
}

The problem with modalUI.ShowDialog() is that it is a blocking call, so the processing stops until the dialog is closed. It would not be a problem if the window was modeless, but it has to be modal, as dictated by the project requirements.

Is there a way to get around this with async/await?

like image 598
avo Avatar asked Jan 04 '14 06:01

avo


People also ask

How do I freeze the background in modal pop up?

A simple solution would be to add a <div> that covers the background, and is positioned below the popup but above all other content.

What is modal in UI?

Modals (also known as modal windows, overlays, and dialogs) are large UI elements that sit on top of an application's main window—often with a layer of transparency behind them to give users a peek into the main app. To return to the application's main interface, users must interact with the modal layer.

What is the difference between a modal and a dialog?

Dialog (dialogue): A conversation between two people. In a user interface, a dialog is a “conversation” between the system and the user. Mode: A special state of the system in which the same system has somewhat different user interfaces.

What is a modal dialogue box?

Definition: A modal dialog is a dialog that appears on top of the main content and moves the system into a special mode requiring user interaction. This dialog disables the main content until the user explicitly interacts with the modal dialog.


1 Answers

This can be achieved by executing modalUI.ShowDialog() asynchronously (upon a future iteration of the UI thread's message loop). The following implementation of ShowDialogAsync does that by using TaskCompletionSource (EAP task pattern) and SynchronizationContext.Post.

Such execution workflow might be a bit tricky to understand, because your asynchronous task is now spread across two separate WPF message loops: the main thread's one and the new nested one (started by ShowDialog). IMO, that's perfectly fine, we're just taking advantage of the async/await state machine provided by C# compiler.

Although, when your task comes to the end while the modal window is still open, you probably want to wait for user to close it. That's what CloseDialogAsync does below. Also, you probably should account for the case when user closes the dialog in the middle of the task (AFAIK, a WPF window can't be reused for multiple ShowDialog calls).

The following code works for me:

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace WpfAsyncApp
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.Content = new TextBox();
            this.Loaded += MainWindow_Loaded;
        }

        // AsyncWork
        async Task AsyncWork(int n, CancellationToken token)
        {
            // prepare the modal UI window
            var modalUI = new Window();
            modalUI.Width = 300; modalUI.Height = 200;
            modalUI.Content = new TextBox();

            try
            {
                using (var client = new HttpClient())
                {
                    // main loop
                    for (var i = 0; i < n; i++)
                    {
                        token.ThrowIfCancellationRequested();

                        // do the next step of async process
                        var data = await client.GetStringAsync("http://www.bing.com/search?q=item" + i);

                        // update the main window status
                        var info = "#" + i + ", size: " + data.Length + Environment.NewLine;
                        ((TextBox)this.Content).AppendText(info);

                        // show the modal UI if the data size is more than 42000 bytes (for example)
                        if (data.Length < 42000)
                        {
                            if (!modalUI.IsVisible)
                            {
                                // show the modal UI window asynchronously
                                await ShowDialogAsync(modalUI, token);
                                // continue while the modal UI is still visible
                            }
                        }

                        // update modal window status, if visible
                        if (modalUI.IsVisible)
                            ((TextBox)modalUI.Content).AppendText(info);
                    }
                }

                // wait for the user to close the dialog (if open)
                if (modalUI.IsVisible)
                    await CloseDialogAsync(modalUI, token);
            }
            finally
            {
                // always close the window
                modalUI.Close();
            }
        }

        // show a modal dialog asynchronously
        static async Task ShowDialogAsync(Window window, CancellationToken token)
        {
            var tcs = new TaskCompletionSource<bool>();
            using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: true))
            {
                RoutedEventHandler loadedHandler = (s, e) =>
                    tcs.TrySetResult(true);

                window.Loaded += loadedHandler;
                try
                {
                    // show the dialog asynchronously 
                    // (presumably on the next iteration of the message loop)
                    SynchronizationContext.Current.Post((_) => 
                        window.ShowDialog(), null);
                    await tcs.Task;
                    Debug.Print("after await tcs.Task");
                }
                finally
                {
                    window.Loaded -= loadedHandler;
                }
            }
        }

        // async wait for a dialog to get closed
        static async Task CloseDialogAsync(Window window, CancellationToken token)
        {
            var tcs = new TaskCompletionSource<bool>();
            using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: true))
            {
                EventHandler closedHandler = (s, e) =>
                    tcs.TrySetResult(true);

                window.Closed += closedHandler;
                try
                {
                    await tcs.Task;
                }
                finally
                {
                    window.Closed -= closedHandler;
                }
            }
        }

        // main window load event handler
        async void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            var cts = new CancellationTokenSource(30000);
            try
            {
                // test AsyncWork
                await AsyncWork(10, cts.Token);
                MessageBox.Show("Success!");
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString());
            }
        }
    }
}

[EDITED] Below is a slightly different approach which uses Task.Factory.StartNew to invoke modalUI.ShowDialog() asynchronously. The returned Task can be awaited later to make sure the user has closed the modal dialog.

async Task AsyncWork(int n, CancellationToken token)
{
    // prepare the modal UI window
    var modalUI = new Window();
    modalUI.Width = 300; modalUI.Height = 200;
    modalUI.Content = new TextBox();

    Task modalUITask = null;

    try
    {
        using (var client = new HttpClient())
        {
            // main loop
            for (var i = 0; i < n; i++)
            {
                token.ThrowIfCancellationRequested();

                // do the next step of async process
                var data = await client.GetStringAsync("http://www.bing.com/search?q=item" + i);

                // update the main window status
                var info = "#" + i + ", size: " + data.Length + Environment.NewLine;
                ((TextBox)this.Content).AppendText(info);

                // show the modal UI if the data size is more than 42000 bytes (for example)
                if (data.Length < 42000)
                {
                    if (modalUITask == null)
                    {
                        // invoke modalUI.ShowDialog() asynchronously
                        modalUITask = Task.Factory.StartNew(
                            () => modalUI.ShowDialog(),
                            token,
                            TaskCreationOptions.None,
                            TaskScheduler.FromCurrentSynchronizationContext());

                        // continue after modalUI.Loaded event 
                        var modalUIReadyTcs = new TaskCompletionSource<bool>();
                        using (token.Register(() => 
                            modalUIReadyTcs.TrySetCanceled(), useSynchronizationContext: true))
                        {
                            modalUI.Loaded += (s, e) =>
                                modalUIReadyTcs.TrySetResult(true);
                            await modalUIReadyTcs.Task;
                        }
                    }
                }

                // update modal window status, if visible
                if (modalUI.IsVisible)
                    ((TextBox)modalUI.Content).AppendText(info);
            }
        }

        // wait for the user to close the dialog (if open)
        if (modalUITask != null)
            await modalUITask;
    }
    finally
    {
        // always close the window
        modalUI.Close();
    }
}
like image 144
noseratio Avatar answered Sep 21 '22 11:09

noseratio