Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF - Task.Run(() => window.ShowDialog) fails

I have to implement busy indication and progress reporting. The constraint is, that I have to use the provided Control Library, which offers a Window for progress reporting.

The following code works fine, but does not block the UI, which in some times is required.

private async void StartLongRunningTask2Sync() {
var wndHandle = Application.Current.Windows.OfType<Window>().SingleOrDefault(x => x.IsActive);
if (wndHandle == null)
{
    return;
}

IntPtr windowHandle = new WindowInteropHelper(wndHandle).Handle;
var progressWindow = new ProgressBarCustomized(windowHandle)
{
    Value = 0, CanCancel = true, CanRetry = false, Thumbnail = null, IsIndeterminate = true
};
progressWindow.Show();

await Task.Run(() => this.longRunningTaskComponent.DoLongRunningTask(this.taskIterations, this.iterationSleepTime));
progressWindow.Close();

}

The following code, which blocks the UI would work so far that the dialog is opened, but the long running task never gets executed until the dialog is closed again:

private async void StartLongRunningTask2Sync() {
var wndHandle = Application.Current.Windows.OfType<Window>().SingleOrDefault(x => x.IsActive);
if (wndHandle == null)
{
    return;
}

IntPtr windowHandle = new WindowInteropHelper(wndHandle).Handle;
var progressWindow = new ProgressBarCustomized(windowHandle)
{
    Value = 0, CanCancel = true, CanRetry = false, Thumbnail = null, IsIndeterminate = true
};
progressWindow.ShowDialog();

await Task.Run(() => this.longRunningTaskComponent.DoLongRunningTask(this.taskIterations, this.iterationSleepTime));
progressWindow.Close();

}

So I tried with this approach:

private async void StartLongRunningTask2Sync() {
var wndHandle = Application.Current.Windows.OfType<Window>().SingleOrDefault(x => x.IsActive);
if (wndHandle == null)
{
    return;
}

IntPtr windowHandle = new WindowInteropHelper(wndHandle).Handle;
var progressWindow = new ProgressBarCustomized(windowHandle)
{
    Value = 0, CanCancel = true, CanRetry = false, Thumbnail = null, IsIndeterminate = true
};
Task.Run(() => progressWindow.ShowDialog());

await Task.Run(() => this.longRunningTaskComponent.DoLongRunningTask(this.taskIterations, this.iterationSleepTime));
progressWindow.Close();

}

When doing this, I get the following error: The calling thread cannot access this object because a different thread owns it.

After investigation of the custom progress window I found out, that the call "base.ShowDialog()" throws this error.

Is there a way to do what I like or do I have to do this with a totally different approach? Best regards

UPDATE: Yes, I have searched for this error and yes, I have tried several approaches with Dispatcher.Invoke() etc...

So the "real" question: How can I show a blocking Window when a long running task is running and closing it after the long running task has finished and, eventually, inform the window about the progress of the action. The solution should (preferably) work with the MVVM pattern and not rely on (too much) code behind.

like image 708
Yannik Avatar asked Sep 02 '14 08:09

Yannik


2 Answers

So the "real" question: How can I show a blocking Window when a long running task is running and closing it after the long running task has finished and, eventually, inform the window about the progress of the action.

You've already got most of the pieces; you just need to put them together.

How can I show a blocking Window

All UI should go on a single GUI thread. This isn't strictly necessary, but it's a great simplifier and works for the vast, vast majority of applications. A "blocking Window" is known in the UI world as a "modal dialog", and you show one by calling ShowDialog.

// Start the long-running operation
var task = LongRunningOperationAsync();

// Show the dialog
progressWindow.ShowDialog();

// Retrieve results / propagate exceptions
var results = await task;

closing it after the long running task has finished

For this, you need to wire up the completion of the task to close the window. This is pretty straightforward to do using async/await:

async Task DoOperationAsync(ProgressWindow progressWindow)
{
  try
  {
    await LongRunningOperationAsync();
  }
  finally
  {
    progressWindow.Close();
  }
}

// Start the long-running operation
var task = DoOperationAsync(progressWindow);

// Show the dialog
progressWindow.ShowDialog();

// Retrieve results / propagate exceptions
var results = await task;

inform the window about the progress of the action

Assuming your operation is using the standard IProgress<T> interface for reporting progress:

async Task DoOperationAsync(Window progressWindow, IProgress<int> progress)
{
  try
  {
    await LongRunningOperationAsync(progress);
  }
  finally
  {
    progressWindow.Close();
  }
}

var progressWindowVM = ...;
var progress = new Progress<int>(value =>
{
  progressWindowVM.Progress = value;
});

var task = DoOperationAsync(progressWindow, progress);
progressWindow.ShowDialog();
var results = await task;

Another common use case to consider is the cancelling of the operation if the user closes the progress dialog themselves. Again, this is straightfoward if your operation is already using the standard CancellationToken:

async Task DoOperationAsync(Window progressWindow, CancellationToken token, IProgress<int> progress)
{
  try
  {
    await LongRunningOperationAsync(token, progress);
  }
  catch (OperationCanceledException) { }
  finally
  {
    progressWindow.Close();
  }
}

var progressWindowVM = ...;
var progress = new Progress<int>(value =>
{
  progressWindowVM.Progress = value;
});

var cts = new CancellationTokenSource();
progressWindow.Closed += (_, __) => cts.Cancel();

var task = DoOperationAsync(progressWindow, cts.Token, progress);
progressWindow.ShowDialog();
var results = await task;

The solution should (preferably) work with the MVVM pattern and not rely on (too much) code behind.

MVVM works great within a single window. As soon as you start trying to data-bind window-level actions and attributes, a lot of it falls apart. This is not due to MVVM being a poor pattern, but rather just that a lot of MVVM frameworks do not handle this well.

The example code above only uses data binding to report progress to the progress dialog. If your MVVM framework can data-bind the showing/hiding of a modal window, then you could use my NotifyTaskCompletion type to drive that. Also, some frameworks have a more elegant (MVVM) way to handle Window.Closed, but the details depend on your framework.

like image 100
Stephen Cleary Avatar answered Sep 28 '22 07:09

Stephen Cleary


The calling thread cannot access this object because a different thread owns it.

This is a very common error and if you had searched online, you would have found a very simple explanation.

You cannot manipulate UI objects on a non UI thread.

The solution is simple. Don't attempt to open a dialog Window on a non UI thread.

Perhaps if you can clarify what your actual question is (by editing your question, not by commenting), then I can help further?

like image 33
Sheridan Avatar answered Sep 28 '22 08:09

Sheridan