Adding tab items dynamically in WPF / MVVM

Some instructions on how to add/remove tab items within a WPF / MVVM setting.

Step 1: Create a new WPF application

Step 2: Add classes to implement ICommand

RelayCommand.cs

using System;
using System.Windows.Input;

namespace Tabs
{
   public class RelayCommand<T> : ICommand
   {
      private readonly Predicate<T> _canExecute;
      private readonly Action<T> _execute;

      public RelayCommand(Action<T> execute)
         : this(execute, null)
      {
         _execute = execute;
      }

      public RelayCommand(Action<T> execute, Predicate<T> canExecute)
      {
         if (execute == null)
         {
            throw new ArgumentNullException("execute");
         }
         _execute = execute;
         _canExecute = canExecute;
      }

      public bool CanExecute(object parameter)
      {
         return _canExecute == null || _canExecute((T)parameter);
      }

      public void Execute(object parameter)
      {
         _execute((T)parameter);
      }

      public event EventHandler CanExecuteChanged
      {
         add { CommandManager.RequerySuggested += value; }
         remove { CommandManager.RequerySuggested -= value; }
      }
   }

   public class RelayCommand : ICommand
   {
      private readonly Predicate<object> _canExecute;
      private readonly Action<object> _execute;

      public RelayCommand(Action<object> execute)
         : this(execute, null)
      {
         _execute = execute;
      }

      public RelayCommand(Action<object> execute, Predicate<object> canExecute)
      {
         if (execute == null)
         {
            throw new ArgumentNullException("execute");
         }
         _execute = execute;
         _canExecute = canExecute;
      }

      public bool CanExecute(object parameter)
      {
         return _canExecute == null || _canExecute(parameter);
      }

      public void Execute(object parameter)
      {
         _execute(parameter);
      }

      // Ensures WPF commanding infrastructure asks all RelayCommand objects whether their
      // associated views should be enabled whenever a command is invoked 
      public event EventHandler CanExecuteChanged
      {
         add
         {
            CommandManager.RequerySuggested += value;
            CanExecuteChangedInternal += value;
         }
         remove
         {
            CommandManager.RequerySuggested -= value;
            CanExecuteChangedInternal -= value;
         }
      }

      private event EventHandler CanExecuteChangedInternal;

      public void RaiseCanExecuteChanged()
      {
         CanExecuteChangedInternal.Raise(this);
      }
   }
}

EventRaiser.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Tabs
{
   public static class EventRaiser
   {
      public static void Raise(this EventHandler handler, object sender)
      {
         handler?.Invoke(sender, EventArgs.Empty);
      }

      public static void Raise<T>(this EventHandler<EventArgs<T>> handler, object sender, T value)
      {
         handler?.Invoke(sender, new EventArgs<T>(value));
      }

      public static void Raise<T>(this EventHandler<T> handler, object sender, T value) where T : EventArgs
      {
         handler?.Invoke(sender, value);
      }

      public static void Raise<T>(this EventHandler<EventArgs<T>> handler, object sender, EventArgs<T> value)
      {
         handler?.Invoke(sender, value);
      }
   }
}

EventArgs.cs

using System;

namespace Tabs
{
   public class EventArgs<T> : EventArgs
   {
      public EventArgs(T value)
      {
         Value = value;
      }

      public T Value { get; private set; }
   }
}

Step 3: Add the ViewModel class

We need a class to implement the button click events, as well contain data for tab-related information.

MainWindowViewModel.cs

using System;
using System.ComponentModel;
using System.Windows.Input;
using System.Diagnostics;
using System.Collections.ObjectModel;
using System.Linq;

namespace Tabs
{
   public class MainWindowViewModel : INotifyPropertyChanged
   {
      static int tabs = 1;

      public MainWindowViewModel()
      {
         Titles = new ObservableCollection<Item>();
      }

      public ObservableCollection<Item> Titles
      {
         get { return _titles; }
         set
         {
            _titles = value;
            OnPropertyChanged("Titles");
         }
      }

      public class Item
      {
         public string Header { get; set; }
         public string Content { get; set; }
      }

      private ICommand _addTab;
      private ICommand _removeTab;
      private ObservableCollection<Item> _titles;

      public ICommand AddTab
      {
         get
         {
            return _addTab ?? (_addTab = new RelayCommand(
               x =>
               {
                  AddTabItem();
               }));
         }
      }

      public ICommand RemoveTab
      {
         get
         {
            return _removeTab ?? (_removeTab = new RelayCommand(
               x =>
               {
                  RemoveTabItem();
               }));
         }
      }

      private void RemoveTabItem()
      {
         Titles.Remove(Titles.Last());
         tabs--;
      }

      private void AddTabItem()
      {
         var header = "Tab " + tabs;
         var content = "Content " + tabs;
         var item = new Item { Header = header, Content = content };
         
         Titles.Add(item);
         tabs++;
         OnPropertyChanged("Titles");
      }

      public event PropertyChangedEventHandler PropertyChanged;

      private void OnPropertyChanged(string propertyName)
      {
         VerifyPropertyName(propertyName);
         var handler = PropertyChanged;
         handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
      }

      [Conditional("DEBUG")]
      private void VerifyPropertyName(string propertyName)
      {
         if (TypeDescriptor.GetProperties(this)[propertyName] == null)
            throw new ArgumentNullException(GetType().Name + " does not contain property: " + propertyName);
      }
   }
}

Step 4: Create the XAML view

MainWindow.xaml

<Window x:Class="Tabs.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:Tabs"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="10" />
            <RowDefinition Height="*" />
            <RowDefinition Height="10" />
        </Grid.RowDefinitions>

        <Grid 
            Grid.Column="0" Grid.Row="1">
            <StackPanel>
                <Button 
                    Margin="0,0,0,10"
                    Content="Add"
                    Command="{Binding AddTab}"
                    Height="30" Width="80" 
                    VerticalAlignment="Top" />

                <Button 
                    Content="Remove"
                    Command="{Binding RemoveTab}"
                    Height="30" Width="80" 
                    VerticalAlignment="Top" />
            </StackPanel>
            
        </Grid>

        <TabControl 
            TabStripPlacement="Top"
            ItemsSource="{Binding Titles, Mode=TwoWay}"
            Grid.Column="1" Grid.Row="1">

            <TabControl.ItemTemplate>
                <DataTemplate>
                    <TextBlock
                    Text="{Binding Header}" />
                </DataTemplate>
            </TabControl.ItemTemplate>
            
            <TabControl.ContentTemplate>
                <DataTemplate>
                    <TextBlock
                    Text="{Binding Content}" />
                </DataTemplate>
            </TabControl.ContentTemplate>
        </TabControl>
    </Grid>
</Window>

So that when the code is run and the ‘Add’ button is pressed new tab items are added dynamically:

And that on pressing the ‘Remove’ button a few times tab item data is deleted from the list and the view is updated accordingly: