for WPF developers
Home Profile Tips 全記事一覧

タブを追加/削除する

(2017/06/29 7:56:17 created.)

切り替えられるコンテンツの数は固定であることもありますが、変化することもあります。ここでは、ContentViewModel を用いたタブの追加/削除方法について紹介します。

まず、タブが追加されるということは、TabControl コントロールの ItemsSource プロパティに指定されているコレクションの数が増えるということです。つまり、コレクションを保持している MainViewModel で、新たな ContentViewModel をコレクションに追加することで実現できます。

そういうわけで、MainViewModel に AddContentCommand を追加します。

MainViewModel.cs
  1. namespace Tips_TabControl.ViewModels
  2. {
  3.     using System.Collections.ObjectModel;
  4.  
  5.     public class MainViewModel : NotificationObject
  6.     {
  7.         private ObservableCollection<ContentViewModel> _contents = new ObservableCollection<ContentViewModel>();
  8.         /// <summary>
  9.         /// コンテンツのコレクションを取得します。
  10.         /// </summary>
  11.         public ObservableCollection<ContentViewModel> Contents
  12.         {
  13.             get { return this._contents; }
  14.         }
  15.  
  16.         private DelegateCommand _addContentCommand;
  17.         /// <summary>
  18.         /// コンテンツ追加コマンドを取得します。
  19.         /// </summary>
  20.         public DelegateCommand AddContentCommand
  21.         {
  22.             get
  23.             {
  24.                 return this._addContentCommand ?? (this._addContentCommand = new DelegateCommand(
  25.                 _ =>
  26.                 {
  27.                     var content = new ContentViewModel("Item" + _count);
  28.                     _count++;
  29.                     this.Contents.Add(content);
  30.                 },
  31.                 _ => this.Contents.Count < 4));
  32.             }
  33.         }
  34.  
  35.         private int _count;
  36.     }
  37. }

追加されるコンテンツの違いがわかるように、Title プロパティにカウンタの数値を含めます。また、実行条件として、コレクションの数を 4 個までとし、それ以上は追加できなくなるようにしています。

このコマンドを使用してコンテンツが追加できるように View のほうも変更します。

MainView.xaml
  1. <Window x:Class="Tips_TabControl.Views.MainView"
  2.         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.         Title="MainView" Height="300" Width="300">
  5.     <DockPanel>
  6.         <Button DockPanel.Dock="Top" Content="Add" Command="{Binding AddContentCommand}" />
  7.  
  8.         <TabControl ItemsSource="{Binding Contents}" SelectedIndex="0">
  9.             <TabControl.ItemTemplate>
  10.                 <DataTemplate>
  11.                     <TextBlock Text="{Binding Title}" />
  12.                 </DataTemplate>
  13.             </TabControl.ItemTemplate>
  14.             <TabControl.ContentTemplate>
  15.                 <DataTemplate>
  16.                     <TextBlock Text="{Binding Title, StringFormat={}これは{0}のコンテンツです}" />
  17.                 </DataTemplate>
  18.             </TabControl.ContentTemplate>
  19.         </TabControl>
  20.     </DockPanel>
  21. </Window>


追加できるのは 4 個まで

次に、タブを削除する方法を紹介します。タブを削除するということは、MainViewModel が保持しているコレクションの数を減らすということになります。ここでは、タブのヘッダ部に表示される閉じるボタンを押すことで削除するという実装方法を説明します。

タブのヘッダ部に表示するボタンは、ContentViewModel が DataContext になります。したがって、ContentViewModel の中になんらかのコマンドを実装する必要があります。そして、そのコマンドを起点として、MainViewModel のコレクションから該当するアイテムを削除しなくてはなりません。ここでは、ContentViewModel にあるイベントを定義して、それを MainViewModel が購読することでこの一連の動作を実現します。

ContentViewModel.cs
  1. namespace Tips_TabControl.ViewModels
  2. {
  3.     using System;
  4.  
  5.     public class ContentViewModel : NotificationObject, IDisposable
  6.     {
  7.         /// <summary>
  8.         /// 新しいインスタンスを生成します。
  9.         /// </summary>
  10.         /// <param name="title">タイトルを指定します。</param>
  11.         public ContentViewModel(string title)
  12.         {
  13.             this.Title = title;
  14.         }
  15.  
  16.         private string _title;
  17.         /// <summary>
  18.         /// タイトルを取得します。
  19.         /// </summary>
  20.         public string Title
  21.         {
  22.             get { return this._title; }
  23.             private set { SetProperty(ref this._title, value); }
  24.         }
  25.  
  26.         private DelegateCommand _closeCommand;
  27.         /// <summary>
  28.         /// 閉じるコマンドを取得します。
  29.         /// </summary>
  30.         public DelegateCommand CloseCommand
  31.         {
  32.             get
  33.             {
  34.                 return this._closeCommand ?? (this._closeCommand = new DelegateCommand(_ =>
  35.                 {
  36.                     Dispose();
  37.                     RaiseClosed();
  38.                 }));
  39.             }
  40.         }
  41.  
  42.         /// <summary>
  43.         /// 閉じたときに発生します。
  44.         /// </summary>
  45.         public event EventHandler<EventArgs> Closed;
  46.  
  47.         /// <summary>
  48.         /// Closed イベントを発行します。
  49.         /// </summary>
  50.         private void RaiseClosed()
  51.         {
  52.             var h = this.Closed;
  53.             if (h != null)
  54.                 h(this, EventArgs.Empty);
  55.         }
  56.  
  57.         #region IDisposable のメンバ
  58.         /// <summary>
  59.         /// リソースの破棄をおこないます。
  60.         /// </summary>
  61.         public void Dispose()
  62.         {
  63.             Dispose(true);
  64.             GC.SuppressFinalize(this);
  65.         }
  66.  
  67.         /// <summary>
  68.         /// アンマネージリソースの破棄をおこないます。
  69.         /// マネージリソースも同時に破棄できます。
  70.         /// </summary>
  71.         /// <param name="disposing">マネージリソースも破棄する場合に true を指定します。</param>
  72.         protected virtual void Dispose(bool disposing)
  73.         {
  74.             if (disposing)
  75.             {
  76.                 // 管理 (managed) リソースの破棄処理をここに記述します。
  77.             }
  78.  
  79.             // 非管理 (unmanaged) リソースの破棄処理をここに記述します。
  80.         }
  81.  
  82.         /// <summary>
  83.         /// デストラクタ
  84.         /// </summary>
  85.         ~ContentViewModel()
  86.         {
  87.             Dispose(false);
  88.         }
  89.         #endregion IDisposable のメンバ
  90.     }
  91. }

Closed イベントを定義し、CloseCommand が実行されたときにこのイベントを発行します。このとき、例えば閉じられた後はもう用済みとなる場合には、保持していたリソースを破棄しておく必要があるため、IDisposable インターフェースを実装し、Dispose() メソッドをコールするようにしています。

ContentViewModel に Closed イベントを定義したので、これを MainViewModel が購読し、発行されたときにコレクションから除外する必要があります。

MainViewModel.cs
  1. namespace Tips_TabControl.ViewModels
  2. {
  3.     using System;
  4.     using System.Collections.ObjectModel;
  5.  
  6.     public class MainViewModel : NotificationObject
  7.     {
  8.         private ObservableCollection<ContentViewModel> _contents = new ObservableCollection<ContentViewModel>();
  9.         /// <summary>
  10.         /// コンテンツのコレクションを取得します。
  11.         /// </summary>
  12.         public ObservableCollection<ContentViewModel> Contents
  13.         {
  14.             get { return this._contents; }
  15.         }
  16.  
  17.         private DelegateCommand _addContentCommand;
  18.         /// <summary>
  19.         /// コンテンツ追加コマンドを取得します。
  20.         /// </summary>
  21.         public DelegateCommand AddContentCommand
  22.         {
  23.             get
  24.             {
  25.                 return this._addContentCommand ?? (this._addContentCommand = new DelegateCommand(
  26.                 _ =>
  27.                 {
  28.                     var content = new ContentViewModel("Item" + _count);
  29.                     _count++;
  30.                     content.Closed += OnContentClosed;
  31.                     this.Contents.Add(content);
  32.                 },
  33.                 _ => this.Contents.Count < 4));
  34.             }
  35.         }
  36.  
  37.         /// <summary>
  38.         /// ContentViewModel の Closed イベントハンドラ
  39.         /// </summary>
  40.         /// <param name="sender">イベント発行元</param>
  41.         /// <param name="e">イベント引数</param>
  42.         private void OnContentClosed(object sender, EventArgs e)
  43.         {
  44.             var content = sender as ContentViewModel;
  45.             if (content == null)
  46.                 throw new Exception("ContentViewModel 以外から飛んでくることはあり得ない。");
  47.  
  48.             content.Closed -= OnContentClosed;
  49.             this.Contents.Remove(content);
  50.         }
  51.  
  52.         private int _count;
  53.     }
  54. }

AddContentCommand によってコンテンツを追加するときに Closed イベントを購読することで、すべてのコレクション要素の Closed イベントにイベントハンドラを登録させます。

イベントハンドラ内では、どの要素から発生したイベントなのかを入力引数である sender 変数をアンボックス化することで調べることができます。

閉じるボタンを実装した View は次のように記述します。

MainView.xaml
  1. <Window x:Class="Tips_TabControl.Views.MainView"
  2.         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.         Title="MainView" Height="300" Width="300">
  5.     <DockPanel>
  6.         <Button DockPanel.Dock="Top" Content="Add" Command="{Binding AddContentCommand}" />
  7.  
  8.         <TabControl ItemsSource="{Binding Contents}" SelectedIndex="0">
  9.             <TabControl.ItemTemplate>
  10.                 <DataTemplate>
  11.                     <StackPanel Orientation="Horizontal">
  12.                         <TextBlock Text="{Binding Title}" VerticalAlignment="Center" />
  13.                         <Button Content="x" Command="{Binding CloseCommand}" />
  14.                     </StackPanel>
  15.                 </DataTemplate>
  16.             </TabControl.ItemTemplate>
  17.             <TabControl.ContentTemplate>
  18.                 <DataTemplate>
  19.                     <TextBlock Text="{Binding Title, StringFormat={}これは{0}のコンテンツです}" />
  20.                 </DataTemplate>
  21.             </TabControl.ContentTemplate>
  22.             <TabControl.Style>
  23.                 <Style TargetType="TabControl" BasedOn="{StaticResource {x:Type TabControl}}">
  24.                     <Style.Triggers>
  25.                         <DataTrigger Binding="{Binding Contents.Count}" Value="0">
  26.                             <Setter Property="Visibility" Value="Collapsed" />
  27.                         </DataTrigger>
  28.                     </Style.Triggers>
  29.                 </Style>
  30.             </TabControl.Style>
  31.         </TabControl>
  32.     </DockPanel>
  33. </Window>

Add ボタンで追加できる

ヘッダ部のボタンを押すとそのタブが削除される