WPF TabControl: Children unloaded when other tab is selected - wpf

Is it possible to prevent WPF TabControl from unloading the children of a TabItem when selecting an other tab?
The problem I'm facing is similar to the one described here:
WPF TabControl - Preventing Unload on Tab Change?
The solution provided there seems to work only if the TabControl is data bound.
It doesn't work if you add TabItems:
<local:TabControlEx>
<TabItem Header="First Tab">
<TreeView ItemsSource="{Binding TreeNodes}" Unloaded="treeView_Unloaded">
<TreeView.Resources>
<DataTemplate DataType="{x:Type local:NodeViewModel}">
<TextBlock Text="{Binding NodeName}" />
</DataTemplate>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</TabItem>
<TabItem Header="Second Tab">
<TextBlock Name="txText2">Second Text 2</TextBlock>
</TabItem>
</local:TabControlEx>
When you select "Second Tab", treeView_Unloaded is triggered.
Background:
In my real application, one of the TabItems contains a UserControl containing a data bound TreeView: TreeViewItem.IsSelected is bound to a property. Setting the Property bound to IsSelected selects the corresponding TreeViewItem. If the user switches to another tab, the TreeView is unloaded (removed from VisualTree). In that situation, setting IsSelected to true on any node not having a corresponding TreeViewItem causes the TreeView to misbehave - probably because the TreeView does not create a TreeViewItem for the node which should be selected, because the TreeView is not currently part of the visual tree. So what I want to achieve is that I can select any other node by setting IsSelected to true - even if the TreeView is currently on a non-visible tab. Moving the TreeView to some place outside the TabControl seems to resolve the problem - even if it is inside a panel which has visibility collapsed. So, visibility doesn't seem to be the problem, but the fact that the TreeView is not currently part of the visual tree.

The solution presented in the original question works if you override the control template based on the default control template of TabControl:
- Remove ContentPresenter
- Add a Grid named PART_ItemsHolder.
No more Unloaded events - no more trouble with the TreeView when it is on a non-selected TabItem.

Related

WPF Tabcontrol DataTemplate and child user control initialization

I have a view with a TabControl and the content of each tab is defined as a Resource in TabControl.Resources. In this content, I have a user control which basically contains a datagrid.
<TabControl ItemsSource="{Binding MyTabs}"
SelectionChanged="TabSelected">
<TabControl.Resources>
<DataTemplate DataType="{x:Type uc:Tab}">
<uc:MyUserControlWithDatagrid />
</DataTemplate>
</TabControl.Resources>
</TabControl>
To my surprise, I notice that even if I have multiple tab items, I only enter once in MyUserControlWithDatagrid's constructor. I am guessing that it is "shared" between all tab items?
If that is correct, how can I make it so that a usercontrol instance is created for each tab item?
You may want to read this article about the virtualization of tabs.
You can download the code samples from here.
The solution proposed enables you to define a custom template:
<TabControl ikriv:TabContent.IsCached="True">
<ikriv:TabContent.Template>
<DataTemplate>
<!-- custom content template goes here -->
</DataTemplate>
</ikriv:TabContent.Template>
</TabControl>

Listbox "IsSelected" binding only partially working

I have a ListBox that I populate dynamically via a binding (this is defined in a DataTemplate, which is why the binding is somewhat unusual):
<ListBox SelectionMode="Extended" ItemsSource="{Binding DataContext.ResultList, RelativeSource={RelativeSource AncestorType=Window}}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Label Content="{Binding Object}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Each ListBoxItem's IsSelected property is bound to an IsSelected property on a custom object.
When I select individual ListBoxItems, the binding works properly - the custom object's IsSelected property is updated in my ViewModel. However, if I select all of the ListBoxItems with a Ctrl+A command, only the currently visible ListBoxItems (those that are currently in my scrolling viewport) update their ViewModel bindings. On the frontend, all the ListBoxItems appear to be selected, and the ListBox.SelectedItems.Count property on the container ListBox shows that all items are selected.
Furthermore, as I scroll through the ListBox after selecting all ListBoxItems with Ctrl+A, the bindings are successfully updated when each ListBoxItem is scrolled into view.
Why does this binding seem to be only partially working? Is there a better way to handle the binding of the IsSelected property when large numbers of ListBoxItems can be selected simultaneously?
Edit:
This behavior doesn't happen exclusively with the Ctrl+A command - I get the same results when selecting all the items using a shift+click.
I think the behavior you're seeing is to due to VirtualizingStackPanel.IsVirtualizing which is True by default when binding to ItemsSource of ListBox
if you for eg set your ListBox such as:
<ListBox VirtualizingStackPanel.IsVirtualizing="False" SelectionMode="Extended" ItemsSource="{Binding DataContext.ResultList, RelativeSource={RelativeSource AncestorType=Window}}">
or
<ListBox ...>
...
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
then you should see all your bound items have their IsSelected updated accordingly with Ctrl+A or Shift + ...
Properties such as Count of the collection even with virtualization would report the correct value to accommodate for things like computing the required ScrollBar.Height. Items which are outside the View-port do not get rendered hence no bindings are in effect on them until they actually get used.

WPF TabControl, switching to another TabItem does not work, sticks to first TabItem

I have a control that contains following XAML code. It works fine excepted that I caInnot switch to another TabItem. I read that TabControl virtualizes the TabItem, I suspect the strange behaviour, namely that I cannot get to display any other TabItem as the first one, is related to this.
<TabControl ItemsSource="{Binding Items}">
<TabControl.ItemTemplate>
<DataTemplate> <!-- header -->
<TextBlock Text="{Binding Title}"></TextBlock>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate x:Shared="False"> <!-- tabitem content -->
<controls:ItemControl Item="{Binding}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
I tried to set the x:Shared attribute of the DataTemplate to False but has not the expected effect. Is there a way to reach this without going the way of using a custom style and replacing the TabControl with an ItemsControl. I mean the functionality of TabControl is what I would like, I would like to simply use it with ItemsSource binding...
This behaviour will happen if you are binding to a collection that has duplicate objects in it.
Duplication can occur due to having added an object multiple times or because equality has been redefined for the objects in question.

How to bind to Tabcontrol.Items

I have a WPF application that I'm trying to dynamically add items to a tabcontrol. I have a list of menu items that should be databound to the tabcontrol's items. The only problem is that TabControl.Items does not notify others that items have been added. I've tested this by binding instead to TabControl.Items.Count and get calls to the converter (but the value passed in is the count and not something useful). Here's the relevent code that doesn't get databound properly because Items doesn't call out updates:
<MenuItem ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TabControl}}, Path=Items, Converter={StaticResource TabControlItemConverter}}">
This MenuItem XAML is inside a ControlTemplate for a TabControl. With static items, i.e., items that are already defined in a TabControl, this code works perfectly. But I have a TabControl that gets items added at runtime and can't seem to update this binding. Has anyone added some sort of attached property to a TabControl that can bind to the Items collection?
Edit for background info
The TabControl that has items added to it is a region (this is a Prism application). Here is the relevent XAML
<TabControl cal:RegionManager.RegionName="{x:Static local:LocalRegionNames.SelectedItemRegion}" >
<TabControl.Resources>
<Style TargetType="TabItem" BasedOn="{StaticResource TabItemStyle}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Style="{StaticResource tabItemImage}" Height="20" />
<TextBlock Text="{Binding Content.DataContext.TabHeader, RelativeSource={RelativeSource AncestorType=TabItem}}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.Resources>
</TabControl>
The relevent code for adding a view to the region is here:
ProjectDetailView view = new ProjectDetailView();
ProjectDetailViewModel viewModel = new ProjectDetailViewModel();
viewModel.CurrentProject = project;
view.DataContext = viewModel;
IRegionManager retManager = RegionManager.Regions[LocalRegionNames.SelectedItemRegion].Add(view, null, true);
RegionManager.Regions[LocalRegionNames.SelectedItemRegion].Activate(view);
All this works fine...views get added, the tab control adds items, and views appear. But the Items property on the tabcontrol never broadcasts the changes to its collection.
You do the same thing for TabControls, you bind the ItemsSource, the only thing you need to take into account is that the source collection should implement INotifyCollectionChanged if you want it updated if items are added. ObservableCollection<T> already implements the interface and is often used as source for such bindings.

WPF : TreeView virtualization not working

What can stop a TreeView from virtualizing if the TreeView is set up as follows?
<TreeView
ItemsSource="{Binding}"
VirtualizingStackPanel.IsVirtualizing="True">
<TreeView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</TreeView.ItemsPanel>
<TreeView.ItemContainerStyle>
<Style
TargetType="{x:Type TreeViewItem}">
<Setter
Property="IsExpanded"
Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
I have one that is not virtualizing, when i expand the nodes (and use snoop to check) i have all of the TreeViewItems being created. I am wondering if there is some combination of containers that would prevent the TreeView from virtualizing its content. (like hosting it in a StackPanel for example)
The problem was with the styling. After some research we found that there was an unnamed style targeting the TreeView (i.e. one with DataType={x:Type TreeView} without an x:Key) and one targetting the TreeViewItem in our App.xaml (or equivalent) It was overriding the ControlTemplate for each respectively.
These styles did not have the triggers to set the ItemsPanel to a VirtualizingStackPanel and had no mention of any virtualization. When the styles are removed the TreeView works fine. Even though the local properties set the ItemsPanel and the VirtualizingStackPanel.Isvirtualizing="True" on the TreeView these properties were not being propogated to the TreeViewItems so the top level of the TreeView would virtualize whilst the sub categories would not (as their virtualization behaviour was dependant on the TreeViewItem)
Had the same problem. In my case, the initial size of the TreeView was not limited (TreeView is within a popup). Therfore, the virtualization panel initialized all controls for the first time.
Setting the MaxHeigt of the TreeView to 1000 solvend the problem.

Resources