
據我了解,WPF“訊息”(例如按鈕單擊處理程式)被添加到內部優先佇列中。然后一個 UI 執行緒負責處理排隊的訊息。
不幸的是,我對 WPF 的了解還不夠深入,無法理解框架的內部作業。所以我的問題是,鑒于只有 1 個執行緒處理訊息......
- 導致選項卡無回應的內部事件序列(高級)是什么?
觀察到的行為
- 如果您緩慢單擊,則會按
TabControl預期運行。- 要重現:每 4 秒單擊 1 個選項卡。
- 看來,如果您給
TabControl.SelectedIndex資料系結一個完成的機會,控制元件將按設計運行。
- 如果您快速單擊選項卡,則某些選項卡將變得無回應。
- To reproduce: click as many tabs as you can within 3 seconds.
Additional Reading
- Two selected tabs in tabcontroller
- While the behavior is similar, this article is different because the symptom is the result of using a
TabMessageBox.
- While the behavior is similar, this article is different because the symptom is the result of using a
Sample Code
The following code can be used to reproduce the behavior, whereby, WPF tabs become permanently selected.
Paste into 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>
Paste into 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}");
}
}
uj5u.com熱心網友回復:
您的完整代碼在單個執行緒上執行。您不能使用單個執行緒執行并發操作。您目前正在做的是通過同步呼叫兩個可能長時間運行的操作來阻塞主執行緒兩次(太長):
- 使用同步
Dispatcher呼叫Dispatcher.Invoke:
Application.Current.Dispatcher.Invoke(() => {}, DispatcherPriority.Background);
- 同步執行緒休眠:
Thread.Sleep(TimeSpan.FromSeconds(3));
在執行這些同步操作時,主執行緒不能自由地執行管理托管項的選擇狀態的邏輯(在本例中是 的一部分Selector,它是 的超類)。
主執行緒被阻塞等待回傳,然后將其發送到睡眠狀態,即掛起它。
因此無法取消選擇先前選擇的. 能夠取消選擇程序,這涉及處理選定的專案并取消選擇所有其他專案(如果不支持多選)
。顯然,取消選擇/處理未決專案。
你應該能夠通過聽TabControlDispatcherSelectorTabItemSelectorSelectorSelector.Unselected您附加到TabItem. 它不應該被提高。顯然,主執行緒的阻塞為Selector. 要解決此競爭條件,將排隊的調度程式訊息的數量增加到至少(上圖)
就足夠了:DispatcherPriorityDispatcherPriority.DataBindDispatcherPriority.Input
Application.Current.Dispatcher.Invoke(() => {}, DispatcherPriority.DataBind);
這不是推薦的修復方法,盡管它修復了競爭條件,因此作為關鍵代碼現在可以及時執行多個選定選項卡的問題。真正的潛在問題是阻塞的主執行緒(實際上是阻塞的Dispatcher)。
你永遠不想阻塞主執行緒。現在你明白為什么了。
出于這個原因,.NET 引入了 TPL。此外,編譯器/運行時環境允許真正的異步執行:通過將執行委托給作業系統,.NET 可以使用中斷等內核級功能。這樣,.NET 可以允許主執行緒繼續,例如處理與設備輸入相關的基本 UI 相關事件。
作業系統級別和框架級別之間的部分介面是Dispatcher和InputManager. 基本上管理關聯的Dispatcher執行緒。在 STA 應用程式中,這是主執行緒。現在,當您使用 阻塞主執行緒時Thread.Sleep,Dispatcher無法繼續處理包含在關聯調度程式執行緒(主執行緒)上執行的處理程式的訊息佇列。
現在Dispatcher無法執行 發布的輸入事件InputManager。由于依賴屬性系統(路由事件和資料系結引擎所基于的)以及通常 UI 控制元件的代碼也在 上執行Dispatcher,因此它們也被掛起。
非常低的調度程式優先級DispatcherPriority.Background與 long 相結合Thread.Sleep使問題更加嚴重。
解決辦法是不阻塞主執行緒:
- 通過呼叫將作業異步發布到作業
Dispatcher并允許應用程式在作業排隊和掛起時繼續Dispatcher.InvokeAsync:
Application.Current.Dispatcher.InvokeAsync(() => {}, DispatcherPriority.Background);
async使用/異步執行阻塞 I/O 系結操作await:
await Task.Delay(TimeSpan.FromSeconds(3));
- 并發執行阻塞 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}");
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/446095.html
