XAML DataTemplate only ever creates one instance

1.9k views Asked by At

The following only ever creates one instance of MyTabView. I've confirmed this by putting a breakpoint in the constructor in MyTabView.xaml.cs. The view is displayed in a tab, and however many tabs I create, I only ever hit that constructor once.

<DataTemplate DataType="{x:Type vm:MyTabViewModel}" x:Shared="false">
    <vw:MyTabView />
</DataTemplate>

Tab control:

    <TabControl
        Grid.Row="1"
        ItemsSource="{Binding Tabs}"
        SelectedItem="{Binding SelectedTab}"
        >
        <TabControl.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding DisplayName}" />
            </DataTemplate>
        </TabControl.ItemTemplate>
    <TabControl>

This causes all the tabs to reflect share any state that's not bound to a view model: If you move a GridSplitter, that's the same GridSplitter in all the other tabs, so it appears to the user that you moved all of them. It's absurd.

I don't understand. Is there any way to use TabControl with multiple items of the same type?

EDIT: Added x:Shared="false" to DataTemplate.

UPDATE:

So I've found a couple of fixes, but I don't like them very much. I'm going to take a look at writing a Converter that converts ObservableCollection<Object> to ObservableCollection<TabItem> -- kind of like a live-updated version of

coll.Select(vm => new TabItem() { Content = vm });

...but we'll see whether or not it likes getting TabItem instances from ItemsSource. My money says don't bet on it. But we'll see.

UPDATE 2: Took a while to get back to this. The gimmick with swapping in a collection of tab items works, though SelectedItem is problem. Turns out there's another solution (below) that doesn't create that issue, and also avoids the complexity and goofiness of creating a "middleman" collection that has to mirror changes in the source collection.

3

There are 3 answers

3
Bradley Uffner On

Try adding an x:Shared="false" attribute to your template.

0
Brian Booth On

I also had yet another version of this problem. I have a UserControl with 2 grid columns. When selecting a TreeView node from the left grid column, it sets a ContentPresenter/Content on the right. My problem was that the rendered DataTemplate/UserControl in the ContentPresenter has a ListView which is instantiated only once. The result is when choosing a new item from the left-side-TreeView, the right-side UserControl/ListView would not deselect items from before.

Hopefully the above makes sense. Long-story-short, I found that the easiest solution is to create a DataTemplateSelector that always returns null from SelectTemplate.

public class DynamicTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        return null;
    }
}

XAML looks like this:


<UserControl.Resources>
    <local:DynamicTemplateSelector x:Key="DynamicSelector" />

    <DataTemplate DataType="{x:Type local:MyViewModel}">
        <local:MyUserControl />
    </DataTemplate>

</UserControl.Resources>
    ...

<ContentControl
    Content="{Binding ElementName=treeView, Path=SelectedItem}"
    ContentTemplateSelector="{StaticResource DynamicSelector}"
    />

0
grek40 On

I had some problems with ElementName bindings within the DataTemplate when using the accepted approach with dataTemplate.LoadContent.

Building on the Solution from @EdPlunkett, I tried to utilize an intermediate ContentPresenter per TabItem and it works well for my usage scenario.

  • Multiple tabs with the same template and same datacontext Type but different actual data contained
  • GridSplitter inside the DataTemplate
  • ElementName Binding inside the DataTemplate
  • I don't care whether the TabControl virtualization works for my amount of tabs (and I didn't check what happens to virtualization in my case)

Xaml:

<TabControl.ItemContainerStyle>
    <Style TargetType="TabItem">
        <EventSetter Event="Loaded" Handler="TabItem_Loaded"/>
    </Style>
</TabControl.ItemContainerStyle>
<TabControl.Resources>
    <DataTemplate x:Key="MyTemplateKey">
        ...
    </DataTemplate>
</TabControl.Resources>

Code behind (I changed DataTemplateKey to a string based key just for my use case with a named template resource. The DataTemplateKey approach for selecting the template should work as well)

private void TabItem_Loaded(object sender, RoutedEventArgs e)
{
    var tabItem = (sender as TabItem);

    if (null != tabItem.DataContext)
    {
        var dataTemplate = (DataTemplate)tabItem.FindResource("MyTemplateKey");

        var cp = new ContentPresenter();
        cp.ContentTemplate = dataTemplate;
        cp.Content = tabItem.DataContext;
        tabItem.Content = cp;
    }
}