从不同窗口的构造函数更新 UI 不起作用
Posted
技术标签:
【中文标题】从不同窗口的构造函数更新 UI 不起作用【英文标题】:Update UI from the constructor of a different window not working 【发布时间】:2020-04-14 08:01:59 【问题描述】:我目前正在尝试实现启动画面。我以this tutorial 为起点。
我的 App.xaml.cs 中的 OnStartup 如下所示:
protected override void OnStartup(StartupEventArgs e)
//initialize the splash screen and set it as the application main window
splashScreen = new MySplashScreen();
this.MainWindow = splashScreen;
splashScreen.Show();
//in order to ensure the UI stays responsive, we need to
//do the work on a different thread
Task.Factory.StartNew(() =>
//we need to do the work in batches so that we can report progress
for (int i = 1; i <= 100; i++)
//simulate a part of work being done
System.Threading.Thread.Sleep(30);
//because we're not on the UI thread, we need to use the Dispatcher
//associated with the splash screen to update the progress bar
splashScreen.Dispatcher.Invoke(() => splashScreen.Progress = i);
splashScreen.Dispatcher.Invoke(() => splashScreen.MyText = i.ToString());
//once we're done we need to use the Dispatcher
//to create and show the main window
this.Dispatcher.Invoke(() =>
//initialize the main window, set it as the application main window
//and close the splash screen
var mainWindow = new MainWindow();
this.MainWindow = mainWindow;
mainWindow.Show();
splashScreen.Close();
);
);
这非常有效。启动画面被调用,进度(ProgressBar)增加到 100。
现在我不仅要从 OnStartup 中写入进度到启动屏幕,还要从 MainWindow 的构造函数中写入进度。
我的 MainWindow 构造函数:
public MainWindow()
InitializeComponent();
((App)Application.Current).splashScreen.Dispatcher.Invoke(() => ((App)Application.Current).splashScreen.MyText = "From MainWindow");
// do some stuff that takes a few seconds...
这没有按预期工作。只有在完全调用构造函数后,才会在初始屏幕的文本框中更新文本“From MainWindow”。在执行“做一些需要几秒钟的事情......”之前并不像预期的那样。
我的错误是什么?这是否和我想的一样?
【问题讨论】:
【参考方案1】:当您使用 Dispatcher.Invoke
调用构造函数时,Dispatcher
已经忙于创建 MainWindow
。然后在MainWindow
的构造函数中再次调用Dispatcher
。 Dispatcher.Invoke
有效地将委托排入调度程序队列。一旦第一个委托运行完成,下一个委托(在这种情况下是来自MainWindow
的构造函数内部的委托)被出列并执行(始终相对于给定的DispatcherPriority
)。这就是为什么你必须等到构造函数完成,即第一个委托完成。
我强烈建议使用Progress<T>
,这是从 .NET 4.5 (Async in 4.5: Enabling Progress and Cancellation in Async APIs) 开始推荐的进度报告方式。它的构造函数捕获当前的SynchronizationContext
并对其执行报告回调。由于Progress<T>
的实例是在UI 线程上创建的,回调将在适当的线程上自动执行,因此不再需要Dispatcher
。这将解决您的问题。此外,在异步上下文中使用时,进度报告也可以使用取消。
我还建议使用async/ await
来控制流量。目标是在 UI 线程上创建 MainWindow
的实例。
还要始终避免使用Thread.Sleep
,因为它会阻塞线程。在这种情况下,UI 线程将因此变得无响应并冻结。请改用异步(非阻塞)await Task.Delay
。根据经验,将所有对Thread
的引用替换为Task
,即任务并行库是首选方法(Task-based asynchronous programming)。
我相应地重构了你的代码:
App.xaml.cs
private SplashScreen get; set;
protected override async void OnStartup(StartupEventArgs e)
// Initialize the splash screen.
// The first Window shown becomes automatically the Application.Current.MainWindow
this.SplashScreen = new MySplashScreen();
this.SplashScreen.Show();
// Create a Progress<T> instance which automatically
// captures the current SynchronizationContext (UI thread)
// which makes the Dispatcher obsolete for reporting the progress to the UI.
// Pass a report (UI update) callback to the Progress<T> constructor,
// which will execute automatically on the UI thread.
// Because of the generic parameter which is in this case of type ValueTuple (C# 7),
// 'System.ValueTuple' is required to be referenced (use NuGet Package Manager to install).
// Alternatively replace the tuple with an arg class.
var progressReporter = new Progress<(int Value, string Message)>(ReportProgress);
// Wait asynchronously for the background task to complete
await DoWorkAsync(progressReporter);
// Override the Application.Current.MainWindow instance.
this.MainWindow = new MainWindow();
// Asynchronously wait until MainWindow is initialized
// Pass the Progress<T> instance to the method,
// so that MainWindow can report progress too
await this.MainWindow.InitializeAsync(progressReporter);
this.SplashScreen.Close();
this.MainWindow.Show();
private async Task DoWorkAsync(IProgress<(int Value, string Message)> progressReporter)
// In order to ensure the UI stays responsive, we need to
// do the work on a different thread
await Task.Run(
async () =>
// We need to do the work in batches so that we can report progress
for (int i = 1; i <= 100; i++)
// Simulate a part of work being done
await Task.Delay(30);
progressReporter.Report((i, i.ToString()));
);
// The progress report callback which is automatically invoked on the UI thread.
// Requires 'System.ValueTuple' to be referenced (see NuGet)
private void ReportProgress((int Value, string Message) progress)
this.SplashScreen.Progress = progress.Value;
this.SplashScreen.MyText = progress.Message;
MainWindow.xaml.cs
public partial class MainWindow
public MainWindow()
InitializeComponent();
public async Task InitializeAsync(IProgress<(int Value, string Message)> progressReporter)
await Task.Run(
() =>
progressReporter.Report((100, "From MainWindow"));
// Run the initialization routine that takes a few seconds
【讨论】:
非常感谢您的回答和解释!这很好用。以上是关于从不同窗口的构造函数更新 UI 不起作用的主要内容,如果未能解决你的问题,请参考以下文章