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

时间:2016-06-04 14:25:19

标签: c# wpf asynchronous data-binding tabcontrol

我有一个带标签的面板。我的此面板的视图模型包含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原因的问题。

修改

以下是允许重现问题的代码。它使用prism 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

2 个答案:

答案 0 :(得分:1)

我会关注你原来的问题,没有异步部分。

添加新标签页时未正确选择标签的原因是您在Selected事件处理程序中设置了CollectionChanged值。引发事件会导致顺序调用处理程序按顺序添加它们。由于您在构造函数中添加了处理程序,因此它始终是第一个被调用的处理程序,重要的是,它将始终在更新TabControl之前调用。因此,当您在处理程序中设置Selected属性时,TabControl尚未&#34;知道&#34; 有这样的标签在集合中。更准确地说,标签的标题容器尚未生成,并且无法标记为已选择(这会导致您失去的视觉效果),此外,它不会被标记为何时出现。终于产生了。 TabControl.SelectedItem仍然会更新,因此您会看到该标签的内容,但它也会导致之前标记为已选中的标题容器未标记,最终您最终没有明显选择标签。

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

private void AddNewTab()
{
    var tab = new Tab(GetNextName(), GetRandomContent());
    Tabs.Add(tab);
    Selected = tab;
}

在这种情况下,您不应在Selected处理程序中设置CollectionChanged值,因为它会阻止在正确的时间引发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);
    }
}

然后在需要确保已通知所有AfterCollectionChanged订阅者时订阅CollectionChanged

答案 1 :(得分:0)

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

希望它有所帮助。