5
votes

I have a TabControl with a single specific tab and a collection bound to a collection of VMs, using a different user control. To do this I use a CompositeCollection and DataTemplates defined in the control's resources, selecting correct user control based on the VM type (acting as ContentTemplate).

I also set an ItemTemplate to define the tab item's name with binding, but it's not defined in the resource as I guess would conflict with the "ContentTemplate" ones.

It works fine, but I see the following error traced:

System.Windows.Data Error: 26 : ItemTemplate and ItemTemplateSelector are ignored for items already of the ItemsControl's container type; Type='TabItem'

It looks like there's some conflict between ContentTemplate and ItemTemplate, but I don't know how to fix it?

Code is the following:

<TabControl HorizontalAlignment="Left" Height="300" Width="500">
    <TabControl.Resources>
        <CollectionViewSource x:Key="personCollection" Source="{Binding Persons}" />
        <DataTemplate DataType="{x:Type viewModel:Main}">
            <local:MainView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewModel:Person}">
            <local:PersonView />
        </DataTemplate>
    </TabControl.Resources>
    <TabControl.ItemsSource>
        <CompositeCollection>
            <TabItem Header="General" Content="{Binding }"/>
            <CollectionContainer Collection="{Binding Source={StaticResource personCollection}}" />
        </CompositeCollection>
    </TabControl.ItemsSource>
    <TabControl.ItemTemplate>
        <DataTemplate DataType="viewModel:Person">
            <TextBlock Text="{Binding FirstName}" />
        </DataTemplate>
    </TabControl.ItemTemplate>
</TabControl>
1

1 Answers

6
votes

The error you observe is pretty obvious.
You define the ItemsSource of your TabControl as a CompositeCollection that contains elements of different types:

  • a TabItem "General";
  • a bunch of Person viewmodels.

So you're just mixing in one collection a view and some viewmodels - that's not neat. WPF informs you about this with the error message. The engine tries to create views (using DataTemplates) for the items and suddenly encounters an already specified view (a TabItem) that is exactly of type of the item container (because for the TabControl, a view for each viewmodel will be inserted in a TabItem container). So WPF simply inserts the TabItem into the TabControl and notifies that it has not used any ItemTemplate or ItemTemplateSelector for creating it.

You could simply ignore this error, because in the end the control should look like you want it to (I suppose).

An alternative (and probably neater) way is not to mix views and viewmodels in one collection, but rather specify a "general" viewmodel for the "General" tab:

<TabControl.ItemsSource>
    <CompositeCollection>
        <viewModel:GeneralViewModel/>
        <CollectionContainer Collection="{Binding Source={StaticResource personCollection}}" />
    </CompositeCollection>
</TabControl.ItemsSource>

And of course you then need to tell WPF how to visualize it:

<TabControl.Resources>
    <DataTemplate DataType="{x:Type viewModel:GeneralViewModel}">
        <local:GeneralView />
    </DataTemplate>
    <!-- ... -->
</TabControl.Resources>

Update

To address the issues in your comments.

1. How do I bind the GeneralViewModel to the one that exist in my DataContext?
This is possible, but with some overhead. You have to create a binding proxy for this. (Take a look here.)
The second thing you will need is a markup extension:

class BindingProxyValue : MarkupExtension
{
    public BindingProxy Proxy { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return Proxy.DataContext;
    }   
}

Use this markup extension together with the binding proxy in your collection:

<TabControl.Resources>
    <local:BindingProxy x:Key="Proxy" DataContext="{Binding GeneralViewModel}"/>
</TabControl.Resources>
<!--...-->
<CompositeCollection>
   <local:BindingProxyValue Proxy="{StaticResource Proxy}"/>
   <CollectionContainer Collection="{Binding Source={StaticResource personCollection}}" />
</CompositeCollection>

You can extend the markup extension as you like, e.g. in such a way that it can observe the object updates and replace the item in the target CompositeCollection.

2. How do I specify general tab's header name?
You can use ItemTemplates, but it becomes a little bit complicated. You have to implement a DataTemplateSelector for your TabControl:

class YourTabItemDataTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        FrameworkElement element = container as FrameworkElement;

        if (element != null && item != null)
        {
            if (item is GeneralViewmodel)
            {
                return (DataTemplate)element.FindResource("GeneralTabItemTemplate");
            }
            else
            {
                return (DataTemplate)element.FindResource("PersonTabItemTemplate");
            }
        }

        return null;
    }
}

Then you can define the different ItemTemplates for different TabItems:

<TabControl.Resources>
    <!-- ... -->
    <DataTemplate x:Key="GeneralTabItemTemplate">
        <TextBlock Text="General" />
    </DataTemplate>
    <DataTemplate x:Key="PersonTabItemTemplate">
        <TextBlock Text="{Binding FirstName}" />
    </DataTemplate>
</TabControl.Resources>

The question is: is this effort worth it or are you okay with that error message 26? You decide.