TabControl with button Add new tab (+)

What is the correct way to add tabs to the + button at the end of all tabs in the tab bar of a tab control in WPF?

  • It should work correctly with multiple tab header lines.
  • It should be at the end of all tabs
  • The inclusion of tabs should work correctly ( Alt + Tab ), that is, the + tab should be skipped.
  • I do not need to modify the original collection to which I am attached. That is, control must be reused.
  • The solution should work with MVVM

Enter image description here

enter image description here

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

I'm just looking for a general approach to this.

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

+42
button wpf tabs tabcontrol
Aug 12 2018-10-12
source share
6 answers

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 

This is almost complete, because the tab loop does not skip the + tab and will show empty content (which is not very good, but I can live with it until a better solution appears ...).

+31
Aug 13 '10 at 13:35
source share

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> 

The code in the corresponding presentation model is as follows:

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

Note: I packaged a TabPanel using a StackPanel to flip the + button along with TabPanel relative to the value of the TabStripPlacement property. Without inheritance and without code-behind in your view.

+4
Mar 15 '16 at 2:09
source share

I believe that I came up with a complete solution, I started with an NVM solution to create my template. And then referenced the DataGrid source code to come up with an advanced TabControl that can add and remove elements.

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> 
+2
Jan 6 '15 at 18:55
source share

Define a TabControl checklist as follows:

  <!-- 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 top line in the grid will be a TabPanel, but you put it in the StackPanel using the button next to the TabPanel and write the button down so that it looks like a tab.

Now the button will create a new TabItem (possibly your custom one) and add it to the ObservableCollection from the tabs that you have as the Itemssource for your TabControl.

2 and 3) It should always be displayed at the end, and this is not a tab, so I hope it will not be part of the tab loop

4) Well, your TabControl should use the ObservableCollection TabItems as an Itemssource to notify when a new one is added / removed

Some codes:

Usercontrol.cs file for NewTabButton

 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 need to find a way so that it always appears at the bottom right, and also adds an event and style for it (there is a plus sign as a placeholder).

+1
Aug 12
source share

This will probably be better than commenting on @NVM's own solution; but I don’t have any comments yet to comment ...

If you are trying to use the decision and don’t get the add command to run, you probably don’t have user control named "parentUserControl".

You can modify the @NVM TabControl expression as follows to make it work:

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

Obviously, this is not a good name for tab management :); but I believe that @NVM had a data context connected to its visual tree, to the element corresponding to the name.

Note that I personally chose to use relative snapping by changing the following:

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

For this:

 <Button Content="+" Command="{Binding DataContext.NewCommand, RelativeSource={RelativeSource AncestorType={x:Type TabControl}}}"/> 
+1
Nov 15 '16 at 17:24
source share

In addition to the answer of NVM. I do not use so many templates and a selector for NewItemPlaceholder. A simple solution without 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 decided to disable. It is not so simple, you have to subscribe to KeyDown on the parent element, i.e. Window (Ctrl + Shift + Tab is also processed 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); } 
0
Nov 18 '16 at 12:32
source share



All Articles