How to fix this behavior in a WPF TabControl?

A simplified example.

Collection item:

using Simplified;

namespace AddTabItem
{
    public class TabVm : BaseInpc
    {
        string _header;
        bool _isPlaceholder;
        private string _text;

        public string Header { get => _header; set => Set(ref _header, value); }

        public bool IsPlaceholder { get => _isPlaceholder; set => Set(ref _isPlaceholder, value); }

        public string Text { get => _text; set => Set(ref _text, value); }
    }
}

ViewModel:

using Simplified;
using System.Collections.ObjectModel;

namespace AddTabItem
{
    public class TabsCollectionViewModel : BaseInpc
    {
        private TabVm _selectedTab;
        private RelayCommand _addNewTabCommand;
        private RelayCommand _removeTabCommand;

        public ObservableCollection<TabVm> Tabs { get; } = new ObservableCollection<TabVm>();

        public TabVm SelectedTab { get => _selectedTab; set => Set(ref _selectedTab, value); }

        public RelayCommand AddNewTabCommand => _addNewTabCommand
            ?? (_addNewTabCommand = new RelayCommand(
                () =>
                {
                    TabVm tab = new TabVm() { Header = $"Tab{Tabs.Count}" };
                    Tabs.Add(tab);
                    SelectedTab = tab;
                }));

        public RelayCommand RemoveTabCommand => _removeTabCommand
            ?? (_removeTabCommand = new RelayCommand<TabVm>(
                tab =>
                {
                    int index = Tabs.IndexOf(tab);
                    if (index >= 0)
                    {
                        Tabs.RemoveAt(index);
                        if (index >= Tabs.Count)
                            index = Tabs.Count - 1;
                        if (index < 0)
                            SelectedTab = null;
                        else
                            SelectedTab = Tabs[index];
                    }
                },
                tab => Tabs.Contains(tab)));
    }
}

Window XAML:

<Window x:Class="AddTabItem.AddTabExamleWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:AddTabItem"
        mc:Ignorable="d"
        Title="AddTabExamleWindow" Height="450" Width="800"
        DataContext="{DynamicResource viewModel}">
    <FrameworkElement.Resources>
        <local:TabsCollectionViewModel x:Key="viewModel"/>
        <local:TabVm x:Key="newTab"/>
        <CollectionViewSource x:Key="tabsCollectionView"
                              Source="{Binding Tabs}"/>
        <CompositeCollection x:Key="tabs">
            <CollectionContainer Collection="{Binding Mode=OneWay, Source={StaticResource tabsCollectionView}}"/>
            <StaticResource ResourceKey="newTab"/>
        </CompositeCollection>
        <DataTemplate x:Key="TabItem.HeaderTemplate"
                      DataType="{x:Type local:TabVm}">
            <Grid>
                <StackPanel Orientation="Horizontal">
                    <Panel.Style>
                        <Style TargetType="StackPanel">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding}" Value="{StaticResource newTab}">
                                    <Setter Property="Visibility" Value="Collapsed"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Panel.Style>
                    <TextBlock Text="{Binding Header}"
                               Margin="2"/>
                    <Button Content="❌" FontWeight="Bold" Foreground="Red"
                            Command="{Binding RemoveTabCommand, Mode=OneWay, Source={StaticResource viewModel}}"
                            CommandParameter="{Binding Mode=OneWay}"/>
                </StackPanel>
                <Button Content="✚" FontWeight="Bold" Foreground="Green"
                        Command="{Binding AddNewTabCommand, Mode=OneWay, Source={StaticResource viewModel}}">
                    <Button.Style>
                        <Style TargetType="Button">
                            <Setter Property="Visibility" Value="Collapsed"/>
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding}" Value="{StaticResource newTab}">
                                    <Setter Property="Visibility" Value="Visible"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Button.Style>
                </Button>
            </Grid>
        </DataTemplate>
        <DataTemplate x:Key="TabItem.ContentTemplate"
                      DataType="{x:Type local:TabVm}">
            <TextBox Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
                <TextBox.Style>
                    <Style TargetType="TextBox">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding}" Value="{StaticResource newTab}">
                                <Setter Property="Visibility" Value="Collapsed"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </TextBox.Style>
            </TextBox>
        </DataTemplate>
    </FrameworkElement.Resources>
    <Grid>
        <TabControl ItemsSource="{DynamicResource tabs}"
                    ItemTemplate="{DynamicResource TabItem.HeaderTemplate}"
                    ContentTemplate="{DynamicResource TabItem.ContentTemplate}"
                    SelectedItem="{Binding SelectedTab, Mode=TwoWay}"/>
    </Grid>
</Window>

To eliminate ambiguities, I give the codes of the classes used in the example:
BaseInpc:

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Simplified
{
    /// <summary>Base class with implementation of the <see cref="INotifyPropertyChanged"/> interface.</summary>
    public abstract class BaseInpc : INotifyPropertyChanged
    {
        /// <inheritdoc cref="INotifyPropertyChanged"/>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>The protected method for raising the event <see cref = "PropertyChanged"/>.</summary>
        /// <param name="propertyName">The name of the changed property.
        /// If the value is not specified, the name of the method in which the call was made is used.</param>
        protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        /// <summary> Protected method for assigning a value to a field and raising 
        /// an event <see cref = "PropertyChanged" />. </summary>
        /// <typeparam name = "T"> The type of the field and assigned value. </typeparam>
        /// <param name = "propertyFiled"> Field reference. </param>
        /// <param name = "newValue"> The value to assign. </param>
        /// <param name = "propertyName"> The name of the changed property.
        /// If no value is specified, then the name of the method 
        /// in which the call was made is used. </param>
        /// <remarks> The method is intended for use in the property setter. <br/>
        /// To check for changes,
        /// used the <see cref = "object.Equals (object, object)" /> method.
        /// If the assigned value is not equivalent to the field value,
        /// then it is assigned to the field. <br/>
        /// After the assignment, an event is created <see cref = "PropertyChanged" />
        /// by calling the method <see cref = "RaisePropertyChanged (string)" />
        /// passing the parameter <paramref name = "propertyName" />. <br/>
        /// After the event is created,
        /// the <see cref = "OnPropertyChanged (string, object, object)" />
        /// method is called. </remarks>
        protected void Set<T>(ref T propertyFiled, T newValue, [CallerMemberName] string propertyName = null)
        {
            if (!object.Equals(propertyFiled, newValue))
            {
                T oldValue = propertyFiled;
                propertyFiled = newValue;
                RaisePropertyChanged(propertyName);

                OnPropertyChanged(propertyName, oldValue, newValue);
            }
        }

        /// <summary> The protected virtual method is called after the property has been assigned a value and after the event is raised <see cref = "PropertyChanged" />. </summary>
        /// <param name = "propertyName"> The name of the changed property. </param>
        /// <param name = "oldValue"> The old value of the property. </param>
        /// <param name = "newValue"> The new value of the property. </param>
        /// <remarks> Can be overridden in derived classes to respond to property value changes. <br/>
        /// It is recommended to call the base method as the first operator in the overridden method. <br/>
        /// If the overridden method does not call the base class, then an unwanted change in the base class logic is possible. </remarks>
        protected virtual void OnPropertyChanged(string propertyName, object oldValue, object newValue) { }
    }
}

RelayCommand:

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;

namespace Simplified
{
    #region Delegates for WPF Command Methods
    public delegate void ExecuteHandler(object parameter);
    public delegate bool CanExecuteHandler(object parameter);
    #endregion

    #region Класс команд - RelayCommand
    /// <summary> A class that implements <see cref = "ICommand" />. <br/>
    /// Implementation taken from <see href = "https://www.cyberforum.ru/wpf-silverlight/thread2390714-page4.html#post13535649" />
    /// and added a constructor for methods without a parameter.</summary>
    public class RelayCommand : ICommand
    {
        private readonly CanExecuteHandler canExecute;
        private readonly ExecuteHandler execute;
        private readonly EventHandler requerySuggested;

        /// <inheritdoc cref="ICommand.CanExecuteChanged"/>
        public event EventHandler CanExecuteChanged;

        /// <summary> Command constructor. </summary>
        /// <param name = "execute"> Command method to execute. </param>
        /// <param name = "canExecute"> Method that returns the state of the command. </param>
        public RelayCommand(ExecuteHandler execute, CanExecuteHandler canExecute = null)
           : this()
        {
            this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
            this.canExecute = canExecute;

            requerySuggested = (o, e) => Invalidate();
            CommandManager.RequerySuggested += requerySuggested;
        }

        /// <inheritdoc cref="RelayCommand(ExecuteHandler, CanExecuteHandler)"/>
        public RelayCommand(Action execute, Func<bool> canExecute = null)
                : this
                (
                      p => execute(),
                      p => canExecute?.Invoke() ?? true
                )
        { }

        private RelayCommand()
            => dispatcher = Application.Current.Dispatcher;

        private readonly Dispatcher dispatcher;

        /// <summary> The method that raises the event <see cref = "CanExecuteChanged" />. </summary>
        public void RaiseCanExecuteChanged()
        {
            if (dispatcher.CheckAccess())
                Invalidate();
            else
                dispatcher.BeginInvoke((Action)Invalidate);
        }
        private void Invalidate()
            => CanExecuteChanged?.Invoke(this, EventArgs.Empty);

        /// <inheritdoc cref="ICommand.CanExecute(object)"/>
        public bool CanExecute(object parameter) => canExecute?.Invoke(parameter) ?? true;

        /// <inheritdoc cref="ICommand.Execute(object)"/>
        public void Execute(object parameter) => execute?.Invoke(parameter);
    }
    #endregion
}

RelayCommand<T>:

using System;
using System.Windows.Input;
namespace Simplified
{
    #region Delegates for WPF Command Methods
    public delegate void ExecuteHandler<T>(T parameter);
    public delegate bool CanExecuteHandler<T>(T parameter);
    #endregion

    /// <summary> RelayCommand implementation for generic parameter methods. </summary>
    /// <typeparam name = "T"> Method parameter type. </typeparam>  
    public class RelayCommand<T> : RelayCommand
    {
        /// <inheritdoc cref="RelayCommand(ExecuteHandler, CanExecuteHandler)"/>
        public RelayCommand(ExecuteHandler<T> execute, CanExecuteHandler<T> canExecute = null)
            : base
            (
                  p =>
                  {
                      if (p is T t)
                          execute(t);
                  },
                  p => (p is T t) && (canExecute?.Invoke(t) ?? true)
            )
        { }
    }
}

Leave a Comment