WPF TabControl: Turning Off Tab Virtualization
Download Demo: PersistentTabControl.zip (41K)
Download attached behavior only: TabContent.cs (12K)
Background
WPF TabControl is known to "virtualize" its tabs when they are created via data binding. Only the visible tab actually exists and is bound to the selected data item. When selection changes, existing controls are reused and bound to new data context. The program must completely rely on data binding to redraw the tab: if any control on the tab is not data bound, its state will not be affected by selection change.
Although, well known, I don't think this behavior is officially documented anywhere in MSDN. If such documentation exists and someone can send me a link, I will be greatful.
Prior Art
This behavior caused questions since 2007.
Numerous methods were proposed to circumvent it
(example 1,
example 2,
example 3,
example 4)
, most revolving around creating a unique ContentControl
for each tab
and somehow planting the right control into the selected TabItem
.
Unfortunately, all the methods I found so far have one or more of the following drawbacks:
- Subclassing the tab control, e.g. creating
class TabControlEx : TabControl
. - "Hijacking"
ItemsSource
and/orSelectedItem
property, that can no longer be used as usual. - Requiring user to apply verbose XAML styles.
- Requiring user to apply more than one attached property to turn off virtualization behavior
Subclasing may not seem like such a big deal, but it will bite if for whatever reason you are required
to use an existing subclass of TabControl
, e.g. MyCompanyTabControl
that you cannot modify.
Design Goals
Not being satisfied with existing solutions, I was looking to create a method that
- Would turn virtualization off with one simple attached property, e.g.
TabContent.IsCached="True"
. - Would not change the meaning of
ItemsSource
orSelectedItem
. - Would not require creating a subclass of
TabControl
. - Would allow use of custom content templates.
- Would not require adding verbose code fragments to your XAML or code-behind.
Design Overview
The main idea behind my solution is to "hijack" the ContentTemplate
property
instead of ItemsSource
. I let the tab control to create templated items as normal,
but I provide a special ContentTemplate
which contains of a single Border
control. This Border
will be created once and remain on screen regardless
of what item is selected.
As in other methods, we create a unique ContentControl
for each tab. When tab selection
changes, we access the Border
and change its Child
to the content control that
corresponds to the currently selected tab.
The difference between this method and previous solutions is that we don't try to replace
automatically generated TabItem
s with our own. Instead, we allow regular date templating
process to take its due course, and then manipulate the Border
created from the content template.
The drawback of this approach is that the TabControl.ContentTemplate
property
is "hijacked" and cannot be used as normal. To mitigate this, we
- Provide an alternative property:
TabContent.Template
. - Carefully check for "illegal" use of
TabControl.ContentTemplate
property and throw descriptive exceptions when it is detected, that tell the programmer how to get things right. This allows the user to discover the problem early and fix it quickly.
Design Details
All attached properties related to tab control virtualization are located in the TabContent
class.
TabContent.IsCached
property acts as a "bootstrapper" that activates the whole tab content management
system. Suppose we have the following xaml:
<TabControl ikriv:TabContent.IsCached="True" />
This triggers the following chain of events:
XAML parser creates a new
TabControl
object.Tab control's attached property
TabContent.IsCached
is set toTrue
.Property change handler
TabContent.OnIsCachedChanged()
creates a data template in code and assigns it toTabControl.ContentTemplate
:<DataTemplate> <Border ikriv:TabContent.InternalTabControl= "{Binding RelativeSource={RelativeSource AncestorType=TabControl}}" /> </DataTemplate>
The WPF templating system creates a
Border
element from the template.The WPF binding system finds the border's ancestor of type
TabControl
and assigns it toTabContent.InternalTabControl
attached property.The property change handler for
TabContent.InternalTabControl
creates a new instance ofTabContent.ContentManager
class.The
ContentManager
object references the tab control and the border element and listens to theSelectionChanged
event on the tab control.When selection changes, the content manager examines selected
TabItem
.If selected
TabItem
does not yet have an associatedContentControl
, the content manager will generate a newContentControl
, and assign it to the tab item'sTabContent.InternalCachedContent
property of the tab item.The
ContentControl
associated with curently selectedTabItem
will then become theChild
of the border and will be displayed on screen.
Here's the resulting object graph:
The code for the TabContent.ContentManager
class looks as follows:
public class ContentManager { TabControl _tabControl; Decorator _border; public ContentManager(TabControl tabControl, Decorator border) { _tabControl = tabControl; _border = border; _tabControl.SelectionChanged += (sender, args) => { UpdateSelectedTab(); }; } public void UpdateSelectedTab() { _border.Child = GetCurrentContent(); } private ContentControl GetCurrentContent() { var item = _tabControl.SelectedItem; if (item == null) return null; var tabItem = _tabControl.ItemContainerGenerator.ContainerFromItem(item); if (tabItem == null) return null; var cachedContent = TabContent.GetInternalCachedContent(tabItem); if (cachedContent == null) { cachedContent = new ContentControl { DataContext = item, ContentTemplate = TabContent.GetTemplate(_tabControl), ContentTemplateSelector = TabContent.GetTemplateSelector(_tabControl) }; cachedContent.SetBinding(ContentControl.ContentProperty, new Binding()); TabContent.SetInternalCachedContent(tabItem, cachedContent); } return cachedContent; } }
Custom Content Templates
Users of the tab control can still define custom content template, but they must use TabContent.Template
attached property instead of regular ContentTemplate
property. Attempt to use both ContentTemplate
and TabContent.IsCached
will result in an exception.
<TabControl ikriv:TabContent.IsCached="True"> <ikriv:TabContent.Template> <DataTemplate> <!-- custom content template goes here --> </DataTemplate> </ikriv:TabContent.Template> </TabControl>
Important Note on Tab Content Creation
Persistent tab control creates tab content in a lazy initialization manner.
The visual tree is created for each tab only when the tab becomes visible for the first time.
If a tab never becomes visible, its content may never be created. This is well demonstrated
by the template DataContextOfHiddenTabs
test project.
Pros and Cons
The advantage of this design is that most of the complexity is hidden behind a single property. Virtualization could be turned off on any existing tab control
by making one addition to its XAML, plus one modification if it is using the ContentTemplate
property.
The major drawback of this design is that it does not work on Silverlight, because Silverlight's version of TabControl
does not have a ContentTemplate
property.
Update Oct 3, 2012
Version 1.1 fixes a crash that occured when currently selected item was removed. Thanks to Simon Brydon for discovering it.
Update Nov 23, 2012
Version 1.2 fixes a bug: DataContext
of a tab content was set to null
whenever the tab became invisible. This becomes important if tab contents monitors DataContext
changes and/or does something with its data context even when hidden from view. The fix is a one line
addition of
DataContext = item
towards the end of TabContent.cs
.
Thanks go to Jean-François Beaulac who spotted this bug.
Feedback
Questions? Comments? Feel free to
Leave feedback
Copyright (c) Ivan Krivyakov. Last updated: Nov 23, 2012