TabControl with Add New Tab Button (+)

WpfButtonTabsTabcontrol

Wpf Problem Overview


What is the proper way of adding a '+' button tab at the end of all the tab items in the tab strip of a tab control in WPF?

  1. It should work correctly with multiple tab header rows.
  2. It should be at the end of all tab items
  3. Tab cycling should work correctly (Alt + Tab), that is, the + tab should be skipped.
  4. I shouldn't have to modify the source collection I am binding to. That is, the control should be reusable.
  5. The solution should work with MVVM

Enter image description here

enter image description here

To be more precise, the button should appear exactly as an additional last tab and not as a separate button somewhere on the right of all tab strip rows.

I am just looking for the general approach to doing this.

Google throws many examples, but if you dig a little deep none of them satisfy all the above five points.

Wpf Solutions


Solution 1 - Wpf

An almost complete solution using IEditableCollectionView:

ObservableCollection<ItemVM> _items;
public ObservableCollection<ItemVM> Items
{
    get
    {
        if (_items == null)
        {
            _items = new ObservableCollection<ItemVM>();
            var itemsView = (IEditableCollectionView)CollectionViewSource.GetDefaultView(_items);
            itemsView.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtEnd;
        }

        return _items;
    }
}

private DelegateCommand<object> _newCommand;
public DelegateCommand<object> NewCommand
{
    get
    {
        if (_newCommand == null)
        {
            _newCommand = new DelegateCommand<object>(New_Execute);
        }

        return _newCommand;
    }
}

private void New_Execute(object parameter)
{
    Items.Add(new ItemVM());
}

<DataTemplate x:Key="newTabButtonContentTemplate">
    <Grid/>
</DataTemplate>

<DataTemplate x:Key="newTabButtonHeaderTemplate">
    <Button Content="+"
        Command="{Binding ElementName=parentUserControl, Path=DataContext.NewCommand}"/>
</DataTemplate>

<DataTemplate x:Key="itemContentTemplate">
    <Grid/>
</DataTemplate>

<DataTemplate x:Key="itemHeaderTemplate">
    <TextBlock Text="TabItem_test"/>
</DataTemplate>

<vw:TemplateSelector x:Key="headerTemplateSelector"
                           NewButtonTemplate="{StaticResource newTabButtonHeaderTemplate}"
                           ItemTemplate="{StaticResource itemHeaderTemplate}"/>

<vw:TemplateSelector x:Key="contentTemplateSelector"
                            NewButtonTemplate="{StaticResource newTabButtonContentTemplate}"
                            ItemTemplate="{StaticResource itemContentTemplate}"/>

<TabControl ItemsSource="{Binding Items}"
        ItemTemplateSelector="{StaticResource headerTemplateSelector}"
        ContentTemplateSelector="{StaticResource contentTemplateSelector}"/>

public class TemplateSelector : DataTemplateSelector
{
    public DataTemplate ItemTemplate { get; set; }
    public DataTemplate NewButtonTemplate { get; set; }

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        if (item == CollectionView.NewItemPlaceholder)
        {
            return NewButtonTemplate;
        }
        else
        {
            return ItemTemplate;
        }
    }
}

Enter code here

It's almost complete, because the tab cycle doesn't skip the '+' tab, and will show empty content (which is not exactly great, but I can live with it until a better solution come around...).

Solution 2 - Wpf

Existing answers were too complex for me and I am lazy. So, I tried to implement a very simple idea.

  1. Always add [+] tab to the last.
  2. When the last tab is selected, make it as a new tab, and add another last tab.

The idea was simple, but the damn WPF is verbose, so the code became a little bit long. But it probably is very simple to understand... because even I did.

enter image description here

Code behind.

public partial class MainWindow : Window
{
	int TabIndex = 1;
	ObservableCollection<TabVM> Tabs = new ObservableCollection<TabVM>();
	public MainWindow()
	{
		InitializeComponent();
		var tab1 = new TabVM()
		{
			Header = $"Tab {TabIndex}",
			Content = new ContentVM("First tab", 1)
		};
		Tabs.Add(tab1);
		AddNewPlusButton();

		MyTabControl.ItemsSource = Tabs;
		MyTabControl.SelectionChanged += MyTabControl_SelectionChanged;

	}

	private void MyTabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
	{
		if(e.Source is TabControl)
		{
			var pos = MyTabControl.SelectedIndex;
			if (pos!=0 && pos == Tabs.Count-1) //last tab
			{
				var tab = Tabs.Last();
				ConvertPlusToNewTab(tab);
				AddNewPlusButton();
			}
		}
	}

	void ConvertPlusToNewTab(TabVM tab)
	{
		//Do things to make it a new tab.
		TabIndex++;
		tab.Header = $"Tab {TabIndex}";
		tab.IsPlaceholder = false;
		tab.Content = new ContentVM("Tab content", TabIndex);
	}

	void AddNewPlusButton()
	{
		var plusTab = new TabVM()
		{
			Header = "+",
			IsPlaceholder = true
		};
		Tabs.Add(plusTab);
	}

	class TabVM:INotifyPropertyChanged
	{
		string _Header;
		public string Header
		{
			get => _Header;
			set
			{
				_Header = value;
				OnPropertyChanged();
			}
		}

		bool _IsPlaceholder = false;
		public bool IsPlaceholder
		{
			get => _IsPlaceholder;
			set
			{
				_IsPlaceholder = value;
				OnPropertyChanged();
			}
		}

		ContentVM _Content = null;
		public ContentVM Content
		{
			get => _Content;
			set
			{
				_Content = value;
				OnPropertyChanged();
			}
		}

		public event PropertyChangedEventHandler PropertyChanged;
		void OnPropertyChanged([CallerMemberName] string property = "")
		{
			PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
		}
	}

	class ContentVM
	{
		public ContentVM(string name, int index)
		{
			Name = name;
			Index = index;
		}
		public string Name { get; set; }
		public int Index { get; set; }
	}

	private void OnTabCloseClick(object sender, RoutedEventArgs e)
	{
		var tab = (sender as Button).DataContext as TabVM;
		if (Tabs.Count>2)
		{ 
			var index = Tabs.IndexOf(tab);
			if(index==Tabs.Count-2)//last tab before [+]
			{
				MyTabControl.SelectedIndex--;
			}
			Tabs.RemoveAt(index);
		}
	}
}

XAML

<TabControl Name="MyTabControl">
    <TabControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Header, Mode=OneWay}" />
                <Button Click="OnTabCloseClick" Width="20" Padding="0" Margin="8 0 0 0" Content="X">
                    <Button.Style>
                        <Style TargetType="Button" x:Name="CloseButtonStyle">
                            <Setter Property="Visibility" Value="Visible"/>
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding IsPlaceholder}" Value="True">
                                    <Setter Property="Visibility" Value="Collapsed"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Button.Style>
                </Button>
            </StackPanel>
        </DataTemplate>
    </TabControl.ItemTemplate>

    <TabControl.ContentTemplate>
        <DataTemplate>
            <ContentControl>
                <ContentControl.Resources>
                    <ContentControl x:Key="TabContentTemplate">
                        <StackPanel DataContext="{Binding Content}" Orientation="Vertical">
                            <TextBlock Text="{Binding Path=Name}"/>
                            <TextBlock Text="{Binding Path=Index}"/>
                        </StackPanel>
                    </ContentControl>
                </ContentControl.Resources>
                <ContentControl.Style>
                    <Style TargetType="ContentControl">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding IsPlaceholder}" Value="True">
                                <Setter Property="Content"
                                        Value="{x:Null}"/>                                    
                            </DataTrigger>
                            <DataTrigger Binding="{Binding IsPlaceholder}" Value="False">
                                <Setter Property="Content"
                                        Value="{StaticResource TabContentTemplate}"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </ContentControl.Style>
            </ContentControl>
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>

Solution 3 - Wpf

I used a modification of the tab control template and binding to the AddNewItemCommand command in my view model. XAML:

<TabControl x:Class="MyNamespace.MyTabView"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            ItemsSource="{Binding MyItemSource}"
            SelectedIndex="{Binding LastSelectedIndex}"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Control.Template>
        <ControlTemplate TargetType="{x:Type TabControl}">
            <Grid ClipToBounds="true"
                  SnapsToDevicePixels="true"
                  KeyboardNavigation.TabNavigation="Local">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition x:Name="ColumnDefinition0" />
                    <ColumnDefinition x:Name="ColumnDefinition1"
                                      Width="0" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition x:Name="RowDefinition0"
                                   Height="Auto" />
                    <RowDefinition x:Name="RowDefinition1"
                                   Height="*" />
                </Grid.RowDefinitions>
                <StackPanel Grid.Column="0"
                            Grid.Row="0"
                            Orientation="Horizontal"
                            x:Name="HeaderPanel">
                    <TabPanel x:Name="_HeaderPanel"
                              IsItemsHost="true"
                              Margin="2,2,2,0"
                              KeyboardNavigation.TabIndex="1"
                              Panel.ZIndex="1" />
                    <Button Content="+"
                            Command="{Binding AddNewItemCommand}" />
                </StackPanel>

                <Border x:Name="ContentPanel"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        Background="{TemplateBinding Background}"
                        Grid.Column="0"
                        KeyboardNavigation.DirectionalNavigation="Contained"
                        Grid.Row="1"
                        KeyboardNavigation.TabIndex="2"
                        KeyboardNavigation.TabNavigation="Local">
                    <ContentPresenter x:Name="PART_SelectedContentHost"
                                      ContentSource="SelectedContent"
                                      Margin="{TemplateBinding Padding}"
                                      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                </Border>
            </Grid>
            <ControlTemplate.Triggers>
                <Trigger Property="TabStripPlacement"
                         Value="Bottom">
                    <Setter Property="Grid.Row"
                            TargetName="HeaderPanel"
                            Value="1" />
                    <Setter Property="Grid.Row"
                            TargetName="ContentPanel"
                            Value="0" />
                    <Setter Property="Height"
                            TargetName="RowDefinition0"
                            Value="*" />
                    <Setter Property="Height"
                            TargetName="RowDefinition1"
                            Value="Auto" />
                    <Setter Property="Margin"
                            TargetName="HeaderPanel"
                            Value="2,0,2,2" />
                </Trigger>
                <Trigger Property="TabStripPlacement"
                         Value="Left">
                    <Setter Property="Orientation"
                            TargetName="HeaderPanel"
                            Value="Vertical" />
                    <Setter Property="Grid.Row"
                            TargetName="HeaderPanel"
                            Value="0" />
                    <Setter Property="Grid.Row"
                            TargetName="ContentPanel"
                            Value="0" />
                    <Setter Property="Grid.Column"
                            TargetName="HeaderPanel"
                            Value="0" />
                    <Setter Property="Grid.Column"
                            TargetName="ContentPanel"
                            Value="1" />
                    <Setter Property="Width"
                            TargetName="ColumnDefinition0"
                            Value="Auto" />
                    <Setter Property="Width"
                            TargetName="ColumnDefinition1"
                            Value="*" />
                    <Setter Property="Height"
                            TargetName="RowDefinition0"
                            Value="*" />
                    <Setter Property="Height"
                            TargetName="RowDefinition1"
                            Value="0" />
                    <Setter Property="Margin"
                            TargetName="HeaderPanel"
                            Value="2,2,0,2" />
                </Trigger>
                <Trigger Property="TabStripPlacement"
                         Value="Right">
                    <Setter Property="Orientation"
                            TargetName="HeaderPanel"
                            Value="Vertical" />
                    <Setter Property="Grid.Row"
                            TargetName="HeaderPanel"
                            Value="0" />
                    <Setter Property="Grid.Row"
                            TargetName="ContentPanel"
                            Value="0" />
                    <Setter Property="Grid.Column"
                            TargetName="HeaderPanel"
                            Value="1" />
                    <Setter Property="Grid.Column"
                            TargetName="ContentPanel"
                            Value="0" />
                    <Setter Property="Width"
                            TargetName="ColumnDefinition0"
                            Value="*" />
                    <Setter Property="Width"
                            TargetName="ColumnDefinition1"
                            Value="Auto" />
                    <Setter Property="Height"
                            TargetName="RowDefinition0"
                            Value="*" />
                    <Setter Property="Height"
                            TargetName="RowDefinition1"
                            Value="0" />
                    <Setter Property="Margin"
                            TargetName="HeaderPanel"
                            Value="0,2,2,2" />
                </Trigger>
                <Trigger Property="IsEnabled"
                         Value="false">
                    <Setter Property="Foreground"
                            Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Control.Template>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="5" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="{Binding Caption}" />
                <Button Content="x"
                        Grid.Column="2"
                        VerticalAlignment="Top"/>
            </Grid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</TabControl>

Code in the relevant view model looks like this:

public ICommand AddNewItemCommand
{
    get
    {
        return new DelegateCommand((param) =>
        {
            MyItemSource.Add(CreateMyValueViewModel());
        },
        (param) => MyItemSource != null);
    }
}

Pay attention: I wrapped TabPanel by StackPanel to flip the "+" button together with TabPanel regarding to value of property "TabStripPlacement". Without inheritance and without code-behind in your view.

Solution 4 - Wpf

I believe I have come up with a complete solution, I started with NVM's solution to create my template. And then referenced the DataGrid source code to come up with an extended TabControl capable of adding and removing items.

ExtendedTabControl.cs

public class ExtendedTabControl : TabControl
{
    public static readonly DependencyProperty CanUserAddTabsProperty = DependencyProperty.Register("CanUserAddTabs", typeof(bool), typeof(ExtendedTabControl), new PropertyMetadata(false, OnCanUserAddTabsChanged, OnCoerceCanUserAddTabs));

    public bool CanUserAddTabs
    {
        get { return (bool)GetValue(CanUserAddTabsProperty); }
        set { SetValue(CanUserAddTabsProperty, value); }
    }

    public static readonly DependencyProperty CanUserDeleteTabsProperty = DependencyProperty.Register("CanUserDeleteTabs", typeof(bool), typeof(ExtendedTabControl), new PropertyMetadata(true, OnCanUserDeleteTabsChanged, OnCoerceCanUserDeleteTabs));

    public bool CanUserDeleteTabs
    {
        get { return (bool)GetValue(CanUserDeleteTabsProperty); }
        set { SetValue(CanUserDeleteTabsProperty, value); }
    }

    public static RoutedUICommand DeleteCommand
    {
        get { return ApplicationCommands.Delete; }
    }

    public static readonly DependencyProperty NewTabCommandProperty = DependencyProperty.Register("NewTabCommand", typeof(ICommand), typeof(ExtendedTabControl));

    public ICommand NewTabCommand
    {
        get { return (ICommand)GetValue(NewTabCommandProperty); }
        set { SetValue(NewTabCommandProperty, value); }
    }

    private IEditableCollectionView EditableItems
    {
        get { return (IEditableCollectionView)Items; }
    }

    private bool ItemIsSelected
    {
        get
        {
            if (this.SelectedItem != CollectionView.NewItemPlaceholder)
                return true;

            return false;
        }
    }

    private static void OnCanExecuteDelete(object sender, CanExecuteRoutedEventArgs e)
    {
        ((ExtendedTabControl)sender).OnCanExecuteDelete(e);
    }

    private static void OnCanUserAddTabsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((ExtendedTabControl)d).UpdateNewItemPlaceholder();
    }

    private static void OnCanUserDeleteTabsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        // The Delete command needs to have CanExecute run.
        CommandManager.InvalidateRequerySuggested();
    }

    private static object OnCoerceCanUserAddTabs(DependencyObject d, object baseValue)
    {
        return ((ExtendedTabControl)d).OnCoerceCanUserAddOrDeleteTabs((bool)baseValue, true);
    }

    private static object OnCoerceCanUserDeleteTabs(DependencyObject d, object baseValue)
    {
        return ((ExtendedTabControl)d).OnCoerceCanUserAddOrDeleteTabs((bool)baseValue, false);
    }

    private static void OnExecutedDelete(object sender, ExecutedRoutedEventArgs e)
    {
        ((ExtendedTabControl)sender).OnExecutedDelete(e);
    }

    private static void OnSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue == CollectionView.NewItemPlaceholder)
        {
            var tc = (ExtendedTabControl)d;

            tc.Items.MoveCurrentTo(e.OldValue);
            tc.Items.Refresh();
        }
    }

    static ExtendedTabControl()
    {
        Type ownerType = typeof(ExtendedTabControl);
        
        DefaultStyleKeyProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(typeof(ExtendedTabControl)));
        SelectedItemProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(OnSelectionChanged));
        
        CommandManager.RegisterClassCommandBinding(ownerType, new CommandBinding(DeleteCommand, new ExecutedRoutedEventHandler(OnExecutedDelete), new CanExecuteRoutedEventHandler(OnCanExecuteDelete)));
    }

    protected virtual void OnCanExecuteDelete(CanExecuteRoutedEventArgs e)
    {
        // User is allowed to delete and there is a selection.
        e.CanExecute = CanUserDeleteTabs && ItemIsSelected; 
        e.Handled = true;
    }

    protected virtual void OnExecutedDelete(ExecutedRoutedEventArgs e)
    {
        if (ItemIsSelected)
        {
            int indexToSelect = -1;

            object currentItem = e.Parameter ?? this.SelectedItem;
            if (currentItem == this.SelectedItem)
                indexToSelect = Math.Max(this.Items.IndexOf(currentItem) - 1, 0);

            if (currentItem != CollectionView.NewItemPlaceholder)
                EditableItems.Remove(currentItem);

            if (indexToSelect != -1)
            {
                // This should focus the row and bring it into view. 
                SetCurrentValue(SelectedItemProperty, this.Items[indexToSelect]);
            }
        }

        e.Handled = true;
    }

    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    {
        base.OnItemsSourceChanged(oldValue, newValue);

        CoerceValue(CanUserAddTabsProperty);
        CoerceValue(CanUserDeleteTabsProperty);

        UpdateNewItemPlaceholder();
    }

    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        if (Keyboard.FocusedElement is TextBox)
            Keyboard.FocusedElement.RaiseEvent(new RoutedEventArgs(LostFocusEvent));
        
        base.OnSelectionChanged(e);
    }

    private bool OnCoerceCanUserAddOrDeleteTabs(bool baseValue, bool canUserAddTabsProperty)
    {
        // Only when the base value is true do we need to validate
        // that the user can actually add or delete rows. 
        if (baseValue)
        {
            if (!this.IsEnabled)
            {
                // Disabled TabControls cannot be modified. 
                return false;
            }
            else
            {
                if ((canUserAddTabsProperty && !this.EditableItems.CanAddNew) || (!canUserAddTabsProperty && !this.EditableItems.CanRemove))
                {
                    // The collection view does not allow the add or delete action.
                    return false;
                }
            }
        }

        return baseValue;
    }

    private void UpdateNewItemPlaceholder()
    {
        var editableItems = EditableItems;
        
        if (CanUserAddTabs)
        {
            // NewItemPlaceholderPosition isn't a DP but we want to default to AtEnd instead of None
            // (can only be done when canUserAddRows becomes true).  This may override the users intent
            // to make it None, however they can work around this by resetting it to None after making
            // a change which results in canUserAddRows becoming true.
            if (editableItems.NewItemPlaceholderPosition == NewItemPlaceholderPosition.None)
                editableItems.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtEnd;
        }
        else
        {
            if (editableItems.NewItemPlaceholderPosition != NewItemPlaceholderPosition.None)
                editableItems.NewItemPlaceholderPosition = NewItemPlaceholderPosition.None;
        }

        // Make sure the newItemPlaceholderRow reflects the correct visiblity 
        TabItem newItemPlaceholderTab = (TabItem)ItemContainerGenerator.ContainerFromItem(CollectionView.NewItemPlaceholder);
        if (newItemPlaceholderTab != null)
            newItemPlaceholderTab.CoerceValue(VisibilityProperty);
    }
}

CustomStyleSelector.cs

internal class CustomStyleSelector : StyleSelector
{
    public Style NewItemStyle { get; set; }

    public override Style SelectStyle(object item, DependencyObject container)
    {
        if (item == CollectionView.NewItemPlaceholder)
            return NewItemStyle;
        else
            return Application.Current.FindResource(typeof(TabItem)) as Style;
    }
}

TemplateSelector.cs

internal class TemplateSelector : DataTemplateSelector
{
    public DataTemplate ItemTemplate { get; set; }
    public DataTemplate NewItemTemplate { get; set; }

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        if (item == CollectionView.NewItemPlaceholder)
            return NewItemTemplate;
        else
            return ItemTemplate;
    }
}

Generic.xaml

<!-- This style explains how to style a NewItemPlaceholder. -->
<Style x:Key="NewTabItemStyle" TargetType="{x:Type TabItem}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TabItem}">
                <ContentPresenter ContentSource="Header" HorizontalAlignment="Left" />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!-- This template explains how to render a tab item with a close button. -->
<DataTemplate x:Key="ClosableTabItemHeader">
    <DockPanel MinWidth="120">
        <Button DockPanel.Dock="Right" Command="ApplicationCommands.Delete" CommandParameter="{Binding}" Content="X" Cursor="Hand" Focusable="False" FontSize="10" FontWeight="Bold" Height="16" Width="16" />
        <TextBlock Padding="0,0,10,0" Text="{Binding DisplayName}" VerticalAlignment="Center" />
    </DockPanel>
</DataTemplate>

<!-- This template explains how to render a tab item with a new button. -->
<DataTemplate x:Key="NewTabItemHeader">
    <Button Command="{Binding NewTabCommand, RelativeSource={RelativeSource AncestorType={x:Type local:ExtendedTabControl}}}" Content="+" Cursor="Hand" Focusable="False" FontWeight="Bold"
            Width="{Binding ActualHeight, RelativeSource={RelativeSource Self}}"/>
</DataTemplate>

<local:CustomStyleSelector x:Key="StyleSelector" NewItemStyle="{StaticResource NewTabItemStyle}" />
<local:TemplateSelector x:Key="HeaderTemplateSelector" ItemTemplate="{StaticResource ClosableTabItemHeader}" NewItemTemplate="{StaticResource NewTabItemHeader}" />
<Style x:Key="{x:Type local:ExtendedTabControl}" BasedOn="{StaticResource {x:Type TabControl}}" TargetType="{x:Type local:ExtendedTabControl}">
    <Setter Property="ItemContainerStyleSelector" Value="{StaticResource StyleSelector}" />
    <Setter Property="ItemTemplateSelector" Value="{StaticResource HeaderTemplateSelector}" />
</Style>

Solution 5 - Wpf

Define the ControlTemplate of the TabControl like this:

 <!-- Sets the look of the Tabcontrol. -->
<Style x:Key="TabControlStyle" TargetType="{x:Type TabControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TabControl}">
                <Grid>
                    <!-- Upperrow holds the tabs themselves and lower the content of the tab -->
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                    </Grid.RowDefinitions>

The upper row in the grid would be the TabPanel, but you would put that into a StackPanel with a button following the TabPanel, and style the button to look like a tab.

Now the button would create a new TabItem (your custom-created one perhaps) and add it to the ObservableCollection of Tabs you have as the Itemssource for your TabControl.

2 & 3) It should always appear at the end, and it's not a tab so hopefully not part of tab cycling

  1. Well, your TabControl should use a ObservableCollection of TabItems as Itemssource to be notified when a new one is added/removed

Some code:

The NewTabButton usercontrol .cs file

public partial class NewTabButton : TabItem
{
    public NewTabButton()
    {
        InitializeComponent();

        Header = "+";
    }
}

And the main window:

public partial class Window1 : Window
{
    public ObservableCollection<TabItem> Tabs { get; set; }

    public Window1()
    {
        InitializeComponent();

        Tabs = new ObservableCollection<TabItem>();

        for (int i = 0; i < 20; i++)
        {
            TabItem tab = new TabItem();
            tab.Header = "TabNumber" + i.ToString();
            Tabs.Add(tab);
        }

        Tabs.Add(new NewTabButton());

        theTabs.ItemsSource = Tabs;
    }
}

Now we would need to find a way to let it always appear bottom right and also add the event and style for it (the plus sign is there as a placeholder).

Solution 6 - Wpf

This would likely be better as a comment on @NVM's own solution; but I don't have the rep to comment yet so...

If you are trying to use the accepted solution and not getting the add command to trigger then you probably don't have a usercontrol named "parentUserControl".

You can alter @NVM's TabControl declaration as follows to make it work:

<TabControl x:Name="parentUserControl"
            ItemsSource="{Binding Items}"
            ItemTemplateSelector="{StaticResource headerTemplateSelector}"
            ContentTemplateSelector="{StaticResource contentTemplateSelector}"/>

Obviously not a good name to give a tab control :); but I guess @NVM had the data context hooked further up his visual tree to an element to match the name.

Note that personally I preferred to use a relative binding by changing the following:

<Button Content="+" 
        Command="{Binding ElementName=parentUserControl, 
                          Path=DataContext.NewCommand}"/>

To this:

<Button Content="+" 
        Command="{Binding DataContext.NewCommand, 
                          RelativeSource={RelativeSource AncestorType={x:Type TabControl}}}"/>

Solution 7 - Wpf

In addition to NVM's answer. I don't use so many templates and selector's for NewItemPlaceholder. Easier solution with no empty content:

    <TabControl.ItemContainerStyle>
        <Style TargetType="TabItem">
           <Style.Triggers>
              <DataTrigger Binding="{Binding}" Value="{x:Static CollectionView.NewItemPlaceholder}">
                 <Setter Property="Template">
                    <Setter.Value>
                       <ControlTemplate>
                          <Button Command="{Binding DataContext.AddPageCommand, RelativeSource={RelativeSource AncestorType={x:Type TabControl}}}"
                                  HorizontalContentAlignment="Center" VerticalContentAlignment="Center" ToolTip="Add page" >
                             +
                          </Button>
                       </ControlTemplate>
                    </Setter.Value>
                 </Setter>
              </DataTrigger>
           </Style.Triggers>
        </Style>
     </TabControl.ItemContainerStyle>

Ctrl+Tab I desided to disable. It's not SO easy, you should subscribe on KeyDown on parent element, i.e. Window (Ctrl+Shift+Tab also handled correctly):

  public View()
  {
     InitializeComponent();
     AddHandler(Keyboard.PreviewKeyDownEvent, (KeyEventHandler)controlKeyDownEvent);
  }

  private void controlKeyDownEvent(object sender, KeyEventArgs e)
  {
     e.Handled = e.Key == Key.Tab && Keyboard.Modifiers.HasFlag(ModifierKeys.Control);
  }

Solution 8 - Wpf

To complete the answer given by @NVM what you have to add is the PreviewMouseDown event:

<TabControl PreviewMouseDown="ActionTabs_PreviewMouseDown"
</TabControl>

And then:

private void ActionTabs_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
  ouseButtonEventArgs args = e as MouseButtonEventArgs;

  FrameworkElement source = (FrameworkElement)args.OriginalSource;

  if (source.DataContext.ToString() == "{NewItemPlaceholder}")
  {
      e.Handled = true;
  }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNVMView Question on Stackoverflow
Solution 1 - WpfNVMView Answer on Stackoverflow
Solution 2 - WpfDamn VegetablesView Answer on Stackoverflow
Solution 3 - WpfMr.BView Answer on Stackoverflow
Solution 4 - WpfDerrick MoellerView Answer on Stackoverflow
Solution 5 - WpfIngó ValsView Answer on Stackoverflow
Solution 6 - WpfWizbitView Answer on Stackoverflow
Solution 7 - WpfLevView Answer on Stackoverflow
Solution 8 - WpfDarkKodKodView Answer on Stackoverflow