1

I do not understand why when I toggle the expanding of a tab's treeview in WPF, that it then affects the expanding of all tab's treeviews. I want each tab's treeview to be independent from one another. It's a very simple MVVM setup with a few classes.

enter image description here

Here are the files from the project

MainWindow.xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:data="clr-namespace:WpfApplication1"
        xmlns:view="clr-namespace:WpfApplication1.View"
        WindowStartupLocation="CenterScreen"
        Title="MainWindow" Height="350" Width="250">

    <Window.DataContext>
        <data:ViewModel/>
    </Window.DataContext>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!--<Button Content="Add" Command="{Binding AddCommand}" Grid.Row="0"></Button>-->
        <TabControl x:Name="tabControl1" ItemsSource="{Binding TabItems}" Grid.Row="1" Background="LightBlue">
            <TabControl.ItemTemplate>
                <DataTemplate>
                        <TextBlock Text="{Binding Header}" VerticalAlignment="Center"/>
                </DataTemplate>
            </TabControl.ItemTemplate>

            <TabControl.ContentTemplate>
                <DataTemplate>
                    <view:TabItemView />
                </DataTemplate>
            </TabControl.ContentTemplate>


        </TabControl>

    </Grid>
</Window>

TabItemView.xaml

<UserControl x:Class="WpfApplication1.View.TabItemView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <TextBlock Grid.Row="0" Text="{Binding Content}" />
        <TreeView Grid.Row="1" Background="Transparent">
            <TreeViewItem Header="Favorites">
                <TreeViewItem Header="USA"></TreeViewItem>
                <TreeViewItem Header="Canada"></TreeViewItem>
                <TreeViewItem Header="Mexico"></TreeViewItem>
            </TreeViewItem>
        </TreeView>
    </Grid>
</UserControl>

ViewModel.cs

using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Input;

namespace WpfApplication1
{
    public class ViewModel
    {
        private ObservableCollection<TabItem> tabItems;

        public ObservableCollection<TabItem> TabItems
        {
            get { return tabItems ?? (tabItems = new ObservableCollection<TabItem>()); }
        }

        public ViewModel()
        {
            TabItems.Add(new TabItem { Header = DateTime.Now.ToString("Tab 1"), Content = DateTime.Now.ToString("F") });
            TabItems.Add(new TabItem { Header = DateTime.Now.ToString("Tab 2"), Content = DateTime.Now.ToString("F") });
            TabItems.Add(new TabItem { Header = DateTime.Now.ToString("Tab 3"), Content = DateTime.Now.ToString("F") });
        }
    }

    public class TabItem
    {
        public string Header { get; set; }
        public string Content { get; set; }
    }
}
JokerMartini
  • 5,674
  • 9
  • 83
  • 193
  • I want it to be a new viewmodel for each one. How do i know if im doing that? – JokerMartini Oct 14 '15 at 12:29
  • [Related](http://stackoverflow.com/q/601826/1997232). You have similar problem: `TreeView` is being reused. It is safe to *ignore* that (because normally you shouldn't have View content detached from ViewModel), e.g. `TextBlock` uses binding and it works correctly (press Control+T several times and switch between pages). – Sinatr Oct 14 '15 at 13:13
  • I don't usually go back and look at old questions someone referred me to. But this question was actually written up pretty well by you and, for some reason I don't understand, was never really answered by anyone else (the "related" in the previous comment is of only limited value IMHO). I only learned about the question this week, when I went back to check on my blog (and start writing some new stuff there) and saw your comment asking me about it. I hope you find the answer below helpful, and I apologize it took so long for me to notice your comment. :) – Peter Duniho Feb 26 '16 at 22:44

1 Answers1

0

The TabControl reuses the same content element for each item. All it does is change the bound data context (i.e. your view model), updating the elements within the templated element that are bound to the view model. So the other state of the view remains the same as you switch from one tab to the next.

It is possible to force a new content element to be created each time you change the tab; one way of doing this is to declare the content element's template as a resource, add x:Shared="False" to the resource declaration, and then use the resource as the value for a Setter applied in a Style targeting the TabItem type.

Going through the setter to apply the template to each TabItem is required — and by TabItem here I mean the WPF TabItem, not your view model class, the name of which I'd change, to avoid confusion, if I were you. Using x:Shared won't help if you just set the TabControl.ContentTemplate directly once.

For example:

  <TabControl.Resources>
    <DataTemplate x:Key="tabItemTemplate" x:Shared="False">
      <l:TabItemView />
    </DataTemplate>
    <s:Style TargetType="TabItem">
      <Setter Property="ContentTemplate" Value="{StaticResource ResourceKey=tabItemTemplate}"/>
    </s:Style>
  </TabControl.Resources>

However, this has the opposite effect: rather than keeping the state for each item as you switch from tab to tab, the view state is reset entirely, because a whole new content element is created every time you switch.

If this is acceptable to you, then that will work fine. If however you are looking for each tab to retain whatever configuration it was in when the user last viewed that tab, you will have to preserve that state yourself. For example, you could add a bool property to your view model to remember the current setting for each item and bind that to the IsExpanded property for the top-level TreeViewItem:

View model:

public class TabItem
{
    public string Header { get; set; }
    public string Content { get; set; }
    public bool IsExpanded { get; set; }
}

View:

<UserControl x:Class="TestSO33125188TreeViewTemplate.TabItemView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <TextBlock Grid.Row="0" Text="{Binding Content}" />
    <TreeView Grid.Row="1" Background="Transparent">
      <TreeViewItem Header="Favorites" IsExpanded="{Binding IsExpanded, Mode=TwoWay}">
        <TreeViewItem Header="USA"></TreeViewItem>
        <TreeViewItem Header="Canada"></TreeViewItem>
        <TreeViewItem Header="Mexico"></TreeViewItem>
      </TreeViewItem>
    </TreeView>
  </Grid>
</UserControl>

Note that for this to work, you need to explicitly set the binding Mode property to TwoWay, as the default is OneWay and won't otherwise copy the current TreeViewItem.IsExpanded value back to the view model.

Note also that for your specific example, you can get away with a simple container view model, without implementing INotifyPropertyChanged or similar mechanism. WPF is forced to copy the updated property value back to the view whenever the current tab is changed. But personally, I would go ahead and add the INotifyPropertyChanged implementation; it would be too easy to find yourself reusing the technique in a different scenario where WPF doesn't automatically detect that you've updated the property value, causing a frustrating bug that takes a while to track down and fix.

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136