[C#/WPF]動的にタブを生成するタブコントロール

# はじめに
C#、WPFにおけるMVVMモデルで動的にタブを生成するタブコントロールを作成します。
本記事最後にコードビハインドによるコードも載せます。

# 開発環境
言語：C#
フレームワーク：.NET Framework4.7.2
UIフレームワーク：Windows Presentation Foundation
開発環境:Visual Studio 2019

# 概要
各タブにタブ削除ボタン、最後尾のタブがタブ追加ボタンになっているタブコントロールを作成します。
実行画面は以下の画像です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2725080/0470ce62-8e3b-669c-53a5-2caa105766b1.png)

***

# UIの作成
動的なタブを生成するタブコントロールを作成するには、ヘッダーとコンテンツのテンプレートを記述し、タブコントロールの``ItemsSource``にデータのコレクションをバインドする必要があります。**この場合、``Items``に個別のタブを追加することはできず、テンプレートのタブを増減することしかできません。**
そのため、今回は各タブに「Mode」という値をバインドしておき、その値によって通常のタブかタブ追加ボタンかを分けるように記述します。

## タブコントロール
タブコントロールの記述方法は``ItemTemplate``にヘッダーのテンプレート、``ContentTemplate``にコンテンツのテンプレートを記述します。
ヘッダーは横並びに3つのコントロールを配置するため、``StackPanel``を配置し``Orientation``をHorizontalにしておきます。
``ItemContainerStyle``は各タブのスタイルを変更できますが、今回は高さを変更しているだけです。

```xml:MainWindow.xaml
<!-- タブコントロール -->
<TabControl x:Name="tabControl"
            ItemsSource="{Binding TabItems}">

    <!-- タブの高さを30にする -->
    <TabControl.ItemContainerStyle>
        <Style TargetType="{x:Type TabItem}">
            <Setter Property="Height" Value="30" />
        </Style>
    </TabControl.ItemContainerStyle>
    
    <!-- ヘッダーテンプレート -->
    <TabControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
            <!-- ヘッダーの内容 -->
            </StackPanel>
        <DataTemplate>
    </TabControl.ItemTemplate>

    <!-- コンテンツテンプレート -->
    <TabControl.ContentTemplate>
        <DataTemplate>
            <!-- コンテンツの内容 -->
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>
```

## ヘッダーのテンプレート
``StackPanel``内に配置するヘッダーテキストブロック、タブ削除ボタン、タブ追加ボタンについてです。
タブにバインドされている「Mode」が「Add」だった場合ヘッダテキストブロックとタブ削除ボタンを非表示にし、タブ追加ボタンだけを表示させるようにトリガーを記述します。

### ヘッダーテキスト
ヘッダーに表示されるテキスト部分です。
``TextBlock``を配置し、``Text``にヘッダー文字をバインドしておきます。
``DataTrigger``によって自分自身にバインドされているデータ(``DataContext``)の「Mode」が「Add」だった場合、``Visibility``を**Collapsed**にして非表示にします。

```xml:MainWindow.xaml
<!-- ヘッダーテキストブロック -->
<TextBlock x:Name="txtHeader"
           Text="{Binding Header}"
           Padding="0 0 5 0"
           FontSize="14">
    <TextBlock.Style>
        <Style TargetType="{x:Type TextBlock}">
            <Style.Triggers>
                <!-- ModeがAddの時 -->
                <DataTrigger Binding="{Binding ElementName=txtHeader, Path=DataContext.Mode}" Value="Add">
                    <Setter Property="Visibility" Value="Collapsed" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </TextBlock.Style>
</TextBlock>
```

### タブ削除ボタン
タブを削除する×ボタンです。
``Button``を配置し、タブ削除用のコマンドとコマンドの引数としてタブにバインドされているデータを渡します。
``Template``内に``ControlTemplate``を置くことでボタンの見た目を別のコントロールにすることができます。×ボタンを表示したいため、``Path``によって×を表示します。
``Path``内のスタイルでトリガーを記述します。
``DataTrigger``でヘッダーテキストブロック(txtHeader)にバインドされている「Mode」が「Add」だった場合、``Visibility``を**Collapsed**にして非表示にします。
また、``Trigger``を使いカーソルが重なったときに色を変えるようにしています。

```xml:MainWindow.xaml
<!-- タブ削除ボタン -->
<Button x:Name="btnDeleteTab"
        Command="{Binding DataContext.DeleteTabCommand, RelativeSource={RelativeSource AncestorType=TabControl}}"
        CommandParameter="{Binding ElementName=txtHeader, Path=DataContext}">
    <Button.Style>
        <Style TargetType="{x:Type Button}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Button}">
                        <Path Height="13"
                              Stretch="Uniform"
                              Data="M20 6.91L17.09 4L12 9.09L6.91 4L4 6.91L9.09 12L4 17.09L6.91 20L12 14.91L17.09 20L20 17.09L14.91 12L20 6.91Z">
                            <Path.Style>
                                <Style TargetType="{x:Type Path}">
                                    <Setter Property="Fill" Value="#BBB" />
                                    <Setter Property="Stroke" Value="#BBB" />
                                    <Style.Triggers>
                                        <!-- ModeがAddの時 -->
                                        <DataTrigger Binding="{Binding ElementName=txtHeader, Path=DataContext.Mode}" Value="Add">
                                            <Setter Property="Visibility" Value="Collapsed" />
                                        </DataTrigger>
                                        <!-- カーソルが重なったとき -->
                                        <Trigger Property="IsMouseOver" Value="True">
                                            <Setter Property="Fill" Value="Black" />
                                            <Setter Property="Stroke" Value="Black" />
                                        </Trigger>
                                    </Style.Triggers>
                                </Style>
                            </Path.Style>
                        </Path>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Button.Style>
</Button>
```

### タブ追加ボタン
タブを追加する＋ボタンです。
``Button``を配置し、タブ追加用のコマンドとコマンドの引数としてタブコントロールを渡します。
こちらも同様に``Template``内に``ControlTemplate``を配置します。``Path``によって＋を表示します。
セッターによって初期状態では``Visibility``を``Collapsed``にしておきます。
``DataTrigger``でヘッダーテキストブロック(txtHeader)にバインドされている「Mode」が「Add」だった場合、``Visibility``を**Visible**にして表示させます。
また、こちらも同様に``Trigger``を使いカーソルが重なったときに色を変えるようにしています。

```xml:MainWindow.xaml
<!-- タブ追加ボタン -->
<Button x:Name="btnAddTab"
        Command="{Binding DataContext.AddTabCommand, RelativeSource={RelativeSource AncestorType=TabControl}}"
        CommandParameter="{Binding ElementName=tabControl}">
    <Button.Style>
        <Style TargetType="{x:Type Button}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Button}">
                        <Path Height="18"
                              Stretch="Uniform"
                              Data="M20 14H14V20H10V14H4V10H10V4H14V10H20V14Z">
                            <Path.Style>
                                <Style TargetType="{x:Type Path}">
                                    <Setter Property="Visibility" Value="Collapsed"/>
                                    <Setter Property="Fill" Value="#BBB" />
                                    <Setter Property="Stroke" Value="#BBB" />
                                    <Style.Triggers>
                                        <!-- ModeがAddの時 -->
                                        <DataTrigger Binding="{Binding ElementName=txtHeader, Path=DataContext.Mode}" Value="Add">
                                            <Setter Property="Visibility" Value="Visible" />
                                        </DataTrigger>
                                        <!-- カーソルが重なったとき -->
                                        <Trigger Property="IsMouseOver" Value="True">
                                            <Setter Property="Fill" Value="Black" />
                                            <Setter Property="Stroke" Value="Black" />
                                        </Trigger>
                                    </Style.Triggers>
                                </Style>
                            </Path.Style>
                        </Path>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Button.Style>
</Button>
```

## コンテンツのテンプレート
今回はコンテンツはあまり記述しません。テキストブロックにコンテンツの文字をバインドするだけです。

```xml:MainWindow.xaml
<TextBlock x:Name="txtContent"
           Text="{Binding Content}"
           FontSize="25"
           VerticalAlignment="Center"
           HorizontalAlignment="Center"/>
```

# 処理の作成
タブのデータとなるModelとViewModelを作成します。ViewModel内に処理を行うコマンドを記述します。

## タブのデータ(Model)

### 変更通知
バインドするデータを保持するクラスに変更通知メソッドを実装することにより、バインド後に処理によってデータが変更されても、動的に反映されるようになります。
usingディレクティブに必要な宣言をして、クラスに``INotifyPropertyChanged``を継承させます。
その後、変更通知イベントと変更通知発行メソッドを記述します。

```cs:TabItemData.cs
using System.ComponentModel;            //INotifyPropertyChanged
using System.Runtime.CompilerServices;  //CallerMemberName

class TabItemData : INotifyPropertyChanged
{
    //変更通知イベント
    public event PropertyChangedEventHandler PropertyChanged;

    //変更通知発行メソッド
    protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    /* データ省略 */

}
```

### データ
今回はヘッダーに表示するテキスト、コンテンツ内に表示するテキスト、タブのモードを宣言します。
この時、セッター内で値が変更された時、変更通知発行メソッドを呼び出すようにします。これで変更通知を実装できました。

```cs:TabItemData.cs
class TabItemData : INotifyPropertyChanged
{
    /* 変更通知省略 */

    //ヘッダー
    private string _header;
    public string Header
    {
        get { return _header; }
        set
        {
            if (_header != value)
            {
                _header = value;
                NotifyPropertyChanged();
            }
        }
    }
    
    //コンテンツ
    private string _content;
    public string Content
    {
        get { return _content; }
        set
        {
            if (_content != value)
            {
                _content = value;
                NotifyPropertyChanged();
            }
        }
    }
    
    //モード
    private string _mode;
    public string Mode
    {
        get { return _mode; }
        set
        {
            if (_mode != value)
            {
                _mode = value;
                NotifyPropertyChanged();
            }
        }
    }
}
```

## 処理(ViewModel)
UIにバインドするデータを宣言するViewModelと処理をするコマンドについてです。

### 変数宣言
UIに紐づけるデータを宣言します。
タブコントロールに紐づけるコレクション、タブ追加、削除のコマンドを宣言しておきます。
この時、コマンドはViewModelを引数にするため、ゲッター内で新しいインスタンスを宣言します。

```cs:MainWindowViewModel.cs
using System.Collections.ObjectModel;   //ObservableCollection
using System.Windows.Input;             //ICommand

//ViewModel
class MainWindowViewModel
{
    //タブアイテムコレクション
    public ObservableCollection<TabItemData> TabItems { get; set; } = new ObservableCollection<TabItemData>();

    //タブ追加コマンド
    public AddTab AddTabCommand { get { return new AddTab(this); } }

    //タブ削除コマンド
    public DeleteTab DeleteTabCommand { get { return new DeleteTab(this); } }

    /* コンストラクタ省略 */
}
```

### コンストラクタ
コンストラクタ内で初期状態で存在するタブをコレクションに追加します。
「Mode」が「Normal」の初期タブと「Add」のタブ追加ボタンをコレクションに追加します。

```cs:MainWindowViewModel.cs
//コンストラクタ
public MainWindowViewModel()
{
    //初期タブ
    TabItems.Add(new TabItemData()
    {
        Header = "初期タブ",
        Content = "初期タブのコンテンツ",
        Mode = "Normal"
    });

    //タブ追加ボタン
    TabItems.Add(new TabItemData(){ Mode = "Add" });
}
```

### タブ追加コマンド
タブ追加ボタンのコマンドを実行する際にパラメータとしてタブコントロールを渡しています。
タブコントロールの``ItemsSource``プロパティにコレクションがバインドされているとき、テンプレート内ではコレクションの内容しかバインドできないため、ViewModel内のコマンドが候補に挙がってきません。そのため、DataContextから直接バインドしています。

```xml:MainWindow.xaml
<!-- タブ追加ボタン -->
<Button x:Name="btnAddTab"
        Command="{Binding DataContext.AddTabCommand, RelativeSource={RelativeSource AncestorType=TabControl}}"
        CommandParameter="{Binding ElementName=tabControl}">
    <!-- 省略 -->
</Button>
```

ViewModelが引数として渡されるため、フィールドを宣言しておき、コンストラクタ内でフィールドに代入します。
処理内容としては、コレクションの``Insert``でタブ追加ボタンの1つ前に「Mode」が「Normal」のタブを追加します。
その後、引数に渡されたタブコントロールの``SelectedIndex``を追加したタブの添え字に変更します。

```cs:MainWindowViewModel.cs
//タブ追加コマンド
class AddTab : ICommand
{
    private MainWindowViewModel _vm;

    public AddTab(MainWindowViewModel vm)
    {
        _vm = vm;
    }

    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object parameter) { return true; }

    //parameter : TabControl
    public void Execute(object parameter)
    {
        //タブ追加ボタンの前にタブを追加する
        _vm.TabItems.Insert(_vm.TabItems.Count - 1, new TabItemData()
        {
            Header = "追加されたタブ",
            Content = "追加されたタブのコンテンツ",
            Mode = "Normal"
        });

        //追加したタブを選択する
        ((TabControl)parameter).SelectedIndex = _vm.TabItems.Count - 2;
    }
}
```

### タブ削除コマンド
タブ削除ボタンのコマンドを実行する際にパラメータとして現在のタブにバインドされているデータを渡しています。
タブ追加ボタンと同様にコマンドをバインドしています。

```xml:MainWindow.xaml
<!-- タブ削除ボタン -->
<Button x:Name="btnDeleteTab"
        Command="{Binding DataContext.DeleteTabCommand, RelativeSource={RelativeSource AncestorType=TabControl}}"
        CommandParameter="{Binding ElementName=txtHeader, Path=DataContext}">
    <!-- 省略 -->
</Button>
```

こちらも同様にViewModelが引数として渡されるため、フィールドを宣言しておき、コンストラクタ内でフィールドに代入します。
処理内容としては、引数に渡された``TabItemData``を取得後、コレクションから該当のデータを``Remove``で削除します。

```cs:MainWindowViewModel.cs
//タブ削除コマンド
class DeleteTab : ICommand
{
    private MainWindowViewModel _vm;

    public DeleteTab(MainWindowViewModel vm)
    {
        _vm = vm;
    }

    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object parameter) { return true; }

    //parameter : TabItemData
    public void Execute(object parameter)
    {
        //削除ボタンが押されたタブのデータを削除
        _vm.TabItems.Remove((TabItemData)parameter);
    }
}
```


# コード全体(MVVM)
コード全体は以下の通りです。

<details><summary><strong>Viewのコード - MainWindow.xaml</strong></summary>

```xml:MainWindow.xaml
<Window x:Class="WPF_TabTest.MainWindow"
        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:WPF_TabTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    
    <!-- データコンテキスト -->
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    
    <Grid>
        <!-- タブコントロール -->
        <TabControl x:Name="tabControl"
                    ItemsSource="{Binding TabItems}">

            <TabControl.ItemContainerStyle>
                <Style TargetType="{x:Type TabItem}">
                    <Setter Property="Height" Value="30" />
                </Style>
            </TabControl.ItemContainerStyle>
            
            <!-- ヘッダーテンプレート -->
            <TabControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <!-- ヘッダーテキストブロック -->
                        <TextBlock x:Name="txtHeader"
                                   Text="{Binding Header}"
                                   Padding="0 0 5 0"
                                   FontSize="14">
                            <TextBlock.Style>
                                <Style TargetType="{x:Type TextBlock}">
                                    <Style.Triggers>
                                        <!-- ModeがAddの時 -->
                                        <DataTrigger Binding="{Binding ElementName=txtHeader, Path=DataContext.Mode}" Value="Add">
                                            <Setter Property="Visibility" Value="Collapsed" />
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </TextBlock.Style>
                        </TextBlock>
                        
                        <!-- タブ削除ボタン -->
                        <Button x:Name="btnDeleteTab"
                                Command="{Binding DataContext.DeleteTabCommand, RelativeSource={RelativeSource AncestorType=TabControl}}"
                                CommandParameter="{Binding ElementName=txtHeader, Path=DataContext}">
                            <Button.Style>
                                <Style TargetType="{x:Type Button}">
                                    <Setter Property="Template">
                                        <Setter.Value>
                                            <ControlTemplate TargetType="{x:Type Button}">
                                                <Path Height="13"
                                                      Stretch="Uniform"
                                                      Data="M20 6.91L17.09 4L12 9.09L6.91 4L4 6.91L9.09 12L4 17.09L6.91 20L12 14.91L17.09 20L20 17.09L14.91 12L20 6.91Z">
                                                    <Path.Style>
                                                        <Style TargetType="{x:Type Path}">
                                                            <Setter Property="Fill" Value="#BBB" />
                                                            <Setter Property="Stroke" Value="#BBB" />
                                                            <Style.Triggers>
                                                                <!-- ModeがAddの時 -->
                                                                <DataTrigger Binding="{Binding ElementName=txtHeader, Path=DataContext.Mode}" Value="Add">
                                                                    <Setter Property="Visibility" Value="Collapsed" />
                                                                </DataTrigger>
                                                                <!-- カーソルが重なったとき -->
                                                                <Trigger Property="IsMouseOver" Value="True">
                                                                    <Setter Property="Fill" Value="Black" />
                                                                    <Setter Property="Stroke" Value="Black" />
                                                                </Trigger>
                                                            </Style.Triggers>
                                                        </Style>
                                                    </Path.Style>
                                                </Path>
                                            </ControlTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </Style>
                            </Button.Style>
                        </Button>
                        
                        <!-- タブ追加ボタン -->
                        <Button x:Name="btnAddTab"
                                Command="{Binding DataContext.AddTabCommand, RelativeSource={RelativeSource AncestorType=TabControl}}"
                                CommandParameter="{Binding ElementName=tabControl}">
                            <Button.Style>
                                <Style TargetType="{x:Type Button}">
                                    <Setter Property="Template">
                                        <Setter.Value>
                                            <ControlTemplate TargetType="{x:Type Button}">
                                                <Path Height="18"
                                                      Stretch="Uniform"
                                                      Data="M20 14H14V20H10V14H4V10H10V4H14V10H20V14Z">
                                                    <Path.Style>
                                                        <Style TargetType="{x:Type Path}">
                                                            <Setter Property="Visibility" Value="Collapsed"/>
                                                            <Setter Property="Fill" Value="#BBB" />
                                                            <Setter Property="Stroke" Value="#BBB" />
                                                            <Style.Triggers>
                                                                <!-- ModeがAddの時 -->
                                                                <DataTrigger Binding="{Binding ElementName=txtHeader, Path=DataContext.Mode}" Value="Add">
                                                                    <Setter Property="Visibility" Value="Visible" />
                                                                </DataTrigger>
                                                                <!-- カーソルが重なったとき -->
                                                                <Trigger Property="IsMouseOver" Value="True">
                                                                    <Setter Property="Fill" Value="Black" />
                                                                    <Setter Property="Stroke" Value="Black" />
                                                                </Trigger>
                                                            </Style.Triggers>
                                                        </Style>
                                                    </Path.Style>
                                                </Path>
                                            </ControlTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </Style>
                            </Button.Style>
                        </Button>
                    </StackPanel>
                </DataTemplate>
            </TabControl.ItemTemplate>

            <!-- コンテンツテンプレート -->
            <TabControl.ContentTemplate>
                <DataTemplate>
                    <TextBlock x:Name="txtContent"
                               Text="{Binding Content}"
                               FontSize="25"
                               VerticalAlignment="Center"
                               HorizontalAlignment="Center"/>
                </DataTemplate>
            </TabControl.ContentTemplate>
        </TabControl>
    </Grid>
</Window>
```

</details>

<details><summary><strong>ViewModelのコード - MainWindowViewModel.cs</strong></summary>

```cs:MainWindowViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Collections.ObjectModel;   //ObservableCollection
using System.Windows.Input;             //ICommand

namespace WPF_TabTest
{
    //ViewModel
    class MainWindowViewModel
    {
        //タブアイテムコレクション
        public ObservableCollection<TabItemData> TabItems { get; set; } = new ObservableCollection<TabItemData>();

        //タブ追加コマンド
        public AddTab AddTabCommand { get { return new AddTab(this); } }

        //タブ削除コマンド
        public DeleteTab DeleteTabCommand { get { return new DeleteTab(this); } }

        //コンストラクタ
        public MainWindowViewModel()
        {
            //初期タブ
            TabItems.Add(new TabItemData()
            {
                Header = "初期タブ",
                Content = "初期タブのコンテンツ",
                Mode = "Normal"
            });

            //タブ追加ボタン
            TabItems.Add(new TabItemData(){ Mode = "Add" });
        }
    }

    //タブ追加コマンド
    class AddTab : ICommand
    {
        private MainWindowViewModel _vm;

        public AddTab(MainWindowViewModel vm)
        {
            _vm = vm;
        }

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter) { return true; }

        public void Execute(object parameter)
        {
            //タブ追加ボタンの前にタブを追加する
            _vm.TabItems.Insert(_vm.TabItems.Count - 1, new TabItemData()
            {
                Header = "追加されたタブ",
                Content = "追加されたタブのコンテンツ",
                Mode = "Normal"
            });

            //追加したタブを選択する
            ((TabControl)parameter).SelectedIndex = _vm.TabItems.Count - 2;
        }
    }

    //タブ削除コマンド
    class DeleteTab : ICommand
    {
        private MainWindowViewModel _vm;

        public DeleteTab(MainWindowViewModel vm)
        {
            _vm = vm;
        }

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter) { return true; }

        public void Execute(object parameter)
        {
            //削除ボタンが押されたタブのデータを削除
            _vm.TabItems.Remove((TabItemData)parameter);
        }
    }
}
```

</details>

<details><summary><strong>Modelのコード - TabItemData.cs</strong></summary>

```cs:TabItemData.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.ComponentModel;            //INotifyPropertyChanged
using System.Runtime.CompilerServices;  //CallerMemberName

namespace WPF_TabTest
{
    //タブアイテムデータ定義
    class TabItemData : INotifyPropertyChanged
    {
        //変更通知イベント
        public event PropertyChangedEventHandler PropertyChanged;

        //変更通知発行メソッド
        protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        //ヘッダー
        private string _header;
        public string Header
        {
            get { return _header; }
            set
            {
                if (_header != value)
                {
                    _header = value;
                    NotifyPropertyChanged();
                }
            }
        }

        //コンテンツ
        private string _content;
        public string Content
        {
            get { return _content; }
            set
            {
                if (_content != value)
                {
                    _content = value;
                    NotifyPropertyChanged();
                }
            }
        }

        //モード
        private string _mode;
        public string Mode
        {
            get { return _mode; }
            set
            {
                if (_mode != value)
                {
                    _mode = value;
                    NotifyPropertyChanged();
                }
            }
        }
    }
}
```

</details>

# コード全体(コードビハインド)
コードビハインドにイベントハンドラを記述する手法でのコードを以下に示します。
UI部分はほとんど同じでコマンドをバインドしていた部分がイベントハンドラになっています。
ModelはMVVMによるコードと同じです。

<details><summary><strong>UIのコード - MainWindow.xaml</strong></summary>

```xml:MainWindow.xaml
<Window x:Class="WPF_TabTest.MainWindow"
        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:WPF_TabTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <Grid>
        <!-- タブコントロール -->
        <TabControl x:Name="tabControl"
                    ItemsSource="{Binding TabItems}">

            <TabControl.ItemContainerStyle>
                <Style TargetType="{x:Type TabItem}">
                    <Setter Property="Height" Value="30" />
                </Style>
            </TabControl.ItemContainerStyle>
            
            <!-- ヘッダーテンプレート -->
            <TabControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <!-- ヘッダーテキストブロック -->
                        <TextBlock x:Name="txtHeader"
                                   Text="{Binding Header}"
                                   Padding="0 0 5 0"
                                   FontSize="14">
                            <TextBlock.Style>
                                <Style TargetType="{x:Type TextBlock}">
                                    <Style.Triggers>
                                        <!-- ModeがAddの時 -->
                                        <DataTrigger Binding="{Binding ElementName=txtHeader, Path=DataContext.Mode}" Value="Add">
                                            <Setter Property="Visibility" Value="Collapsed" />
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </TextBlock.Style>
                        </TextBlock>
                        
                        <!-- タブ削除ボタン -->
                        <Button x:Name="btnDeleteTab"
                                Click="btnDeleteTab_Click">
                            <Button.Style>
                                <Style TargetType="{x:Type Button}">
                                    <Setter Property="Template">
                                        <Setter.Value>
                                            <ControlTemplate TargetType="{x:Type Button}">
                                                <Path Height="13"
                                                      Stretch="Uniform"
                                                      Data="M20 6.91L17.09 4L12 9.09L6.91 4L4 6.91L9.09 12L4 17.09L6.91 20L12 14.91L17.09 20L20 17.09L14.91 12L20 6.91Z">
                                                    <Path.Style>
                                                        <Style TargetType="{x:Type Path}">
                                                            <Setter Property="Fill" Value="#BBB" />
                                                            <Setter Property="Stroke" Value="#BBB" />
                                                            <Style.Triggers>
                                                                <!-- ModeがAddの時 -->
                                                                <DataTrigger Binding="{Binding ElementName=txtHeader, Path=DataContext.Mode}" Value="Add">
                                                                    <Setter Property="Visibility" Value="Collapsed" />
                                                                </DataTrigger>
                                                                <!-- カーソルが重なったとき -->
                                                                <Trigger Property="IsMouseOver" Value="True">
                                                                    <Setter Property="Fill" Value="Black" />
                                                                    <Setter Property="Stroke" Value="Black" />
                                                                </Trigger>
                                                            </Style.Triggers>
                                                        </Style>
                                                    </Path.Style>
                                                </Path>
                                            </ControlTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </Style>
                            </Button.Style>
                        </Button>
                        
                        <!-- タブ追加ボタン -->
                        <Button x:Name="btnAddTab"
                                Click="btnAddTab_Click">
                            <Button.Style>
                                <Style TargetType="{x:Type Button}">
                                    <Setter Property="Template">
                                        <Setter.Value>
                                            <ControlTemplate TargetType="{x:Type Button}">
                                                <Path Height="18"
                                                      Stretch="Uniform"
                                                      Data="M20 14H14V20H10V14H4V10H10V4H14V10H20V14Z">
                                                    <Path.Style>
                                                        <Style TargetType="{x:Type Path}">
                                                            <Setter Property="Visibility" Value="Collapsed"/>
                                                            <Setter Property="Fill" Value="#BBB" />
                                                            <Setter Property="Stroke" Value="#BBB" />
                                                            <Style.Triggers>
                                                                <!-- ModeがAddの時 -->
                                                                <DataTrigger Binding="{Binding ElementName=txtHeader, Path=DataContext.Mode}" Value="Add">
                                                                    <Setter Property="Visibility" Value="Visible" />
                                                                </DataTrigger>
                                                                <!-- カーソルが重なったとき -->
                                                                <Trigger Property="IsMouseOver" Value="True">
                                                                    <Setter Property="Fill" Value="Black" />
                                                                    <Setter Property="Stroke" Value="Black" />
                                                                </Trigger>
                                                            </Style.Triggers>
                                                        </Style>
                                                    </Path.Style>
                                                </Path>
                                            </ControlTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </Style>
                            </Button.Style>
                        </Button>
                    </StackPanel>
                </DataTemplate>
            </TabControl.ItemTemplate>

            <!-- コンテンツテンプレート -->
            <TabControl.ContentTemplate>
                <DataTemplate>
                    <TextBlock x:Name="txtContent"
                               Text="{Binding Content}"
                               FontSize="25"
                               VerticalAlignment="Center"
                               HorizontalAlignment="Center"/>
                </DataTemplate>
            </TabControl.ContentTemplate>
        </TabControl>
    </Grid>
</Window>
```

</details>

<details><summary><strong>コードビハインドのコード - MainWindow.xaml.cs</strong></summary>

```cs:MainWinodw.xmal.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;   //ObservableCollection

namespace WPF_TabTest
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        //タブアイテムコレクション
        ObservableCollection<TabItemData> TabItems = new ObservableCollection<TabItemData>();

        //コンストラクタ
        public MainWindow()
        {
            InitializeComponent();

            //初期タブ
            TabItems.Add(new TabItemData()
            {
                Header = "初期タブ",
                Content = "初期タブのコンテンツ",
                Mode = "Normal"
            });

            //タブ追加ボタン
            TabItems.Add(new TabItemData() { Mode = "Add" });

            //データコンテキスト紐づけ
            this.DataContext = new { TabItems = TabItems };

            //最初のタブを選択
            tabControl.SelectedIndex = 0;
        }

        //削除ボタンクリックイベント
        private void btnDeleteTab_Click(object sender, RoutedEventArgs e)
        {
            //ボタンが押されたタブにバインドされているデータ取得
            TabItemData selectedData = ((Button)sender).DataContext as TabItemData;

            //タブを削除する
            TabItems.Remove(selectedData);
        }

        //タブ追加ボタンクリックイベント
        private void btnAddTab_Click(object sender, RoutedEventArgs e)
        {
            //タブ追加ボタンの前にタブを追加する
            TabItems.Insert(TabItems.Count - 1, new TabItemData()
            {
                Header = "追加されたタブ",
                Content = "追加されたタブのコンテンツ",
                Mode = "Normal"
            });

            //追加したタブを選択する
            tabControl.SelectedIndex = TabItems.Count - 2;
        }
    }
}
```

</details>

***

# おわりに
動的にタブを生成するタブコントロールを作成しました。
タブに削除ボタンを配置する方法はいくつか見つかりますが、最後尾のタブをタブ追加ボタンにする方法は見つかりませんでした。なので今回は自分なりの方法で実装しました。

# 参考
### アイコン
https://materialdesignicons.com/

### タブを動的に作る
http://gacken.com/blog/program/wpf-86_20160821-2/

### タブの閉じるボタン
https://tagsqa.com/detail/23233

### トリガー
https://teratail.com/questions/64512

https://www.wpf-tutorial.com/styles/trigger-datatrigger-event-trigger/

https://www.wpf-tutorial.com/tabcontrol/styling-the-tabitems/