【问题标题】:Update UI from the constructor of a different window not working从不同窗口的构造函数更新 UI 不起作用
【发布时间】: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”。在执行“做一些需要几秒钟的事情......”之前并不像预期的那样。

我的错误是什么?这是否和我想的一样?

【问题讨论】:

    标签: c# wpf multithreading user-interface


    【解决方案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   
          }
      }
    }
    

    【讨论】:

    • 非常感谢您的回答和解释!这很好用。
    猜你喜欢
    • 2015-06-14
    • 1970-01-01
    • 1970-01-01
    • 2012-07-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多