4

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?

H.B.
  • 166,899
  • 29
  • 327
  • 400
Fueled
  • 8,776
  • 9
  • 29
  • 31

2 Answers2

3

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.

Rachel
  • 130,264
  • 66
  • 304
  • 490
  • +1 for last paragraph, should be enough to just just change the template of the placeholder to a button though. – H.B. Jan 20 '12 at 17:15
  • This looks promising. I'm gonna have a look at replacing the TabItem with a button when I get back to work on Monday. – Fueled Jan 20 '12 at 17:20
  • I didn't go the whole way but only changed the NewButtonHeaderTemplate to use a button, which works very well. I might have to, since the user can still "miss" the button and click on the tab header (because the button doesn't take the whole width and height), but for the meantime it's a good enough solution. Thanks! – Fueled Jan 23 '12 at 08:16
1

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.

Community
  • 1
  • 1
JHunz
  • 323
  • 1
  • 7
  • I was about to do something like that before posting here, but couldn't figure out how to check for MS.Internal.NameObject. I'll try the suggestion in the link you posted, just for the sake of trying. But I do feel like this solution is just working around the real problem. – Fueled Jan 20 '12 at 19:50
  • I can't seem to bring that option to work. Running the code in the accepted answer on that post, I can indeed get the "DisconnectedItem" object, but for NewItemPlaceholder, "var t = typeof(BindingExpressionBase).GetField("NewItemPlaceholder", BindingFlags.Static | BindingFlags.NonPublic);" always returns null. – Fueled Jan 23 '12 at 07:47
  • The other way around is to check that my SelectedListener property is of type Listener, and only call OnPropertyChanged if it is. I found myself having to use that anyway when removing tabs. – Fueled Jan 23 '12 at 08:09