TabControl with Add New Tab Button (+)

Viewed 40031

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.

8 Answers

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>

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