'WPF TabControl - Add New Tab With Content Template

I am trying to make a demo application to help me understand WPF/MVVM. I have been struggling for 3 days looking at various tutorials and threads. I want to make a tab control with a new tab button (like here) that lets the user create a new tab with specified content template. I create my user control that I want to be the template here:

<UserControl x:Class="MvvmTest.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:MvvmTest"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <ListView d:ItemsSource="{d:SampleData ItemCount=5}">
            <ListView.View>
                <GridView>
                    <GridViewColumn/>
                </GridView>
            </ListView.View>
        </ListView>

    </Grid>
</UserControl>

It is just a control with a ListView. So, I want this ListView to be in any new tab that is opened.

Here is my main window with the actual tab control:

<Window x:Class="MvvmTest.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:MvvmTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Button Content="New Tab" Margin="703,6,10,401" Click="Button_Click"/>

        <TabControl Name= "TabControl1" Margin="0,33,0,-33" Grid.ColumnSpan="2">
        
        </TabControl>

    </Grid>
</Window>

In this code-behind, I try to create a new tab programmatically and set the content template to the new control.

private void Button_Click(object sender, RoutedEventArgs e)
{
    TabControl1.Items.Add(new TabItem() { ContentTemplate = UserControl1 });
}

This fails. I also tried setting properties in the XAML which also failed. I'm not sure what else to try.



Solution 1:[1]

If you're trying to use MVVM, where is your view model? The approach you have so far is not very MVVM because you're using code-behind to add tab items. The MVVM approach would be to bind the ItemSource property of the TabControl to a collection of items and let the view model add the items for you. You also cannot use a UserControl as a ContentTemplate like that without wrapping it in a DataTemplate definition.

The first thing to do is to define some view models:

// MvvmLight (from NuGet) is included for it's INotifyPropertyChanged
// (ViewModelBase) and ICommand (RelayCommand) classes. INotifyPropertyChanged
// is how Binding works between the View and the View Model. You could 
// implement these interfaces yourself if you wanted to. 
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;

using System.Collections.ObjectModel;
using System.Windows.Input;


namespace MvvmTest
{
    public class MainWindowViewModel : ViewModelBase
    {
        // store our list of tabs in an ObservableCollection
        // so that the UI is notified when tabs are added/removed
        public ObservableCollection<TabItemViewModel> Tabs { get; }
            = new ObservableCollection<TabItemViewModel>();

        // this code gets executed when the button is clicked
        public ICommand NewTabCommand 
            => new RelayCommand(() => Tabs.Add(new TabItemViewModel() 
               { Header = $"Tab {Tabs.Count + 1}"})); 
    }

    public class TabItemViewModel : ViewModelBase
    {
        // this is the title of the tab, note that the Set() method
        // invokes PropertyChanged so the view knows if the 
        // header changes
        public string Header
        {
            get => _header;
            set => Set(ref _header, value);
        }
        private string _header;

        // these are the items that will be shown in the list view
        public ObservableCollection<string> Items { get; } 
            = new ObservableCollection<string>() { "One", "Two", "Three" };
    }
}

Then you can fix your XAML so that it refers to the view-models that you defined. This requires defining the DataContext for your MainWindow and binding the elements of MainWindow to properties on the view model:

<Window x:Class="MvvmTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:MvvmTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <!--Set the DataContent to be an instance of our view-model class -->
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!--The Command of the button is bound to the View Model -->
        <Button Grid.Row="0" HorizontalAlignment="Right" Width="100"  
                Content="New Tab" 
                Command="{Binding NewTabCommand}" />

        <!--ItemsSource is bound to the 'Tabs' property on the view-
            model, while DisplayMemeberPath tells TabControl 
            which property on each tab has the tab's name -->
        <TabControl Grid.Row="1" 
                    ItemsSource="{Binding Tabs}" 
                    DisplayMemberPath="Header">

            <!--Defining the ContentTemplate in XAML when is best.
                This template defines how each 'thing' in the Tabs 
                collection will be presented. -->
            <TabControl.ContentTemplate>
                <DataTemplate>
                    <!--The UserControl/Grid were pointless, so I 
                        removed them. ItemsSource of the ListView is 
                        bound to an Items property on each object in
                        the Tabs collection-->
                    <ListView ItemsSource="{Binding Items}">
                        <ListView.View>
                            <GridView>
                                <GridViewColumn Header="Some column"/>
                            </GridView>
                        </ListView.View>
                    </ListView>
                </DataTemplate>
            </TabControl.ContentTemplate>
        </TabControl>

    </Grid>
</Window>

The result is that when you press the button, a new tab gets created and shown

enter image description here

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1