添加选项卡时 TabControl 的 SelectedItem 被 NewItemPlaceholder 覆盖

发布于 2024-12-28 02:58:28 字数 4749 浏览 1 评论 0原文

我正在开发一个 WPF TabControl,其最后一项始终是添加新选项卡的按钮,类似于 Firefox: screenshot 1

TabControl 的 ItemSource 绑定到 ObservableCollection,通过此“+”按钮将项目添加到集合非常有效出色地。我遇到的唯一问题是,单击“+”选项卡后,我一生都无法将新创建的选项卡(或任何其他现有选项卡)设置为焦点,因此当添加选项卡时,UI看起来像这样:

screenshot 2

为了解释一下我如何实现这种“特殊”选项卡行为,TabControl 是模板化的及其 NewButtonHeaderTemplate有一个控件(在我的例子中是图像)在视图模型中调用 AddListener 命令(仅显示相关代码):

<Window x:Class="AIS2.PortListener.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ais="http://www.leica-geosystems.com/xaml"
    xmlns:l="clr-namespace:AIS2.PortListener"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"
    DataContext="{Binding Source={StaticResource Locator}>

<Window.Resources>
    <ResourceDictionary>
       <DataTemplate x:Key="newTabButtonHeaderTemplate">
            <Grid>
                <Image Source="..\Images\add.png" Height="16" Width="16">
                </Image>
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseLeftButtonDown">
                        <cmd:EventToCommand 
                         Command="{Binding Source={StaticResource Locator},
                                   Path=PortListenerVM.AddListenerCommand}"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Grid>
        </DataTemplate>

        <DataTemplate x:Key="newTabButtonContentTemplate"/>

        <DataTemplate x:Key="itemHeaderTemplate">
            <TextBlock Text="{Binding Name}"/>
        </DataTemplate>

        <DataTemplate x:Key="itemContentTemplate">
            <l:ListenerControl></l:ListenerControl>
        </DataTemplate>

        <l:ItemHeaderTemplateSelector x:Key="headerTemplateSelector" 
           NewButtonHeaderTemplate="{StaticResource newTabButtonHeaderTemplate}" 
           ItemHeaderTemplate="{StaticResource itemHeaderTemplate}"/>
        <l:ItemContentTemplateSelector x:Key="contentTemplateSelector"
           NewButtonContentTemplate="{StaticResource newTabButtonContentTemplate}"
           ItemContentTemplate="{StaticResource itemContentTemplate}"/>
    </ResourceDictionary>
</Window.Resources>

<TabControl Name="MainTab" Grid.Row="2" ItemsSource="{Binding Listeners}" 
            ItemTemplateSelector="{StaticResource headerTemplateSelector}"
            ContentTemplateSelector="{StaticResource contentTemplateSelector}" 
            SelectedItem="{Binding SelectedListener}">
</TabControl>

AddListener 命令只是将一个项目添加到 ObservableCollection 中,该项目具有更新 TabControl 的 ItemSource 并添加新项目的效果tab:

private ObservableCollection<Listener> _Listeners;
public ObservableCollection<Listener> Listeners
{
    get { return _Listeners; }
}

private object _SelectedListener;
public object SelectedListener
{
    get { return _SelectedListener; }
    set
    {
        _SelectedListener = value;
        OnPropertyChanged("SelectedListener");
    }
}

public PortListenerViewModel()
{         
    // Place the "+" tab at the end of the tab control
    var itemsView = (IEditableCollectionView)CollectionViewSource.GetDefaultView(_Listeners);
    itemsView.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtEnd;
}

private RelayCommand _AddListenerCommand;
public RelayCommand AddListenerCommand
{
    get
    {
        if (_AddListenerCommand == null)
            _AddListenerCommand = new RelayCommand(param => this.AddListener());

        return _AddListenerCommand;
    }
}

public void AddListener()
{
    var newListener = new TCPListener(0, "New listener");
    this.Listeners.Add(newListener);
    // The following two lines update the property, but the focus does not change
    //this.SelectedListener = newListener;
    //this.SelectedListener = this.Listeners[0];
}

但是设置 SelectedListener 属性不起作用,即使 TabControl 的 SelectedItem 已绑定到它。它一定与 WPF 中更新内容的顺序有关,因为如果我在 SelectedListener 的 set 中设置断点,我可以看到发生以下情况:

  1. this.Listeners.Add( newListener);
  2. this.SelectedListener = newListener;
  3. SelectedListener set 被正确的监听器对象调用
  4. SelectedListener set 被调用NewItemPlaceholder 对象(根据调试器,属于 MS.Internal.NamedObject 类型)

有没有办法可以解决此问题?我的方法有误吗?

I'm working on a WPF TabControl whose last item is always a button to add a new tab, similar to Firefox:
screenshot 1

The TabControl's ItemSource is bound to an ObservableCollection, and adding an item to the collection via this "+" button works very well. The only problem I'm having is that, after having clicked the "+" tab, I cannot for the life of me set the newly created (or any other existing tab) to focus, and so when a tab is added, the UI looks like this:

screenshot 2

To explain a bit how I'm achieving this "special" tab behavior, the TabControl is templated and its NewButtonHeaderTemplate has a control (Image in my case) which calls the AddListener Command in the view-model (only relevant code is shown):

<Window x:Class="AIS2.PortListener.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ais="http://www.leica-geosystems.com/xaml"
    xmlns:l="clr-namespace:AIS2.PortListener"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"
    DataContext="{Binding Source={StaticResource Locator}>

<Window.Resources>
    <ResourceDictionary>
       <DataTemplate x:Key="newTabButtonHeaderTemplate">
            <Grid>
                <Image Source="..\Images\add.png" Height="16" Width="16">
                </Image>
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseLeftButtonDown">
                        <cmd:EventToCommand 
                         Command="{Binding Source={StaticResource Locator},
                                   Path=PortListenerVM.AddListenerCommand}"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Grid>
        </DataTemplate>

        <DataTemplate x:Key="newTabButtonContentTemplate"/>

        <DataTemplate x:Key="itemHeaderTemplate">
            <TextBlock Text="{Binding Name}"/>
        </DataTemplate>

        <DataTemplate x:Key="itemContentTemplate">
            <l:ListenerControl></l:ListenerControl>
        </DataTemplate>

        <l:ItemHeaderTemplateSelector x:Key="headerTemplateSelector" 
           NewButtonHeaderTemplate="{StaticResource newTabButtonHeaderTemplate}" 
           ItemHeaderTemplate="{StaticResource itemHeaderTemplate}"/>
        <l:ItemContentTemplateSelector x:Key="contentTemplateSelector"
           NewButtonContentTemplate="{StaticResource newTabButtonContentTemplate}"
           ItemContentTemplate="{StaticResource itemContentTemplate}"/>
    </ResourceDictionary>
</Window.Resources>

<TabControl Name="MainTab" Grid.Row="2" ItemsSource="{Binding Listeners}" 
            ItemTemplateSelector="{StaticResource headerTemplateSelector}"
            ContentTemplateSelector="{StaticResource contentTemplateSelector}" 
            SelectedItem="{Binding SelectedListener}">
</TabControl>

The AddListener command simply adds an item to the ObservableCollection which has for effect to update the TabControl's ItemSource and add a new tab:

private ObservableCollection<Listener> _Listeners;
public ObservableCollection<Listener> Listeners
{
    get { return _Listeners; }
}

private object _SelectedListener;
public object SelectedListener
{
    get { return _SelectedListener; }
    set
    {
        _SelectedListener = value;
        OnPropertyChanged("SelectedListener");
    }
}

public PortListenerViewModel()
{         
    // Place the "+" tab at the end of the tab control
    var itemsView = (IEditableCollectionView)CollectionViewSource.GetDefaultView(_Listeners);
    itemsView.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtEnd;
}

private RelayCommand _AddListenerCommand;
public RelayCommand AddListenerCommand
{
    get
    {
        if (_AddListenerCommand == null)
            _AddListenerCommand = new RelayCommand(param => this.AddListener());

        return _AddListenerCommand;
    }
}

public void AddListener()
{
    var newListener = new TCPListener(0, "New listener");
    this.Listeners.Add(newListener);
    // The following two lines update the property, but the focus does not change
    //this.SelectedListener = newListener;
    //this.SelectedListener = this.Listeners[0];
}

But setting the SelectedListener property does not work, even though the TabControl's SelectedItem is bound to it. It must have something to do with the order in which things get updated in WPF, because if I set a breakpoint in the SelectedListener's set I can see the following happening:

  1. this.Listeners.Add(newListener);
  2. this.SelectedListener = newListener;
  3. SelectedListener set gets called with correct Listener object
  4. SelectedListener set gets called with NewItemPlaceholder object (of type MS.Internal.NamedObject according to the debugger)

Is there a way that I can work around this issue? Do I have the wrong approach?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

云裳 2025-01-04 02:58:28

我认为当您单击新选项卡时,您会触发两个事件: MouseLeftButtonDownTabControl.SelectionChanged

我认为它们都在排队,然后一次处理一个。

因此,您的项目将被添加,设置为选定状态,然后在重新绘制发生之前,会发生 SelectionChanged 事件以将选择更改为 [+] 选项卡。

也许尝试使用Dispatcher来设置SelectedItem,以便它在TabControl更改其选择之后发生。或者,如果用户尝试切换到 NewTab,它会取消 SelectionChanged 事件,这样所选选项卡实际上不会更改(当然,SelectedTab 将是你的 NewItem 因为 MouseDown 事件将会发生)

当我过去做了类似的事情时,我实际上覆盖了 TabControl Template 将 AddTab 按钮创建为 Button ,而不是作为 TabItem。我想建议这样做,而不是首先使用 NewItemPlaceholder ,但我从未尝试过使用 NewItemPlaceholder 所以不知道它是否更好或比覆盖模板更糟糕。

I think you are triggering two events when you click the new tab: MouseLeftButtonDown and TabControl.SelectionChanged

I think they're both getting queued, then processing one at a time.

So your item is getting added, set as selected, and then before the re-draw occurs the SelectionChanged event occurs to change the selection to the [+] tab.

Perhaps try using the Dispatcher to set the SelectedItem so it occurs after the TabControl changes it's selection. Or make it so if the user tries to switch to the NewTab, it cancels the SelectionChanged event so the selected tab doesn't actually change (of course, the SelectedTab will be your NewItem since the MouseDown event will have occurred)

When I did something like this in the past, I actually overwrote the TabControl Template to create the AddTab button as a Button, not as a TabItem. I want to suggest doing that instead of using the NewItemPlaceholder in the first place, but I've never tried working with the NewItemPlaceholder so don't really know if it's better or worse than overwriting the Template.

怪我太投入 2025-01-04 02:58:28

看看这篇关于哨兵对象的文章: WPF Sentinel 对象以及如何检查内部类型
有多种方法可以解决它们的问题,该帖子提供了其中一种。

Take a look at this post regarding sentinel objects: WPF Sentinel objects and how to check for an internal type
There are several ways to work around issues with them, that post offers one of them.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文