views:

2904

answers:

3

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?

+2  A: 

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

Anderson Imes
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.
Kent Boogaart
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.
Anderson Imes
@Kent - you hit it right on the money. That's exactly what I'd like to have.
dustyburwell
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.
Anderson Imes
@Anderson, 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)
Syd
+1  A: 

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

HTH, Kent

Kent Boogaart
+1 for code-based approach. Pretty easy to understand, rather than using triggers.
Anderson Imes
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.
dustyburwell
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?
dustyburwell
+1  A: 

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".

CyberMonk