TabControl WPF 问题中与 SelectedItem 的异步绑定

Posted

技术标签:

【中文标题】TabControl WPF 问题中与 SelectedItem 的异步绑定【英文标题】:Async binding to SelectedItem in TabControl WPF issues 【发布时间】:2016-06-04 14:25:19 【问题描述】:

我有一个带有标签的面板。我的这个面板的视图模型包含ObservableCollection 的选项卡视图模型,以及选定选项卡的属性。

当某些操作请求聚焦一个选项卡或创建一个新选项卡时,我更改Selected 并且选项卡选择正确更改,差不多,因为内容有效,但所有标题看起来都没有被选中。

我找到了一个解决方案,将IsAsync=True 添加到我的绑定中。这解决了问题,但增加了许多新问题。

第一件事是,当我在调试模式下运行程序时,添加带有按钮的选项卡可以正常工作,选项卡会被切换并正确选择,但是当我尝试单击选项卡以选择它时出现异常

调用线程无法访问此对象,因为另一个线程拥有它。

在设置代表当前选定标签的属性时抛出:

private Tab selected;
public Tab Selected

    get  return Selected; 
    set  SetProperty(ref Selected, value);  // <<< here (I use prism BindableBase)

另一个问题是,当我快速切换选项卡时,可能会出现这样一种情况,即我选择了 Tab1,但它显示了 Tab2 的内容,多次切换选项卡会使事情恢复正常。

我的问题是,我该如何解决这个问题,即在 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 观察选定的属性。我仍然不知道如何使它正确。对我来说最重要的是让选项卡在应该选择的时候被选中,这样选定的选项卡就不会与选项卡控件显示的内容不同步。

这是一个带有此代码的 github 存储库

https://github.com/lukaszwawrzyk/TabIssue

【问题讨论】:

该错误是由从另一个线程更改绑定到 UI 的内容引起的,但在您的代码中没有证据表明这一点。请提供minimal reproducible example。 好的,我能够重现我所说的一切。我会在一分钟内输入代码。 【参考方案1】:

我将专注于您的原始问题,不包括异步部分。

添加新选项卡时选项卡未正确选择的原因是因为您在CollectionChanged 事件处理程序中设置了Selected 值。引发事件会导致处理程序的顺序调用按添加顺序。由于您在构造函数中添加了处理程序,因此它将始终是第一个被调用的处理程序,重要的是,它将始终在更新TabControl 的处理程序之前被调用。因此,当您在处理程序中设置Selected 属性时,TabControl 还不“知道”集合中有这样一个选项卡。更准确地说,选项卡的标题容器尚未生成,并且无法将其标记为已选中(这会导致您缺少视觉效果),而且最终生成时也不会。 TabControl.SelectedItem 仍在更新,因此您可以看到选项卡的内容,但它也会导致之前标记为已选择的标题容器未标记,最终您最终没有明显选择选项卡。

根据您的需要,有几种方法可以解决此问题。如果添加新标签的唯一方法是通过AddNewTabCommand,您只需修改AddNewTab 方法:

private void AddNewTab()

    var tab = new Tab(GetNextName(), GetRandomContent());
    Tabs.Add(tab);
    Selected = tab;

在这种情况下,您不应该在CollectionChanged 处理程序中设置Selected 值,因为它会阻止PropertyChanged 在正确的时间被引发。

如果AddNewTabCommand不是添加标签的唯一方法,我通常会创建一个专门的集合来执行所需的逻辑(这个类嵌套在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 构造函数中实例化集合:

Tabs = new TabsCollection(this);

如果这种情况出现在不同的地方并且您不喜欢重复您的代码,您可以创建一个可重用的集合类:

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

【讨论】:

感谢您提供如此详细的回答!我有更多添加选项卡的方法,但我只是在这两种方法中都使用了第一种方法。这是如此简单和如此明显,以至于我不知道我为什么不首先这样做。现在一切似乎都很完美。【参考方案2】:

当您收到错误“调用线程无法访问此对象,因为另一个线程拥有它”时。这意味着您正在尝试访问另一个并发线程上的对象。为了向您展示如何解决这个问题,我想举一个例子。首先,您必须找到每个运行时对象,例如列表框和列表视图等。 (基本上是 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

希望对您有所帮助。

【讨论】:

我认为我的问题可能来自TabsChanged 方法,但我不确定除非某些命令侦听此属性,否则它不会发生。这是什么Invoke?我在哪里可以找到它?还要注意我使用 mvvm,我不直接访问控件。此外,我不知道即使解决了异常,这是否会解决我的其他问题。

以上是关于TabControl WPF 问题中与 SelectedItem 的异步绑定的主要内容,如果未能解决你的问题,请参考以下文章

WPF TabControl 标头问题

DockPanel 中的 WPF4 TabControl/Grid 隐藏了 StatusBar

是否可以在 WPF TabControl 中左对齐标题?

WPF 自定义TabControl控件样式

TabControl 内的 WPF ContentControl 不显示 DataTemplates

WPF Caliburn.Micro 和 TabControl 与 UserControls 问题