为什么在调用 `Dispatcher.Invoke()` 时 WPF 选项卡变为 "unresponsive"?

Why do WPF tabs become "unresponsive" when `Dispatcher.Invoke()` is called?

据我了解,WPF“消息”(例如按钮单击处理程序)被添加到内部优先级队列中。然后,单个 UI 线程负责处理排队的消息。

遗憾的是,我对 WPF 的了解还不够深入,无法理解该框架的内部工作原理。所以我的问题是,鉴于只有 1 个线程处理消息...

观察到的行为

  1. 如果缓慢单击,TabControl 会按预期运行。
    • 要重现:每 4 秒单击 1 个选项卡。
    • 看来,如果您给 TabControl.SelectedIndex 数据绑定一个完成的机会,控件将按设计运行。
  2. 如果快速单击选项卡,某些选项卡将变得无响应。
    • 重现:在 3 秒内点击尽可能多的标签。

补充阅读

示例代码

以下代码可用于重现该行为,从而永久选中 WPF 选项卡。

粘贴到MainWindow.xaml:

<Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="30" />
            <RowDefinition Height="*" />
            <RowDefinition Height="30" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <WrapPanel Grid.Row="0">
            <TextBlock>
                1. Click on as many tabs as possible within 3 seconds.<LineBreak/>
                2. Wait until multiple tabs are selected.<LineBreak/>
                3. Uncheck the `Simulate Bug` checkbox.<LineBreak/>
            </TextBlock>
        </WrapPanel>
        <CheckBox Grid.Row="1" IsChecked="{Binding CanSimulateBug}" Content="Simulate Bug"/>
        <TabControl x:Name="ColorWorkspaces" Grid.Row="2" SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
            <TabItem x:Name="RedTab" Header="Red"/>
            <TabItem x:Name="OrangeTab" Header="Orange"/>
            <TabItem x:Name="YellowTab" Header="Yellow"/>
            <TabItem x:Name="GreenTab" Header="Green"/>
            <TabItem x:Name="BlueTab" Header="Blue"/>
            <TabItem x:Name="VioletTab" Header="Violet"/>
        </TabControl>
        <TextBlock Grid.Row="3" Text="{Binding Status}"/>
    </Grid>

粘贴到 MainWindow.xaml.cs:

public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private int _selectedTab;
        private string _status;
        private bool _canSimulateBug;

        public MainWindow()
        {
            this.CanSimulateBug = true;
            this.Status = String.Empty;
            this.DataContext = this;
            InitializeComponent();
        }


        protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public bool CanSimulateBug
        {
            get
            {
                return _canSimulateBug;
            }
            set
            {
                _canSimulateBug = value;
                RaisePropertyChanged();
            }
        }

        public string Status
        {
            get
            {
                return _status;
            }
            set
            {
                _status = value ?? string.Empty;
                RaisePropertyChanged();
            }
        }

        public int SelectedTab
        {
            get
            {
                return _selectedTab;
            }
            set
            {
                UpdateStatus($"SelectedTab changing... Value={value}");

                if (this.CanSimulateBug)
                {
                    SimulateBug(value);
                }
                _selectedTab = value;
                UpdateStatus($"SelectedTab changed. Value={value}");
                // This missing line was added as per 
Felix's comment
                RaisePropertyChanged();
            }
        }

        private void UpdateStatus(string message)
        {
            var formattedMessage = $"[{Thread.CurrentThread.ManagedThreadId}] {DateTime.Now.ToLongTimeString()}: {message}";
            this.Status = formattedMessage;
            Debug.WriteLine(formattedMessage);
        }

        private void SimulateBug(int id)
        {
            var delay = TimeSpan.FromSeconds(3);
            UpdateStatus($"Bug simulation started... ID={id}, Delay={delay}");

            // IMPORTANT: If you comment out this following line
            // ... the application will behave as expected.
            Application.Current.Dispatcher.Invoke( // blocking call
                DispatcherPriority.Background, // tells UI thread to execute this as lowest priority job
                new Action(delegate { /* do nothing */ }));

            Thread.Sleep(delay);
            UpdateStatus($"Bug simulation complete. ID={id}");
        }
    }

您的完整代码正在单个线程上执行。您不能使用单个线程执行并发操作。您当前正在做的是通过同步调用两个潜在的 long-running 操作来阻塞主线程两次(太长):

  1. 同步 Dispatcher 调用使用 Dispatcher.Invoke:
Application.Current.Dispatcher.Invoke(() => {}, DispatcherPriority.Background);
  1. 同步线程休眠:
Thread.Sleep(TimeSpan.FromSeconds(3));

在执行这些同步操作时,主线程不能自由执行管理托管项目的选择状态。
通过等待 Dispatcher 到 return 然后将其发送到睡眠状态,即挂起它,主线程被阻塞。
因此,Selector 无法取消选择之前选择的 TabItem
Selector 能够取消选择过程,这涉及处理所选项目并取消选择所有其他项目(如果不支持 multi-select)。很明显,Selector取消了pending项的unselection/processing。
您应该能够通过监听附加到 TabItemSelector.Unselected 事件来对此进行测试。它不应该被提高。显然,主线程的阻塞为 Selector.
的内部项验证创建了竞争条件 要解决此竞争条件,将排队的调度程序消息的 DispatcherPriority 增加到至少 DispatcherPriority.DataBind(高于 DispatcherPriority.Input)应该就足够了:

Application.Current.Dispatcher.Invoke(() => {}, DispatcherPriority.DataBind);

这不是推荐的修复方法,尽管它修复了竞争条件,因此修复了多个选定选项卡的问题,因为关键代码现在可以及时执行。真正的潜在问题是阻塞的主线程(实际上是阻塞的Dispatcher)。

你永远不想阻塞主线程。现在你明白为什么了。
为此,.NET 引入了 TPL。此外,compiler/runtime 环境允许真正的异步执行:通过将执行委托给 OS,.NET 可以使用中断等内核级功能。这样 .NET 可以允许主线程继续,例如,处理基本的 UI 相关事件,如设备输入。

OS 级别和框架级别之间的部分接口是 DispatcherInputManagerDispatcher 基本上管理关联的线程。在 STA 应用程序中,这是主线程。现在,当您使用 Thread.Sleep 阻塞主线程时,Dispatcher 无法继续处理包含在关联的调度程序线程(主线程)上执行的处理程序的消息队列。
Dispatcher 现在无法执行 InputManager 发布的输入事件。由于依赖 属性 系统(路由事件和数据绑定引擎基于该系统)和通常 UI 控件的代码也在 Dispatcher 上执行,它们也被挂起。

极低的调度程序优先级 DispatcherPriority.Background 与长 Thread.Sleep 相结合使问题更加严重。


解决方法是不阻塞主线程:

  1. Post 异步处理 Dispatcher 并允许应用程序在作业排队和等待时通过调用 Dispatcher.InvokeAsync:
  2. 继续
Application.Current.Dispatcher.InvokeAsync(() => {}, DispatcherPriority.Background);
  1. 使用 async/await:
  2. 异步执行阻塞 I/O 绑定操作
await Task.Delay(TimeSpan.FromSeconds(3));
  1. 并发执行阻塞CPU绑定操作:
Task.Run(() => {});

您的固定代码如下所示:

private async Task SimulateNoBugAsync(int id)
{
  var delay = TimeSpan.FromSeconds(3);
  UpdateStatus($"Bug simulation started... ID={id}, Delay={delay}");

  // If you need to wait for a result or for completion in general,
  // await the Dispatcher.InvokeAsync call.
  Application.Current.Dispatcher.InvokeAsync(() => {}, DispatcherPriority.Background);
  await Task.Delay(delay);

  UpdateStatus($"Bug simulation complete. ID={id}");
}