从不同窗口的构造函数更新 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 的构造函数中再次调用DispatcherDispatcher.Invoke 有效地将委托排入调度程序队列。一旦第一个委托运行完成,下一个委托(在这种情况下是来自MainWindow 的构造函数内部的委托)被出列并执行(始终相对于给定的DispatcherPriority)。这就是为什么你必须等到构造函数完成,即第一个委托完成。

我强烈建议使用Progress&lt;T&gt;,这是从 .NET 4.5 (Async in 4.5: Enabling Progress and Cancellation in Async APIs) 开始推荐的进度报告方式。它的构造函数捕获当前的SynchronizationContext 并对其执行报告回调。由于Progress&lt;T&gt; 的实例是在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 不起作用的主要内容,如果未能解决你的问题,请参考以下文章

Qml 注册类型的构造函数中的发射信号不起作用

Moles 在静态构造函数中不起作用

Python继承2.7构造函数不起作用

构造函数的@injection 在Android中不起作用

调用 setVisible(false) 在 QWidget 的构造函数中不起作用

为啥在父类的构造函数中调用重载函数时,在 ES6 类上设置属性不起作用