0

I am trying to make a basic window that uses a WPF TabControl to allow users to add new tabs. I want the end product to look somewhat like the way tabs work in a web browser where the last tab is just a "+" that when clicked it will add a new tab.

I am trying to write the XAML code to set this up and I found that I can specify multiple DataTemplates within "TabControl.Resources" and based on the "DataType" the correct DataTemplate will be used to display the correct view for each tab... but when dealing with the tab headers I can only specify a single DataTemplate for "TabControl.ItemTemplate"

This is what I have so far:

<TabControl ItemsSource="{Binding Tabs}">
    <TabControl.Resources>

        <!-- If the tab is of type "TabViewModel" I want this content -->
        <DataTemplate DataType="x:Type vm:TabViewModel">
            <!-- TabView is defined as a separate user control -->
            <v:TabView/>
        </DataTemplate>

        <!-- If the tab is of type "NewTabViewModel" I want this content -->
        <DataTemplate DataType="x:Type vm:NewTabViewModel">
            <!-- NewTabView is defined as a separate user control -->
            <v:NewTabView/>
        </DataTemplate>
    </TabControl.Resources>

    <TabControl.ItemTemplate>
        <!-- if the tab is of type "TabViewModel" I want this header -->
        <DataTemplate DataType="x:Type vm:TabViewModel">
            <TextBlock Text="{Binding Name}"/>
        </DataTemplate>

        <!-- If the tab is of type "NewTabViewModel" I want this header -->
        <!-- ERROR: Adding a second "DataTemplate" here results in an error -->
        <DataTemplate DataType="x:Type vm:NewTabViewModel">
            <TextBlock Text="+"/>
        </DataTemplate>
    </TabControl.ItemTemplate>
</TabControl>

Googling around I found some articles on setting up a TemplateSelector and writing a bunch of background C# code, but that seems drastically overkill for something this simple. I just want it to display the tab name if it is a regular TabViewModel object and a "+" if it is a NewTabViewModel object.

tjwrona1992
  • 8,614
  • 8
  • 35
  • 98
  • 1
    May be a header template with ContentPresenter and similar typed resources? https://stackoverflow.com/a/84317/12797700 – Drreamer Apr 30 '20 at 21:30
  • Do you really need to do that in the styiling of a `TabControl`? I would do that in a `UserControl` (containing an orizontal `StackPanel` with the `TabControl` and a `Button`. The `Button` then just add one object to the `TabControl` binded `ItemsSource` list) or in a custom `ControlTemplate` for the `TabControl` (following a similar idea). – Federico Rossi Apr 30 '20 at 21:39
  • @FedericoRossi I know I can just make a separate button to add tabs but I really want the nice clean look and feel from having a "+" at the end to add a new tab. Also at this point I'm just really curious how it can be done lol – tjwrona1992 Apr 30 '20 at 21:50
  • @Drreamer I'm not quite following what that link is saying. I'm fairly new to WPF so almost all of that goes over my head. I will look further into it though – tjwrona1992 Apr 30 '20 at 21:51
  • Please refer to answer from @Mark Feldman. I believe he describe my suggestion in detail. – Drreamer May 01 '20 at 06:29

1 Answers1

2

There's two completely different uses of DataTemplate here.

An ItemTemplate property is of type DataTemplate. Specifically, one DataTemplate, not a collection. It's expecting you to set that to whatever template you need, and that's what it will use to populate the tab header. Furthermore, in the case of TabControl, that's the template that will get applied to all headers; you don't get to change it on a per-tab basis.

The tab panels themselves are of type ContentControl, with each one bound to a view model. A ContentControl contains a ContentPresenter, which traverses the logical tree looking for a DataTemplate for the data type that it has been bound to (internally, the DataTemplate's DataType property is just syntactic sugar for setting the type itself as the x:Key).

Your problem is that you're trying to use the ItemTemplate like a ResourceDictionary, specifying multiple DataTemplates, when it's expecting you to provide the template itself, and only one. So to implement what you're after, all you need to do is give it a DataTemplate and populate it with a ContentPresenter (as suggested by Dreamer), the same as the tab panel itself has. This ContentPresenter has its own ResourceDictionary, and that's where you can put your header templates:

<TabControl ItemsSource="{Binding Tabs}">

    <TabControl.Resources>

        <!-- Panel templates -->

        <DataTemplate DataType="{x:Type vm:TabViewModel}">
            <v:TabView />
        </DataTemplate>

        <DataTemplate DataType="{x:Type vm:NewTabViewModel}">
            <v:NewTabView />
        </DataTemplate>
    </TabControl.Resources>

    <TabControl.ItemTemplate>
        <DataTemplate>
            <ContentPresenter Content="{Binding}">
                <ContentPresenter.Resources>

                    <!-- Header templates -->

                    <DataTemplate DataType="{x:Type vm:TabViewModel}">
                        <TextBlock Text="{Binding Name}"/>
                    </DataTemplate>

                    <DataTemplate DataType="{x:Type vm:NewTabViewModel}">
                        <TextBlock Text="+"/>
                    </DataTemplate>

                </ContentPresenter.Resources>
            </ContentPresenter>
        </DataTemplate>
    </TabControl.ItemTemplate>
</TabControl>
Mark Feldman
  • 15,731
  • 3
  • 31
  • 58
  • Thanks that compiles and runs! Now I just have the issue that instead of displaying the content it is displaying the class name. I think there is something wrong with my data binding. – tjwrona1992 May 01 '20 at 14:27
  • In the code you posted above you're doing `DataType="x:Type vm:NewTabViewModel"`, you're not still doing that, are you? You need to add the parenthesis (i.e. `DataType="{x:Type vm:NewTabViewModel}"`), otherwise the ContentPresenter won't be able to match the data type. – Mark Feldman May 01 '20 at 14:32
  • Last noobish question. How does `Content={Binding}` work with the `ContentPresenter`? Does that just bind to whatever the current data context is? – tjwrona1992 May 01 '20 at 14:51
  • Yes, correct. It's also worth nothing that with ContentPresenter, the DataContext is automatically set to the value of Content, whereas with ContentControl it is not. – Mark Feldman May 01 '20 at 22:17