我有两个默认窗口。 我希望一个窗口开始一项工作,以模式(对话框)形式显示另一个窗口(指示进度,但现在并不重要),然后在这项工作完成后关闭它。我在实现中遇到以下问题:
1)工作完成后(出现“Completed!”消息框,但这也不重要,只是指示),ProgressWindow不会自动关闭;
2)如果我通过手动单击红叉将其关闭,则会出现 System.InvalidOperationException 并显示消息“调用线程无法访问此对象,因为另一个线程拥有它。”发生在线上
await task;
3)ContinueWith 中的代码实际上是在 Go 方法完成之前执行的 - 为什么?
我怎样才能实现这样的行为?
我的实现:
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
}
}
}
这里不需要使用延续,只需坚持
await
。不仅如此,因为您使用了async void
,程序没有等待半秒就关闭了窗口。
此外,在这种情况下使用
Task.Run
实际上没有任何好处,因为 Go
已经可以等待。
改进简化后的代码为:
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();
}
}
您收到错误的原因是
Task.Run
创建的任务(的延续)在非UI线程上执行,并且不允许在非UI线程中访问UI(w.Close();
)。
如果您的工作受益于 Task.Run,您可以像这样修改
Go()
方法:
async Task Go(Window w)
{
await Task.Run(() => {
// ... heavy work here
});
w.Close();
}
这里有一个扩展方法
ShowDialogUntilTaskCompletion
,可以用来代替内置的ShowDialog
。当提供的 Task
完成时,它会自动关闭窗口。如果任务完成时出现异常,窗口仍然关闭,并且该方法返回而不抛出异常。
可以通过处理窗口上的
Closing
事件并将 CancelEventArgs.Cancel
属性设置为 true 来阻止用户关闭窗口(并导致方法在任务完成之前返回)。该方法在调用 Window.Close()
方法之前取消订阅事件,否则将被取消。
public static class WindowExtensions
{
public static bool? ShowDialogUntilTaskCompletion(this Window window,
Task task, int minDurationMsec = 500)
{
if (window == null) throw new ArgumentNullException(nameof(window));
if (task == null) throw new ArgumentNullException(nameof(task));
if (minDurationMsec < 0)
throw new ArgumentOutOfRangeException(nameof(minDurationMsec));
window.Closing += PreventUserClosing;
var closeDelay = Task.Delay(minDurationMsec);
HandleTaskCompletion();
return window.ShowDialog();
async void HandleTaskCompletion()
{
try
{
await Task.Yield(); // Ensure that the completion is asynchronous
await task;
}
catch { } // Ignore exception
finally
{
try
{
window.Closing -= PreventUserClosing;
await closeDelay;
window.Close();
}
catch { } // Ignore exception
}
}
void PreventUserClosing(object sender, CancelEventArgs e)
{
e.Cancel = true;
}
}
}
作为奖励,它还接受
minDurationMsec
参数,以使窗口在指定的最短持续时间内保持可见(以便窗口不会在眨眼间关闭)。
使用示例:
async void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Window w = new ProgressWindow();
var 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
}
}
我阅读了 Peter Bons 的答案并重新思考它 - 他的方法使用异步 Go 方法(当然就像我的问题中一样),然后等待其结果而不使用 Task.Run。我得出的另一个变体是基于 Peter Bons 和 Andrew Shkolik 的答案。我通过Task.Run异步调用同步方法Go,并使用Dispatcher来操作窗口。
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();
}));
}
}
您试图从后台线程关闭窗口... 如果您仍然想使用 Task.Run().ContinueWith() 那么您应该使用 Dispatcher 来关闭窗口。但最好使用异步等待语法。
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;
}
async (p) =>
{
await Task.Run(() => {
if (p == null) return;
//code
}).ConfigureAwait(true);
p.Close();
});