WPF TabControl and DataTemplates
Asked Answered
B

5

10

I've got a set of ViewModels that I'm binding to the ItemsSource property of a TabControl. Let's call those ViewModels AViewModel, BViewModel, and CViewModel. Each one of those needs to have a different ItemTemplate (for the header; because they each need to show a different icon) and a different ContentTemplate (because they have very different interaction models).

What I'd like is something like this:

Defined in Resource.xaml files somewhere:

<DataTemplate x:Key="ItemTemplate" DataType="{x:Type AViewModel}">
    ...
</DataTemplate>

<DataTemplate x:Key="ItemTemplate" DataType="{x:Type BViewModel}">
    ...
</DataTemplate>

<DataTemplate x:Key="ItemTemplate" DataType="{x:Type CViewModel}">
    ...
</DataTemplate>

<DataTemplate x:Key="ContentTemplate" DataType="{x:Type AViewModel}">
    ...
</DataTemplate>

<DataTemplate x:Key="ContentTemplate" DataType="{x:Type BViewModel}">
    ...
</DataTemplate>

<DataTemplate x:Key="ContentTemplate" DataType="{x:Type CViewModel}">
    ...
</DataTemplate>

Defined separately:

<TabControl ItemTemplate="[ Some way to select "ItemTemplate" based on the type ]"
            ContentTemplate="[ Some way to select "ContentTemplate" based on the type ]"/>

Now, I know that realistically, each time I define a DataTemplate with the same key the system is just going to complain. But, is there something I can do that's similar to this that will let me put a DataTemplate into a TabControl based on a name and a DataType?

Bowker answered 28/8, 2009 at 16:30 Comment(0)
H
7

One way would be to use DataTemplateSelectors and have each one resolve the resource from a separate ResourceDictionary.

Haematocryal answered 28/8, 2009 at 17:27 Comment(3)
+1 for code-based approach. Pretty easy to understand, rather than using triggers.Coastward
I seem to remember there being a composite key that keyed off of Type and an identifier...perhaps in the .Net 3.0 version of WPF. Is that still around somewhere? That way, my DataTemplateSelector can be pretty generic and not have to worry about how to find different ResourceDictionaries and all that.Bowker
I found the ComponentResourceKey and I created a ComponentResourceKeyDataTemplateSelector that finds a DataTemplate based on the type of the item being templated and a ResourceId that you pass in. Would you consider this a decent solution?Bowker
C
19

The easiest way would be to use the automatic template system, by including the DataTemplates in the resources of a ContentControl. The scope of the templates are limited to the element they reside within!

<TabControl ItemsSource="{Binding TabViewModels}">
    <TabControl.ItemTemplate>
        <DataTemplate>
            <ContentControl Content="{Binding}">
                <ContentControl.Resources>
                    <DataTemplate DataType="{x:Type AViewModel}">
                        ...
                    </DataTemplate>
                    <DataTemplate DataType="{x:Type BViewModel}">
                        ...
                    </DataTemplate>
                    <DataTemplate DataType="{x:Type CViewModel}">
                        ...
                    </DataTemplate>
                </ContentControl.Resources>
            </ContentControl>
        </DataTemplate>
    </TabControl.ItemTemplate>
    <TabControl.Resources>
        <DataTemplate DataType="{x:Type AViewModel}">
            ...
        </DataTemplate>
         <DataTemplate DataType="{x:Type BViewModel}">
            ...
        </DataTemplate>
        <DataTemplate DataType="{x:Type CViewModel}">
            ...
        </DataTemplate>
    </TabControl.Resources>
</TabControl>
Carillon answered 19/5, 2012 at 15:49 Comment(1)
My TabControl only shows me the name of my viewmodel.. How would I show the corresponding view in the contentTemplate?Cotoneaster
C
8

You can remove the x:Key :) This will automatically apply the template when the given type is encountered (probably one of the most powerful and underused features of WPF, imo.

This Dr. WPF article goes over DataTemplates pretty well. The section you'll want to pay attention to is "Defining a Default Template for a Given CLR Data Type".

http://www.drwpf.com/blog/Home/tabid/36/EntryID/24/Default.aspx

If this doesn't help your situation, you might be able to do something close to what you are looking for using a Style (ItemContainerStyle) and setting the content and header based on the type using a data trigger.

The sample below hinges on your ViewModel having a property called "Type" defined pretty much like this (easily put in a base ViewModel if you have one):

public Type Type 
{ 
   get { return this.GetType(); } 
}

So as long as you have that, this should allow you to do anything you want. Note I have "A Header!" in a textblock here, but that could easily be anything (icon, etc).

I've got it in here two ways... one style applies templates (if you have a significant investment in these already) and the other just uses setters to move the content to the right places.

<Window x:Class="WpfApplication1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300"
        xmlns:local="clr-namespace:WpfApplication1">
    <Window.Resources>
        <CompositeCollection x:Key="MyCollection">
            <local:AViewModel Header="A Viewmodel" Content="A Content" />
            <local:BViewModel Header="B ViewModel" Content="B Content" />
        </CompositeCollection>

    <DataTemplate x:Key="ATypeHeader" DataType="{x:Type local:AViewModel}">
        <WrapPanel>
            <TextBlock>A Header!</TextBlock>
            <TextBlock Text="{Binding Header}" />
        </WrapPanel>
    </DataTemplate>
    <DataTemplate x:Key="ATypeContent" DataType="{x:Type local:AViewModel}">
        <StackPanel>
            <TextBlock>Begin "A" Content</TextBlock>
            <TextBlock Text="{Binding Content}" />
        </StackPanel>
    </DataTemplate>

    <Style x:Key="TabItemStyle" TargetType="TabItem">
        <Style.Triggers>
            <!-- Template Application Approach-->
            <DataTrigger Binding="{Binding Path=Type}" Value="{x:Type local:AViewModel}">
                <Setter Property="HeaderTemplate" Value="{StaticResource ATypeHeader}" />
                <Setter Property="ContentTemplate" Value="{StaticResource ATypeContent}" />
            </DataTrigger>

            <!-- Just Use Setters Approach -->
            <DataTrigger Binding="{Binding Path=Type}" Value="{x:Type local:BViewModel}">
                <Setter Property="Header">
                    <Setter.Value>
                        <WrapPanel>
                            <TextBlock Text="B Header!"></TextBlock>
                            <TextBlock Text="{Binding Header}" />
                        </WrapPanel>
                    </Setter.Value>
                </Setter>
                <Setter Property="Content" Value="{Binding Content}" />
            </DataTrigger>
        </Style.Triggers>
    </Style>
</Window.Resources>
<Grid>
    <TabControl ItemContainerStyle="{StaticResource TabItemStyle}" ItemsSource="{StaticResource MyCollection}" />
</Grid>

HTH, Anderson

Coastward answered 28/8, 2009 at 17:26 Comment(5)
That's not what he wants. He needs a composite key such that different templates with the same Types can be resolved from the same scope.Haematocryal
Yeah.. I edited it. I think using these datatriggers based on Type he should be able to set the Header/Content to something unique for each type. All he'd do then is assign the ItemContainerStyle to this style here. I think that ought to work, but let me know if I'm missing the mark. Should do what a data template selector would do, except in xaml.Coastward
@Kent - you hit it right on the money. That's exactly what I'd like to have.Bowker
Updated my answer with a working sample. Allows you to retain your original datatemplates if you want, but the only C# code you have to write is the "Type" property implementation. Hope this helps.Coastward
thanks for your answer. I have been looking for a tab control with multiple tabs but each tabitem is based on a different type.(+1)Annulet
H
7

One way would be to use DataTemplateSelectors and have each one resolve the resource from a separate ResourceDictionary.

Haematocryal answered 28/8, 2009 at 17:27 Comment(3)
+1 for code-based approach. Pretty easy to understand, rather than using triggers.Coastward
I seem to remember there being a composite key that keyed off of Type and an identifier...perhaps in the .Net 3.0 version of WPF. Is that still around somewhere? That way, my DataTemplateSelector can be pretty generic and not have to worry about how to find different ResourceDictionaries and all that.Bowker
I found the ComponentResourceKey and I created a ComponentResourceKeyDataTemplateSelector that finds a DataTemplate based on the type of the item being templated and a ResourceId that you pass in. Would you consider this a decent solution?Bowker
C
3

In this example I use DataTemplates in the resources section of my TabControl for each view model I want to display in the tab items. In this case I map ViewModelType1 to View1 and ViewModelType2 to View2. The view models will be set as DataContext object of the views automatically.

For displaying the tab item header, I use an ItemTemplate. The view models I bind to are of different types, but derive from a common base class ChildViewModel that has a Title property. So I can set up a binding to pick up the title to display it in the tab item header.

In addition I display a "Close" Button in the tab item header. If you do not need that, just remove the button from the example code so you just have the header text.

The contents of the tab items are rendered with a simple ItemTemplate which displays the view in a content control with Content="{Binding}".

<UserControl ...>
    <UserControl.DataContext>
        <ContainerViewModel></ContainerViewModel>
    </UserControl.DataContext>      
        <TabControl ItemsSource="{Binding ViewModels}"
                    SelectedItem="{Binding SelectedViewModel}">
            <TabControl.Resources>
                <DataTemplate DataType="{x:Type ViewModelType1}">
                    <View1/>
                </DataTemplate>
                <DataTemplate DataType="{x:Type ViewModelType2}">
                    <View2/>
                </DataTemplate>             
            </TabControl.Resources>
            <TabControl.ItemTemplate>
                <DataTemplate>
                    <DockPanel>
                        <TextBlock Text="{Binding Title}" />
                        <Button DockPanel.Dock="Right" Margin="5,0,0,0"
                                Visibility="{Binding RemoveButtonVisibility}" 
                                Command="{Binding DataContext.CloseItemCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TypeOfContainingView}}}"
                                >
                            <Image Source="/Common/Images/ActiveClose.gif"></Image>
                        </Button>
                    </DockPanel>
                </DataTemplate>
            </TabControl.ItemTemplate>
            <TabControl.ContentTemplate>
                <DataTemplate>
                    <ContentControl Content="{Binding}"/>
                </DataTemplate>
            </TabControl.ContentTemplate>
        </TabControl>
</UserControl>      


The user control which contains the tab control has a container view model of type ContainerViewModel as DataContext. Here I have a collection of all the view models displayed in the tab control. I also have a property for the currently selected view model (tab item).

This is a shortened version of my container view model (I skipped the change notification part).

public class ContainerViewModel
{
    /// <summary>
    /// The child view models.
    /// </summary>
    public ObservableCollection<ChildViewModel> ViewModels {get; set;}

    /// <summary>
    /// The currently selected child view model.
    /// </summary>
    public ChildViewModel SelectedViewModel {get; set;}
}
Celluloid answered 6/8, 2015 at 14:9 Comment(0)
E
1

Josh Smith uses exactly this technique (of driving a tab control with a view model collection) in his excellent article and sample project WPF Apps With The Model-View-ViewModel Design Pattern. In this approach, because each item in the VM collection has a corresponding DataTemplate linking the View to the VM Type (by omitting the x:Key as Anderson Imes correctly notes), each tab can have a completely different UI. See the full article and source code for details.

The key parts of the XAML are:

 <DataTemplate DataType="{x:Type vm:CustomerViewModel}">
   <vw:CustomerView />
 </DataTemplate>

<DataTemplate x:Key="WorkspacesTemplate">
<TabControl 
  IsSynchronizedWithCurrentItem="True" 
  ItemsSource="{Binding}" 
  ItemTemplate="{StaticResource ClosableTabItemTemplate}"
  Margin="4"
  />

There is one downside - driving a WPF TabControl from an ItemsSource has performance issues if the UI in the tabs is big/complex and therefore slow to draw (e.g., datagrids with lots of data). For more on this issue, search SO for "WPF VirtualizingStackPanel for increased performance".

Exodus answered 12/12, 2009 at 1:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.