Search code examples
wpfdata-bindingwpf-controlstabcontrolitemssource

WPF - Bound TreeView selection resets when switching tabs from TabControl


In my C# WPF Application I have a TabControl which has a model as an item source. In each TabItem a TreeView is created, which is also filled with data from the model.

My problem is that when I switch through the tabs of the TabControl, the selected items and expanded items collapse.

Do I need to save and load the selected and expanded items inside the Model everytime I switch the TabItems?

I created a small example to reproduce it:

Model

public  class ProjectModel
{
    public string Name { get; set; }
    public ObservableCollection<string> Structure { get; set; }
}

XAML

<TabControl x:Name="tcTest" >
    <!-- TabControl Style -->
    <TabControl.Resources>
        <Style TargetType="TabItem" BasedOn="{StaticResource {x:Type TabItem}}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TabItem}">
                        <Grid SnapsToDevicePixels="true">
                            <Border Padding="8,3" BorderThickness="1,1,1,0"  Height="27" Background="CadetBlue" >
                                <DockPanel>
                                    <TextBlock Text="{Binding Name}" />
                                </DockPanel>
                            </Border>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </TabControl.Resources>

    <!-- TabControl Content Template -->
    <TabControl.ContentTemplate>
        <DataTemplate>
            <StackPanel>
                <TreeView ItemsSource="{Binding Path=Structure}"/>
            </StackPanel>
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>

C#

public ObservableCollection<ProjectModel> Projects = new ObservableCollection<ProjectModel>();

public MainWindow()
{
    InitializeComponent();

    Projects.Add(new ProjectModel
    {
        Name = "Test",
        Structure = new ObservableCollection<string>
        {
            "Item 1",
            "Item 2",
            "Item 3",
            "Item 4",
        }
    });
    Projects.Add(new ProjectModel
    {
        Name = "Help",
        Structure = new ObservableCollection<string>
        {
            "Item 5",
            "Item 6",
            "Item 7",
            "Item 8",
        }
    });

    tcTest.ItemsSource = Projects;
}

Solution

  • Unfortunately yes, you need to notice selected and collapsed items if you want to keep them, though I would say, that it belongs more to the ViewModel, not to the Model.

    TabControl has only one so to say "visualization" of ContentTemplate for the current tab, if you switch to another tab only DataContext of this "visualization" being changed. So, as the same controls being filled with new data, all selections etc. go lost. If you add e.g. an empty TextBox, which isn't data bound, to the content's DataTemplate fill it by first tab, you will see the input also by the other tabs.

    Optionally you can try to handle it in the view layer, below is an example how you can notice the selection. You can so try to extend it to notice multiple selection and/or collapse state.

    public class RememberSelection : Behavior<TreeView>
    {
        private Dictionary<object, object> _selections = new Dictionary<object, object>();
        private bool _dcChanged = false;
    
        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.SelectedItemChanged += AssociatedObject_SelectedItemChanged;
            AssociatedObject.DataContextChanged += AssociatedObject_DataContextChanged;
        }
        protected override void OnDetaching()
        {
            AssociatedObject.SelectedItemChanged -= AssociatedObject_SelectedItemChanged;
            AssociatedObject.DataContextChanged -= AssociatedObject_DataContextChanged;
    
            base.OnDetaching();
        }
    
        private void AssociatedObject_DataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
        {
            _dcChanged = true;
        }
    
        private void AssociatedObject_SelectedItemChanged(object sender, System.Windows.RoutedPropertyChangedEventArgs<object> e)
        {
            if (_dcChanged)
            {
                _dcChanged = false;
    
                if (_selections.ContainsKey(AssociatedObject.DataContext) &&
                    AssociatedObject.ItemContainerGenerator.ContainerFromItem(_selections[AssociatedObject.DataContext]) is TreeViewItem itmCnt)
                {
                    itmCnt.IsSelected = true;
                }
                e.Handled = true;
            }
            else
            {
                _selections[AssociatedObject.DataContext] = AssociatedObject.SelectedItem;
            }
        }
    }
    
    <DataTemplate xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity">
        <StackPanel>
            <TreeView ItemsSource="{Binding Path=Structure}">                        
                <i:Interaction.Behaviors>
                    <beh:RememberSelection/>
                </i:Interaction.Behaviors>
            </TreeView>
        </StackPanel>
    </DataTemplate>
    

    Update for hierarchical template:

    public class RememberSelection : Behavior<TreeView>
    {
        private Dictionary<object, Tuple<HashSet<object>, HashSet<object>>> _selecols = new Dictionary<object, Tuple<HashSet<object>, HashSet<object>>>();
        private bool _dcChanged = false;
        private bool _isSelectedItemChangedRunning = false;
    
        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.SelectedItemChanged += AssociatedObject_SelectedItemChanged;
            AssociatedObject.DataContextChanged += AssociatedObject_DataContextChanged;
        }
        protected override void OnDetaching()
        {
            AssociatedObject.SelectedItemChanged -= AssociatedObject_SelectedItemChanged;
            AssociatedObject.DataContextChanged -= AssociatedObject_DataContextChanged;
    
            base.OnDetaching();
        }
    
        private void AssociatedObject_DataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
        {
            _dcChanged = true;
            if (e.OldValue != null)
            {
                var relevantContext = Tuple.Create(new HashSet<object>(), new HashSet<object>());
                foreach (var item in AssociatedObject.Items)
                {
                    SetSelectCollapse(AssociatedObject.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem, relevantContext);
                }
                _selecols[e.OldValue] = relevantContext;
            }
        }
        private void SetSelectCollapse(TreeViewItem tvi, Tuple<HashSet<object>, HashSet<object>> relevantContext)
        {
            if (tvi == null)
            {
                return;
            }
            if (tvi.IsSelected)
            {
                relevantContext.Item1.Add(tvi.DataContext);
            }
            if (tvi.IsExpanded && tvi.Items.Count>0)
            {
                relevantContext.Item2.Add(tvi.DataContext);
            }
            foreach (var item in tvi.Items)
            {
                SetSelectCollapse(tvi.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem, relevantContext);
            }
        }
    
        private void AssociatedObject_SelectedItemChanged(object sender, System.Windows.RoutedPropertyChangedEventArgs<object> e)
        {
            if (_isSelectedItemChangedRunning)
            {
                return;
            }
            _isSelectedItemChangedRunning = true;
            if (_dcChanged)
            {
                _dcChanged = false;
                if (_selecols.ContainsKey(AssociatedObject.DataContext))
                {
                    foreach (var item in AssociatedObject.Items)
                    {
                        SelectCollapse(AssociatedObject.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem, _selecols[AssociatedObject.DataContext]);
                    }
                }
    
                e.Handled = true;
            }
            _isSelectedItemChangedRunning = false;
        }
           
        private void SelectCollapse(TreeViewItem tvi, Tuple<HashSet<object>, HashSet<object>> relevantContext)
        {
            if (tvi == null)
            {
                return;
            }
            tvi.IsSelected = relevantContext.Item1.Contains(tvi.DataContext);
            tvi.IsExpanded = relevantContext.Item2.Contains(tvi.DataContext);
                
            foreach (var item in tvi.Items)
            {
                if (tvi.ItemContainerGenerator.Status != System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
                {
                    tvi.UpdateLayout();
                }
                SelectCollapse(tvi.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem, relevantContext);
            }
        }
    }