2

I have two default windows. I want one window starts a work, shows another window in the modal (dialog) form (progress indicating, but now it is not important), then closes it after this work has been finished. I have the following problems in my implementation:

1) after the work completes ("Completed!" message box shows up, but it also is not important and is just indication), the ProgressWindow doesn't close automatically;

2) if I close it by clicking manually the red cross, the System.InvalidOperationException with the message "The calling thread cannot access this object because a different thread owns it." occurs at the line

await task;

3) the code in ContinueWith actually is performed BEFORE the Go method finishes - why?

How can I achieve such a behavior?

My implementation:

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

        async void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            Window w = new ProgressWindow();

            var task = 
                Task
                .Run(() => Go())
                .ContinueWith(completedTask => w.Close());
            w.ShowDialog();
            await task; // InvalidOperationException throws
        }

        async protected void Go()
        {
            await Task.Delay(500); // imitate some work    
            MessageBox.Show("Completed!"); // indicate that work has been completed
        }
    }
}
5
  • Doesn't ShowDialog block? How would you ever know the task completed without the user closing the dialog anyway? I'm thinking you need to pass the task to the dialog as a parameter and await it somewhere there. The window can close itself when the task is completed.
    – Kenneth K.
    Commented Jan 20, 2018 at 16:15
  • I don't understand what do you mean "pass the task to the dialog". Performing the task somewhere in dialog's code behind?.. But if I do backwards - pass the dialog to the task - I can achieve required behavior.
    – Alex34758
    Commented Jan 20, 2018 at 16:31
  • Actually, I think your approach--though it does not work according to you--is fine. I probably should have put my comment on Peter Bons post. I was flipping back and forth between your code and his.
    – Kenneth K.
    Commented Jan 20, 2018 at 16:33
  • I also propose my own solution in which I achieve the required behavior.
    – Alex34758
    Commented Jan 20, 2018 at 16:36
  • @KennethK. my approach in question is not good also because the code in ContinueWith is actually performed before the Go finishes. It is something strange and I don't know why.
    – Alex34758
    Commented Jan 20, 2018 at 17:01

6 Answers 6

4

There is no need to use a continuation here, just stick to await. Not only that, but because you used async void the program did not wait half a second before closing the window.

Also, there is really no benefit in this case to use Task.Run since Go already can be made awaitable.

The improved and simplified code is:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    async void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        Window w = new ProgressWindow();
        Task work = Go(w);
        w.ShowDialog();
        await work; // exceptions in unawaited task are difficult to handle, so let us await it here.
    }

    async Task Go(Window w)
    {
        await Task.Delay(500);    
        w.Close();
    }
}

The reason you got the error was that (the continuation of) the Task created by Task.Run executes on a non-UI thread and it is not allowed to access the UI (w.Close();) in a non-UI thread.

If you have work that benefits from Task.Run you can modify the Go() method like this:

async Task Go(Window w)
{
    await Task.Run(() => { 
        // ... heavy work here  
    });    
    w.Close();
}
8
  • In your first example after calling w.ShowDialog() nothing is happened before I close the ProgressWindow window. Only after that Go is performed.
    – Alex34758
    Commented Jan 20, 2018 at 15:59
  • Your second example has the same behavior.
    – Alex34758
    Commented Jan 20, 2018 at 16:04
  • I see, I overlooked the fact that the window is opened as a dialog, therefore blocking the start of the task. Is it acceptable to open it as a non-dialog window? See my updated answer
    – Peter Bons
    Commented Jan 20, 2018 at 16:46
  • No, the dialog form is required, because I want to block a user from any activity before the operation has completed. But thank you for your help anyway.
    – Alex34758
    Commented Jan 20, 2018 at 16:51
  • 1
    That is correct, you see, if you use Task.Run you create a background thread. But say you are doing I/O bound operations like accessing files or a network using a native async operation that there is really no need to use Task.Run. See this blogpost serie. Using await Task.Delay(xxx) for example does not block the UI thread so Task.Run is not needed.
    – Peter Bons
    Commented Jan 20, 2018 at 17:49
1

Here is an extension method ShowDialogUntilTaskCompletion that can be used in place of the built-in ShowDialog. It closes automatically the window when the supplied Task is completed. In case the task completes with an exception, the window is still closed, and the method returns without throwing.

It's possible to prevent the user from closing the window (and causing the method to return before the task is complete) by handling the Closing event on the window and setting the CancelEventArgs.Cancel property to true. The method unsubscribes from the event before calling Window.Close() method otherwise that would get cancelled too.

public static class WindowExtensions
{
    public static bool? ShowDialogUntilTaskCompletion(this Window window,
        Task task, int minDurationMsec = 500)
    {
        ArgumentNullException.ThrowIfNull(window);
        ArgumentNullException.ThrowIfNull(task);
        if (minDurationMsec < 0)
            throw new ArgumentOutOfRangeException(nameof(minDurationMsec));

        Task closeDelay = Task.Delay(minDurationMsec);
        window.Closing += PreventUserClosing;
        HandleTaskCompletion();
        return window.ShowDialog();

        async void HandleTaskCompletion()
        {
            try
            {
                await Task.Yield(); // Ensure that the completion is asynchronous
                await task;
            }
            catch { } // Ignore exception
            finally
            {
                await closeDelay;
                window.Closing -= PreventUserClosing;
                window.Close();
            }
        }

        void PreventUserClosing(object sender, CancelEventArgs e)
        {
            e.Cancel = true;
        }
    }
}

As a bonus it also accepts a minDurationMsec parameter, to keep the window visible for the minimum duration specified (so that the window is not closed in the blink of an eye).

Usage example:

async void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    Window w = new ProgressWindow();
    Task task = Task.Delay(2000); // Simulate some asynchronous work
    w.ShowDialogUntilTaskCompletion(task);
    try
    {
        // Most likely the task will be completed at this point
        await task;
    }
    catch (Exception ex)
    {
        // Handle the case of a faulted task
    }
}
0
0

I read the Peter Bons's answer and rethink it - his approach uses the asynchronous Go method (as it was in my question of course) and then awaits its result without Task.Run. Another variant which I came to is based on the answers of Peter Bons and Andrew Shkolik. I call a synchronous method Go asynchronously through Task.Run and use Dispatcher to manipulate the window.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    async void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        Window w = new ProgressWindow();
        Task work = Task.Run(() => Go(w));
        w.ShowDialog();
        await work;
    }

    void Go(Window w)
    {
        Thread.Sleep(2000); // imitate some work
        Dispatcher.BeginInvoke(
            new Action(() =>
            {
                w.Close();
            }));
    }
}
0

Maybe a bit late but here is a fully working solution based on TPL an IProgress

public partial class ProgressDlg<T> : Form where T : class
{
    public bool ErrorOccured { get; private set; }

    private CancellationTokenSource cts;
    private readonly Func<ProgressWorkerData<T>, Task> worker;

    private readonly Form owner;

    public ProgressDlg(Form owner, Func<ProgressWorkerData<T>, Task> worker, bool allowCancel = false)
    {
        this.owner  = owner;
        this.worker = worker;

        InitializeComponent();

        if (!allowCancel)
            panelCancel.Visible = false;
    }

    public void Start(CancellationToken ct)
    {
        Start(null, ct);
    }

    public void Start(T userData, CancellationToken ct)
    {
        bool useWaitCursor          = Application.UseWaitCursor;
        Application.UseWaitCursor   = true;

        var progress = new Progress<ProgressData>(value =>
        {
            labelMessage.Text       = value.Message;
            labelMessage.ForeColor  = value.IsError ? Color.Red : Color.Black;
        });

        cts = CancellationTokenSource.CreateLinkedTokenSource(ct);

        Task.Run(async () =>
        {
            try
            {
                await worker.Invoke(new ProgressWorkerData<T>(userData, progress, cts.Token));
            }
            catch (OperationCanceledException)
            {
            }
            catch (Exception)
            {
                ErrorOccured = true;
            }
            finally
            { 
                cts.Dispose();
            }
        }, cts.Token)
        .ContinueWith(completedTask =>
        {
            Close();
        }, TaskScheduler.FromCurrentSynchronizationContext());

        ShowDialog(owner);

        Application.UseWaitCursor = useWaitCursor;
    }
}

Basically what OP was missing is

.ContinueWith(completedTask =>
{
    Close();
}, TaskScheduler.FromCurrentSynchronizationContext());

The CancellationTokenSource (cts) can be used to cancel a longer operation by e.g. clicking on a dialog's button

Usage:

private void Button_Click(object sender, EventArgs e)
{
    using (var progDlg = new ProgressDlg<string>(this, DoWork, true))
        progDlg.Start(data, CancellationToken); //This is a blocking call
}

private async Task DoWork(ProgressWorkerData<string> workerData)
{
    string myData = workerData.UserData;
    await Task.Delay(10000, workerData.CancellationToken);  //Do sth
}

For completeness

public class ProgressData
{
    public string Message { get; set; }
    public bool IsError { get; set; }

    public ProgressData(string message, bool isError = false)
    {
        Message = message;
        IsError = isError;
    }
}

public class ProgressWorkerData
{
    public CancellationToken CancellationToken { get; }

    private readonly IProgress<ProgressData> progress;

    public ProgressWorkerData(IProgress<ProgressData> progress, CancellationToken ct)
    {
        this.progress       = progress;
        CancellationToken   = ct;
    }

    public void Report(string message, bool isError = false)
    {
        progress.Report(new ProgressData(message, isError));
    }
}

public class ProgressWorkerData<T> : ProgressWorkerData
{
    public T UserData { get; }

    public ProgressWorkerData(T userData, IProgress<ProgressData> progress, CancellationToken ct) :
        base(progress, ct)
    {
        UserData = userData;
    }
}    
-1

You trying to close window from background thread... If you still want use Task.Run().ContinueWith() then you should use Dispatcher to close window. But better use async\await syntax.

    async void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        Window w = new ProgressWindow();

        var task = Task.Run(() => Go()).ContinueWith(completedTask =>
            {
                Application.Current.Dispatcher.BeginInvoke(
                DispatcherPriority.Send, new Action(() =>
                {
                    w.Close(); 
                }));

            });
        w.ShowDialog();
        await task;
    }
2
  • According your code, the ProgressWindow immediately closes after clicking on the main window. Generally, the code in ContinueWith performs immediately after calling Task.Run, without any awaiting of Go to complete. But if I change Task.Delay(2000) to Thread.Sleep(2000), then I achieve the required behavior. Why ContinueWith performs immediately without awaiting Go?
    – Alex34758
    Commented Jan 20, 2018 at 15:53
  • Because Go is not awaitable (it does not return a Task)
    – Peter Bons
    Commented Jan 20, 2018 at 17:18
-2

async (p) => 
      { 
      await Task.Run(() => { 
      
      if (p == null) return; 
      //code
      }).ConfigureAwait(true); 
      p.Close(); 
      });

1
  • 1
    Please include a brief explanation of how this code helps solving the problem in the question.
    – bad_coder
    Commented Mar 23, 2020 at 4:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.