多线程时 WPF 无法更新状态

Posted

技术标签:

【中文标题】多线程时 WPF 无法更新状态【英文标题】:WPF failing to update state when multi-threading 【发布时间】:2021-10-17 19:14:39 【问题描述】:

使用多线程和 WPF 运行问题。我真的不知道自己在做什么,而且通常的 *** 答案也不起作用。

首先,通过以下方式创建了一堆 WPF 窗口:

var thread = new Thread(() =>

  var bar = new MainWindow(command.Monitor, _workspaceService, _bus);
  bar.Show();
  System.Windows.Threading.Dispatcher.Run();
);

thread.Name = "Bar";
thread.SetApartmentState(ApartmentState.STA);
thread.Start();

在生成的窗口的 ctor 中,创建了一个视图模型,并在应该更改视图模型的位置监听一个事件。

this.DataContext = new BarViewModel();

// Listen to an event propagated on main thread.
_bus.Events.Where(@event => @event is WorkspaceAttachedEvent).Subscribe(observer => 

  // Refresh contents of viewmodel.
  (this.DataContext as BarViewModel).SetWorkspaces(monitor.Children);
);

视图模型状态修改如下:

public void SetWorkspaces(IEnumerable<Workspace> workspaces)

  Application.Current.Dispatcher.Invoke((Action)delegate
 
   this.Workspaces.Clear(); // this.Workspaces is an `ObservableCollection<Workspace>`

   foreach (var workspace in workspaces)
     this.Workspaces.Add(workspace);

   this.OnPropertyChanged("Workspaces");
 );

问题是访问Application.Current.Dispatcher 导致NullReferenceException。生成窗口的方式有问题吗?

【问题讨论】:

我真的不知道我在做什么 -> 你为什么选择在一个单独的线程中构建窗口?所有 UI 工作都应在 UI 线程上完成。 该应用程序的其余部分是一个标准的 .NET 核心控制台应用程序,我需要一种以编程方式生成 WPF 窗口的方法。我有一个业务逻辑所在的主线程,所以我这样做只是因为它似乎完成了打开窗口的工作。有没有更好的方法来生成窗口? 【参考方案1】:

应用程序的其余部分是标准的 .NET 核心控制台应用程序,我需要一个 以编程方式生成 WPF 窗口的方法

生成窗口的方式有问题吗?

是的。

在 WPF 应用程序中,您有一个处理所有 UI 工作的主线程(也称为 UI 线程)。尝试从其他线程执行 UI 工作可能会导致异常。

听起来您的入口点不是 WPF 应用程序,而是控制台应用程序,而您正试图从该控制台应用程序生成窗口。这是行不通的。

您需要像平常一样创建一个 WPF 项目,并将其作为您的主要入口点。

另外,不要在应该使用 Tasks 的地方使用 Thread 对象。

【讨论】:

问题在于显示 WPF 窗口更多的是一个附带功能,并且可能存在没有显示窗口的情况。使用上面的代码,我可以生成具有初始状态的窗口,但是从不同的线程更新该状态似乎很棘手。还研究了使用SynchronizationContext,但SynchronizationContext.Current 在更新状态时是null。我很欣赏关于Tasks 的提示! 这是一个有趣的用例。我相信您可以通过创建App 对象和Running 来实现它而无需进行重大重构。试试这些解决方案:***.com/a/43388522/6104191***.com/a/23298284/6104191【参考方案2】:

问题是,您不会有Application.Current 实例。您正在为您的窗口启动线程,每个线程/窗口都有自己的调度程序。所以最简单的解决方案是将 MainWindow 的 Dispatcher 传递给 ViewModel 并使用它而不是 Application.Current.Dispatcher。一个快速且非常肮脏的解决方案:

static void Main(string[] args)

    do
    
        switch (Console.ReadLine())
        
            case "n":
                StartNewWindowOnOwnThread();
                break;
            default:
                return;
        
     while (true);


private static void StartNewWindowOnOwnThread()

    var t = new Thread(() =>
    
        var w = new MainWindow();
        w.Show();
        System.Windows.Threading.Dispatcher.Run();
    );
    t.SetApartmentState(ApartmentState.STA);
    t.Start();

MainWindow的ViewModel:

class MainWindowModel : INotifyPropertyChanged

    private readonly Dispatcher dispatcher;
    private readonly Timer timer;
    private int count;

    public event PropertyChangedEventHandler PropertyChanged;

    internal MainWindowModel(Dispatcher dispatcher)
    
        this.dispatcher = dispatcher;
        //Simulate background activity on another thread
        timer = new Timer(OnTimerTick, null, 1000, 1000);
    

    public string ThreadId => $"Thread dispatcher.Thread.ManagedThreadId";

    public int Count
    
        get  return count; 
        set
        
            count = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
        
    

    private void OnTimerTick(object state)
    
        dispatcher.BeginInvoke(new Action(() => Count++));
    

主窗口:

public partial class MainWindow : Window

    public MainWindow()
    
        InitializeComponent();
        DataContext = new MainWindowModel(Dispatcher);
    

    private void Button_Click(object sender, RoutedEventArgs e)
    
        Dispatcher.InvokeShutdown();
    

XAML - 片段:

<Grid>
    <StackPanel Orientation="Vertical">
        <Button Content="Close" Click="Button_Click"/>
        <TextBlock Text="Binding ThreadId"/>
        <TextBlock Text="Binding Count"/>
    </StackPanel>
</Grid>

【讨论】:

哇,感谢您的回复。我已经尝试完全实现您的方法(尽管基础变量当然不同),并且在通过SetWorkspaces(...) 更新视图模型时遇到'The calling thread cannot access this object because a different thread owns it.'。如果您决定看一下,请将回购公开。有问题的文件是this,可以通过按例如 ALT+4 来复制 在 .NET core 4+ 中发现一条评论提到“在更新 VM 中的 UI 支持属性时,您不再需要担心正确的调度程序”。听起来这可能是我的确切问题,所以我会尝试修改版本,看看是否有什么作用。 ***.com/questions/9732709/… @L.Berger 我假设,事件处理程序是从另一个线程调用的。使用 Dispatcher 调用 RefreshViewModel。将Dispatcher.BeginInvoke(new Action(() =&gt; RefreshViewModel(monitor)); 放入Subscribe(observer =&gt; ... 没错,Subscribe 回调是在另一个线程上调用的。通过Dispatcher 的调用位于视图模型here 中。最终升级到 .NET 5,但这个该死的错误仍在弹出 终于似乎有所进展,但仍然感到困惑。如果在视图模型中,我将状态从ObservableCollection&lt;Workspace&gt; 更改为string(仅用于调试)并在Subscribe 回调中改变它,它正在成功更新。出于某种原因,ObservableCollection 特有的某些东西似乎导致了问题

以上是关于多线程时 WPF 无法更新状态的主要内容,如果未能解决你的问题,请参考以下文章

多线程更新UI的常用方法

“调用线程无法访问此对象,因为不同的线程拥有它”从 WPF 中的不同线程更新 UI 控件时出现错误

WPF怎么跨线程访问UI控件

WPF Dispatcher.BeginInvoke子线程更新UI

WPF ABP框架更新日志(最新2022-11月份)

WPF 根据 UI 线程控制状态更新 NON UI 线程数据