1

I have a WPF .NET Core 5.0 project which has a TabControl that I can add a new TabItem by clicking on a Button represented as [+].

MainWindow.cs:

using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        int TabIndex = 1;
        ObservableCollection<TabVM> Tabs = 
            new ObservableCollection<TabVM>();
        public MainWindow()
        {
            InitializeComponent();
            var tab1 = new TabVM()
            {
                Header = $"Tab {TabIndex}"
            };
            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;
        }

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

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

        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 x: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:Key="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">
                        <Grid>
                            <Label Content="Enter your text here:" 
                                    HorizontalAlignment="Left" 
                                    Margin="30,101,0,0" 
                                    VerticalAlignment="Top" 
                                    Width="298" 
                                    FontSize="18"/>
                            <RichTextBox HorizontalAlignment="Left"
                                            Height="191" 
                                            Margin="8,135,0,0" 
                                            VerticalAlignment="Top" 
                                            Width="330">
                                <FlowDocument/>
                            </RichTextBox>
                        </Grid>
                    </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>

In the XAML I set a Grid in the ContentControl TabContentTemplate, but any control of the Grid like that RichTextBox if I change its text so it reflects to the RichTextBox of all tabs. How to add a Grid there without reflecting its control values to the other tabs?

The gif below shows the problem, whatever I type in the RichTextBox reflects in the other tabs.

enter image description here

Simple
  • 827
  • 1
  • 9
  • 21
  • You are doing some kind of nonsense. The TabItem.Content property takes an item from the TabControl.ItemsSource collection. And in the ContentTemplate how to render this element must be specified. Instead of bindings to TabVM, you display either null or TabContentTemplate. – EldHasp May 19 '21 at 15:21
  • The very implementation of TabVM also raises many questions. Firstly, it is a nested private class and creating bindings to it will be a problem. In general, nested classes are rarely used in Sharpe (not only in WPF). For very specific tasks only. Secondly, where are the actual properties to which bindings should be created in it? There is only Header and IsPlaceholder flag. But you also need properties for the content. – EldHasp May 19 '21 at 15:21
  • @EldHasp You're right, by the way my code is a modified version of the answer: https://stackoverflow.com/a/63167642/8684836 where he uses an actual `Content` property which is a class, but I wanted a way to bind the `Content` directly from the `XAML` `Grid` so it would be easy to work with the layout of my `Grid` from the `Designer`. – Simple May 19 '21 at 17:59
  • I will not say that this is a bad example, but it is clearly not suitable for your task. For a more detailed answer, you need additional clarification of your question. What exactly do you want to see on the tabs? It is best if, in addition to explanations, you also show an image of the tab layout. What data on tabs is common and what data is specific for each tab and collection item? – EldHasp May 19 '21 at 18:10
  • @EldHasp I just want to make a copy of all controls and layout of the main tab to the next tabs, but they must be independent, I mean if I enter an input in a control of tab X that should NOT reflect/change in the control of tab Y which is actually what is happening in the issue. I will update my question with a gif showing the problem. – Simple May 19 '21 at 19:21
  • @EldHasp Question updated with the gif. – Simple May 19 '21 at 19:42
  • @Simple: Either data bind all controls in the `ContentTemplate` to a source property or turn off the default caching behaviour as suggested [here](https://stackoverflow.com/questions/9794151/stop-tabcontrol-from-recreating-its-children). – mm8 May 19 '21 at 20:04
  • @mm8 The first option seems better for me, could you show me a simple example of data binding a `Control` such as that `RichTextBox` so I could do the rest for any control. – Simple May 19 '21 at 23:04
  • 1
    @Simple: Check [this answer](https://stackoverflow.com/a/2641774/7252182) for an example of how you could bind to the `RichTextBox`. – mm8 May 20 '21 at 12:57

1 Answers1

3

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>
        /// <returns>Returns <see langword="true"/> if the value being assigned
        /// was not equal to the value of the field and
        /// therefore the value of the field was changed.</returns>
        /// <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 bool Set<T>(ref T propertyFiled, T newValue, [CallerMemberName] string propertyName = null)
        {
            bool notEquals = !object.Equals(propertyFiled, newValue);
            if (notEquals)
            {
                T oldValue = propertyFiled;
                propertyFiled = newValue;
                RaisePropertyChanged(propertyName);

                OnPropertyChanged(propertyName, oldValue, newValue);
            }
            return notEquals;
        }

        /// <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)
            )
        { }
    }
}
EldHasp
  • 6,079
  • 2
  • 9
  • 24