54

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
NVM
  • 5,442
  • 4
  • 41
  • 61

8 Answers8

37

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...).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
NVM
  • 5,442
  • 4
  • 41
  • 61
  • 1
    You should accept this as your right answer so people will find it when looking for answer to the same question. Great Job btw. – Ingó Vals Aug 16 '10 at 10:00
  • 1
    I was hoping someone would suggest a complete solution. But I guess I've waited long enough.... – NVM Aug 16 '10 at 10:24
  • This is an awesome solution. Worker perfectly for me. – jmogera Aug 30 '11 at 21:49
  • Did you ever come back around to this? I was curious if you came up with a complete solution? – Derrick Moeller Dec 26 '14 at 20:52
  • Sorry I stopped working with WPF after this project. Something did come to mind then but I no longer remember it. I guess someone might be able to answer that specific question if you asked a new one. – NVM Dec 27 '14 at 05:06
7

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>
Damn Vegetables
  • 11,484
  • 13
  • 80
  • 135
  • In the `XAML` is there a way to set a grid in `TabContentTemplate`? I did that but any control of the grid like a `TextBox` if I change its Text so it reflects to the controls of all tabs. How to add a grid there without reflecting its values to the other tabs? – Simple May 19 '21 at 04:26
5

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Mr.B
  • 3,484
  • 2
  • 26
  • 40
2

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>
Derrick Moeller
  • 4,808
  • 2
  • 22
  • 48
  • Is there any project demo of this anywhere? – sorrell Feb 26 '15 at 19:36
  • 1
    Uh... how to use it? Especially the Generic.XAML part. I put all of that into but nothing happened. You have gone through writing all these code, but very little explanation on how to use it. That is like giving someone a canned food without a can opener. – Damn Vegetables Jul 30 '20 at 04:52
1

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

4) 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).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ingó Vals
  • 4,788
  • 14
  • 65
  • 113
  • Thanks Ingo, I just added an image to make the question more clear. What you suggest wont work when there are multiple rows of tab items I think. – NVM Aug 12 '10 at 15:31
  • You could try to create a UserControls that inherits TabItem and create a event on that one that when it is selected it creates a new TabItem and puts it into the collection infront itself. That should work perfectly. If I have time I might add code later on. – Ingó Vals Aug 12 '10 at 17:09
  • If you are binding through ItemsSource you cannot add an additional TabItem in XAML. If you are talking about using a CompositeCollection I think it will only work with static TabItems and you cannot bind the items – NVM Aug 12 '10 at 17:18
  • You can add to a ObservableCollection that is the itemssource, have done that and works fine. The problem I found with this solution is that the last Tab ends in the line above when there are multiple lines, I'll add code and we can work from there. – Ingó Vals Aug 12 '10 at 17:21
  • Aah, sorry I took MVVM as a given. With what you suggest I need to take UI (TabItem) into my VM which is not acceptable. – NVM Aug 12 '10 at 17:24
  • Well when you add the new tab you are going to need to fetch new Data from your VM right? How do control what data goes into tabs and what not? – Ingó Vals Aug 12 '10 at 17:30
  • More precisely, the + button should fire a "AddNew" event, which will be wired to the VM as a "AddNewCommand" which will add an item to the bound observablecollection of items and the datatemplate will take care of the rest. Thats pretty standard MVVM stuff I think. – NVM Aug 12 '10 at 17:33
  • Hmm then the only solution (short of waiting until WPF adds this feature ) I see is to somehow edit the TabPanel to feature a button among the tabs. Problem is TabPanel isn't a control I think. – Ingó Vals Aug 12 '10 at 18:06
  • Actually IEditableCollectionView looks like a good bet. It has a NewItemPlaceHolder whose purpose is exactly to do a Add Neww thing. eg DataGrid Add new row works using this. Unfortunately I am relatively new to WPF to judge whether this is the way to go or not and so posted the question hoping people more knowledgeable will point me in the right direction lest I dig myself into a hole. – NVM Aug 12 '10 at 18:21
  • And also I am happy to build a derived tabcontrol/panel etc if that is indeed the right approach. – NVM Aug 12 '10 at 18:24
  • IEditableCollectionView is just a wrapper you would wrap ObservableCollection in for example. It's nice as you can create a new item and cancel it without ever having to connect to the database. But I can't see it help you here. I guess a derived TabPanel would be the right approach I just don't know how hard it will be. I tried just creating a NewTabPanel.cs class that inherits TabPanel and used it in my TabControl ControlTemplate and that works fine. Now we just have to find out how to edit it correctly. – Ingó Vals Aug 12 '10 at 18:35
  • Ingo, I really appreciate your help with this. http://msdn.microsoft.com/en-us/library/system.windows.data.collectionview.newitemplaceholder.aspx If the collectionview add a NewItemPlaceholder item automatically, I can simply add a datatemplate containing a + button for that item. Rest of the items will use the tabcontrol's default template. It sounds almost perfect!! Any thoughts? – NVM Aug 12 '10 at 18:41
  • BTW the NewItemPlaceHolder takes care of everything, its can not be the current item(so I am assuming it wont cause trouble with the tab cycle), it can be at the last item, it doesn't get involved in filtering/sorting etc.... – NVM Aug 12 '10 at 18:47
  • Yeah sorry somehow missed your earlier mention of this NewItemPlaceholder, It sounds like the thing you need, good find. Let me know how it works out. As I already am implementing IEditable in my App I might add it as well :) – Ingó Vals Aug 12 '10 at 18:59
1

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}}}"/>
Wizbit
  • 421
  • 1
  • 4
  • 13
0

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);
  }
Lev
  • 811
  • 12
  • 13
0

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;
  }
}