在 TabControl WPF 问题中异步绑定到 SelectedItem
Async binding to SelectedItem in TabControl WPF issues
我有一个带标签的面板。我的此面板视图模型包含 ObservableCollection
个选项卡视图模型,以及一个 属性 个 select 选项卡视图模型。
当某些操作请求聚焦选项卡或创建新选项卡时,我会更改 Selected
并且选项卡 selection 会正确更改,差不多,因为内容有效,但所有headers 看起来什么都没有 selected。
我找到了一个解决方案,说要将 IsAsync=True
添加到我的绑定中。这解决了问题,但增加了一堆新问题。
第一件事是,当我 运行 在调试模式下编程时,添加带按钮的选项卡工作正常,选项卡被切换并正确 selected 但是当我尝试单击选项卡以 select 我得到异常
The calling thread cannot access this object because a different thread owns it.
设置 属性 表示当前 selected 选项卡时抛出:
private Tab selected;
public Tab Selected
{
get { return Selected; }
set { SetProperty(ref Selected, value); } // <<< here (I use prism BindableBase)
}
另一个问题是,当我快速切换标签时,可能会出现这样的情况:我有 Tab1 selected 但它显示的是 Tab2 的内容,多切换几次标签就会恢复正常。
我的问题是,我该如何解决这个问题,即在更改 Selected
时让我的选项卡 headers selected(有点突出显示),而不会出现评估 IsAsync
原因。
编辑
这是允许重现问题的代码。它使用棱镜 6.1.0
MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<StackPanel DockPanel.Dock="Top"
Orientation="Horizontal"
Margin="0,5"
Height="25">
<Button
Command="{Binding AddNewTabCommand}"
Content="New Tab"
Padding="10,0"/>
<Button
Command="{Binding OtherCommand}"
Content="Do nothing"
Padding="10,0"/>
</StackPanel>
<TabControl
SelectedItem="{Binding Selected, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, IsAsync=True}" <!--remove IsAsync to break tab header selecting-->
ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Margin="5"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBox Text="{Binding Text}"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</DockPanel>
</Window>
后面的代码:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new TabGroup();
}
}
Tab.cs
public class Tab : BindableBase
{
public Tab(string name, string text)
{
this.name = name;
this.text = text;
}
private string name;
public string Name
{
get { return name; }
set { SetProperty(ref name, value); }
}
private string text;
public string Text
{
get { return text; }
set { SetProperty(ref text, value); }
}
}
TabGroup.cs
public class TabGroup : BindableBase
{
private Random random;
public TabGroup()
{
this.random = new Random();
this.addNewTabCommand = new Lazy<DelegateCommand>(() => new DelegateCommand(AddNewTab, () => true));
this.otherCommand = new Lazy<DelegateCommand>(() => new DelegateCommand(Method, () => Selected != null).ObservesProperty(() => Selected));
Tabs.CollectionChanged += TabsChanged;
}
private void Method()
{
}
private void TabsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var newItems = e.NewItems?.Cast<Tab>().ToList();
if (newItems?.Any() == true)
{
Selected = newItems.Last();
}
}
private void AddNewTab()
{
Tabs.Add(new Tab(GetNextName(), GetRandomContent()));
}
private string GetRandomContent()
{
return random.Next().ToString();
}
private int num = 0;
private string GetNextName() => $"{num++}";
private Tab selected;
public Tab Selected
{
get { return selected; }
set { SetProperty(ref selected, value); }
}
public ObservableCollection<Tab> Tabs { get; } = new ObservableCollection<Tab>();
private readonly Lazy<DelegateCommand> addNewTabCommand;
public DelegateCommand AddNewTabCommand => addNewTabCommand.Value;
private readonly Lazy<DelegateCommand> otherCommand;
public DelegateCommand OtherCommand => otherCommand.Value;
}
准备这个让我弄清楚异常是从哪里来的。这是因为 OtherCommand 观察 selected 属性。我仍然不知道如何使它正确。对我来说最重要的是让选项卡在应该的时候被 selected,这样 selected 的选项卡就不会与选项卡控件显示的内容不同步。
这是一个 github 代码库
当您收到错误 "The calling thread cannot access this object because a different thread owns it." 时,这意味着您正在尝试访问另一个并发线程上的对象。为了向您展示如何解决这个问题,我想举一个例子。首先,您必须找到每个 运行time 对象,例如列表框和列表视图等。 (基本上是 GUI 控件)。他们 运行 在 GUI 线程上。当您尝试在另一个线程上 运行 它们时,例如后台工作者或任务线程,会出现错误。所以这就是你想要做的:
//Lets say i got a listBox i want to update in realtime
//this method is for the purpose of the example running async(background)
public void method(){
//get data to add to listBox1;
//listBox1.Items.Add(item); <-- gives the error
//what you want to do:
Invoke(new MethodInvoker(delegate { listBox1.Items.Add(item); }));
//This invokes another thread, that we can use to access the listBox1 on.
//And this should work
}
希望对您有所帮助。
我将专注于您原来的问题,不包括异步部分。
添加新选项卡时未正确选择选项卡的原因是因为您在 CollectionChanged
事件处理程序中设置了 Selected
值。引发事件会导致按添加处理程序的顺序顺序调用处理程序。由于您在构造函数中添加了处理程序,因此它将始终是第一个被调用的,重要的是,它将始终在更新 TabControl
的那个之前被调用。因此,当您在处理程序中设置 Selected
属性 时,TabControl
还 "know" collection。更准确地说,选项卡的 header 容器尚未生成,并且无法将其标记为已选中(这会导致您缺少视觉效果),而且最终生成时也不会。 TabControl.SelectedItem
仍在更新,因此您可以看到选项卡的内容,但这也会导致 header 之前标记为已选中的容器未标记,最终您最终没有看到任何选项卡被选中。
根据您的需要,有多种方法可以解决此问题。如果添加新标签的唯一方法是通过 AddNewTabCommand
,您可以修改 AddNewTab
方法:
private void AddNewTab()
{
var tab = new Tab(GetNextName(), GetRandomContent());
Tabs.Add(tab);
Selected = tab;
}
在这种情况下,您不应在 CollectionChanged
处理程序中设置 Selected
值,因为它会阻止 PropertyChanged
在正确的时间被引发。
如果 AddNewTabCommand
不是添加选项卡的唯一方法,我通常做的是创建一个专用的 collection 来完成所需的逻辑(这个 class 嵌套在TabGroup
):
private class TabsCollection : ObservableCollection<Tab>
{
public TabsCollection(TabGroup owner)
{
this.owner = owner;
}
private TabGroup owner;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e); //this will update the TabControl
var newItems = e.NewItems?.Cast<Tab>()?.ToList();
if (newItems?.Any() == true)
owner.Selected = newItems.Last();
}
}
然后在TabGroup
构造函数中简单实例化collection:
Tabs = new TabsCollection(this);
如果这种情况出现在不同的地方并且您不喜欢重复您的代码,您可以创建一个可重复使用的 collection class:
public class MyObservableCollection<T> : ObservableCollection<T>
{
public event NotifyCollectionChangedEventHandler AfterCollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
AfterCollectionChanged?.Invoke(this, e);
}
}
然后在您需要确保所有 CollectionChanged
订阅者都已收到通知时订阅 AfterCollectionChanged
。
我有一个带标签的面板。我的此面板视图模型包含 ObservableCollection
个选项卡视图模型,以及一个 属性 个 select 选项卡视图模型。
当某些操作请求聚焦选项卡或创建新选项卡时,我会更改 Selected
并且选项卡 selection 会正确更改,差不多,因为内容有效,但所有headers 看起来什么都没有 selected。
我找到了一个解决方案,说要将 IsAsync=True
添加到我的绑定中。这解决了问题,但增加了一堆新问题。
第一件事是,当我 运行 在调试模式下编程时,添加带按钮的选项卡工作正常,选项卡被切换并正确 selected 但是当我尝试单击选项卡以 select 我得到异常
The calling thread cannot access this object because a different thread owns it.
设置 属性 表示当前 selected 选项卡时抛出:
private Tab selected;
public Tab Selected
{
get { return Selected; }
set { SetProperty(ref Selected, value); } // <<< here (I use prism BindableBase)
}
另一个问题是,当我快速切换标签时,可能会出现这样的情况:我有 Tab1 selected 但它显示的是 Tab2 的内容,多切换几次标签就会恢复正常。
我的问题是,我该如何解决这个问题,即在更改 Selected
时让我的选项卡 headers selected(有点突出显示),而不会出现评估 IsAsync
原因。
编辑
这是允许重现问题的代码。它使用棱镜 6.1.0
MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<StackPanel DockPanel.Dock="Top"
Orientation="Horizontal"
Margin="0,5"
Height="25">
<Button
Command="{Binding AddNewTabCommand}"
Content="New Tab"
Padding="10,0"/>
<Button
Command="{Binding OtherCommand}"
Content="Do nothing"
Padding="10,0"/>
</StackPanel>
<TabControl
SelectedItem="{Binding Selected, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, IsAsync=True}" <!--remove IsAsync to break tab header selecting-->
ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Margin="5"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBox Text="{Binding Text}"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</DockPanel>
</Window>
后面的代码:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new TabGroup();
}
}
Tab.cs
public class Tab : BindableBase
{
public Tab(string name, string text)
{
this.name = name;
this.text = text;
}
private string name;
public string Name
{
get { return name; }
set { SetProperty(ref name, value); }
}
private string text;
public string Text
{
get { return text; }
set { SetProperty(ref text, value); }
}
}
TabGroup.cs
public class TabGroup : BindableBase
{
private Random random;
public TabGroup()
{
this.random = new Random();
this.addNewTabCommand = new Lazy<DelegateCommand>(() => new DelegateCommand(AddNewTab, () => true));
this.otherCommand = new Lazy<DelegateCommand>(() => new DelegateCommand(Method, () => Selected != null).ObservesProperty(() => Selected));
Tabs.CollectionChanged += TabsChanged;
}
private void Method()
{
}
private void TabsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var newItems = e.NewItems?.Cast<Tab>().ToList();
if (newItems?.Any() == true)
{
Selected = newItems.Last();
}
}
private void AddNewTab()
{
Tabs.Add(new Tab(GetNextName(), GetRandomContent()));
}
private string GetRandomContent()
{
return random.Next().ToString();
}
private int num = 0;
private string GetNextName() => $"{num++}";
private Tab selected;
public Tab Selected
{
get { return selected; }
set { SetProperty(ref selected, value); }
}
public ObservableCollection<Tab> Tabs { get; } = new ObservableCollection<Tab>();
private readonly Lazy<DelegateCommand> addNewTabCommand;
public DelegateCommand AddNewTabCommand => addNewTabCommand.Value;
private readonly Lazy<DelegateCommand> otherCommand;
public DelegateCommand OtherCommand => otherCommand.Value;
}
准备这个让我弄清楚异常是从哪里来的。这是因为 OtherCommand 观察 selected 属性。我仍然不知道如何使它正确。对我来说最重要的是让选项卡在应该的时候被 selected,这样 selected 的选项卡就不会与选项卡控件显示的内容不同步。
这是一个 github 代码库
当您收到错误 "The calling thread cannot access this object because a different thread owns it." 时,这意味着您正在尝试访问另一个并发线程上的对象。为了向您展示如何解决这个问题,我想举一个例子。首先,您必须找到每个 运行time 对象,例如列表框和列表视图等。 (基本上是 GUI 控件)。他们 运行 在 GUI 线程上。当您尝试在另一个线程上 运行 它们时,例如后台工作者或任务线程,会出现错误。所以这就是你想要做的:
//Lets say i got a listBox i want to update in realtime
//this method is for the purpose of the example running async(background)
public void method(){
//get data to add to listBox1;
//listBox1.Items.Add(item); <-- gives the error
//what you want to do:
Invoke(new MethodInvoker(delegate { listBox1.Items.Add(item); }));
//This invokes another thread, that we can use to access the listBox1 on.
//And this should work
}
希望对您有所帮助。
我将专注于您原来的问题,不包括异步部分。
添加新选项卡时未正确选择选项卡的原因是因为您在 CollectionChanged
事件处理程序中设置了 Selected
值。引发事件会导致按添加处理程序的顺序顺序调用处理程序。由于您在构造函数中添加了处理程序,因此它将始终是第一个被调用的,重要的是,它将始终在更新 TabControl
的那个之前被调用。因此,当您在处理程序中设置 Selected
属性 时,TabControl
还 "know" collection。更准确地说,选项卡的 header 容器尚未生成,并且无法将其标记为已选中(这会导致您缺少视觉效果),而且最终生成时也不会。 TabControl.SelectedItem
仍在更新,因此您可以看到选项卡的内容,但这也会导致 header 之前标记为已选中的容器未标记,最终您最终没有看到任何选项卡被选中。
根据您的需要,有多种方法可以解决此问题。如果添加新标签的唯一方法是通过 AddNewTabCommand
,您可以修改 AddNewTab
方法:
private void AddNewTab()
{
var tab = new Tab(GetNextName(), GetRandomContent());
Tabs.Add(tab);
Selected = tab;
}
在这种情况下,您不应在 CollectionChanged
处理程序中设置 Selected
值,因为它会阻止 PropertyChanged
在正确的时间被引发。
如果 AddNewTabCommand
不是添加选项卡的唯一方法,我通常做的是创建一个专用的 collection 来完成所需的逻辑(这个 class 嵌套在TabGroup
):
private class TabsCollection : ObservableCollection<Tab>
{
public TabsCollection(TabGroup owner)
{
this.owner = owner;
}
private TabGroup owner;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e); //this will update the TabControl
var newItems = e.NewItems?.Cast<Tab>()?.ToList();
if (newItems?.Any() == true)
owner.Selected = newItems.Last();
}
}
然后在TabGroup
构造函数中简单实例化collection:
Tabs = new TabsCollection(this);
如果这种情况出现在不同的地方并且您不喜欢重复您的代码,您可以创建一个可重复使用的 collection class:
public class MyObservableCollection<T> : ObservableCollection<T>
{
public event NotifyCollectionChangedEventHandler AfterCollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
AfterCollectionChanged?.Invoke(this, e);
}
}
然后在您需要确保所有 CollectionChanged
订阅者都已收到通知时订阅 AfterCollectionChanged
。