4
votes

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?

2
+1 for excellent question. Interested to hear the answer - Dr. Andrew Burnett-Thompson
have you tried extending TabControl to achieve this functionality? ..or maybe an attached property? - Jake Berger
@jberger: I'm fairly new to WPF, and I wouldn't really know where to begin. You're welcome to post a solution below. - Fueled

2 Answers

3
votes

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.

1
votes

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.