Click here to Skip to main content
15,883,901 members
Articles / Desktop Programming / WPF
Alternative
Article

WPF TabControl: Turning Off Tab Virtualization

Rate me:
Please Sign up or sign in to vote.
4.92/5 (36 votes)
2 Dec 2012CPOL5 min read 176.8K   3.8K   47   75
This is an alternative for "Persist the Visual Tree when switching tabs in the WPF TabControl (optimized)".

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/or SelectedItem 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

  1. Would turn virtualization off with one simple attached property, e.g. TabContent.IsCached="True".
  2. Would not change the meaning of ItemsSource or SelectedItem.
  3. Would not require creating a subclass of TabControl.
  4. Would allow use of custom content templates.
  5. 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 TabItems 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

  1. Provide an alternative property: TabContent.Template.
  2. 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:

XAML
<TabControl ikriv:TabContent.IsCached="True" />

This triggers the following chain of events:

  1. XAML parser creates a new TabControl object.

  2. Tab control's attached property TabContent.IsCached is set to True.

  3. Property change handler TabContent.OnIsCachedChanged() creates a data template in code and assigns it to TabControl.ContentTemplate:

  4. XAML
    <DataTemplate>
        <Border ikriv:TabContent.InternalTabControl=
                "{Binding RelativeSource={RelativeSource AncestorType=TabControl}}" />
    </DataTemplate>
  5. The WPF templating system creates a Border element from the template.

  6. The WPF binding system finds the border's ancestor of type TabControl and assigns it to TabContent.InternalTabControl attached property.

  7. The property change handler for TabContent.InternalTabControl creates a new instance of TabContent.ContentManager class.

  8. The ContentManager object references the tab control and the border element and listens to the SelectionChanged event on the tab control.

  9. When selection changes, the content manager examines selected TabItem.

  10. If selected TabItem does not yet have an associated ContentControl, the content manager will generate a new ContentControl, and assign it to the tab item's TabContent.InternalCachedContent property of the tab item.

  11. The ContentControl associated with curently selected TabItem will then become the Child of the border and will be displayed on screen.

Here's the resulting object graph:

Image 1

The code for the TabContent.ContentManager class looks as follows:

C#
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 
            { 
                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.

XAML
<TabControl ikriv:TabContent.IsCached="True">
    <ikriv:TabContent.Template>
        <DataTemplate>
            <!-- custom content template goes here -->
        </DataTemplate>
    </ikriv:TabContent.Template>
</TabControl>

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead Thomson Reuters
United States United States
Ivan is a hands-on software architect/technical lead working for Thomson Reuters in the New York City area. At present I am mostly building complex multi-threaded WPF application for the financial sector, but I am also interested in cloud computing, web development, mobile development, etc.

Please visit my web site: www.ikriv.com.

Comments and Discussions

 
AnswerRe: Finding a control with a template Pin
Jason Curl17-Aug-13 7:20
professionalJason Curl17-Aug-13 7:20 
GeneralRe: Finding a control with a template Pin
Ivan Krivyakov17-Aug-13 7:25
Ivan Krivyakov17-Aug-13 7:25 
QuestionCannot find source for binding error Pin
Jason Curl7-Aug-13 10:07
professionalJason Curl7-Aug-13 10:07 
AnswerRe: Cannot find source for binding error Pin
Ivan Krivyakov7-Aug-13 17:59
Ivan Krivyakov7-Aug-13 17:59 
GeneralMy vote of 5 Pin
Jason Curl7-Aug-13 10:05
professionalJason Curl7-Aug-13 10:05 
GeneralMy vote of 5 Pin
pinestreet12-May-13 9:20
pinestreet12-May-13 9:20 
QuestionHow to get rid of Unloaded event on Tab change? Pin
Christian Sander17-Apr-13 22:23
Christian Sander17-Apr-13 22:23 
AnswerRe: How to get rid of Unloaded event on Tab change? Pin
Ivan Krivyakov18-Apr-13 5:30
Ivan Krivyakov18-Apr-13 5:30 
This behavior is by design. Unloaded event is raised when control is removed from visual tree. A control may be later added to the visual tree and then removed again. Thus, it may legitimately receive multiple Unloaded events during its lifetime.

In "classic" tab control the old tab may either get reused (if new tab has the same data template), or thrown away. If the old tab is reused, it remains visible, only its DataContext is changed. If the old tab is not reused, it will be unloaded and thrown away.

In my version the old tab is unloaded, but not thrown away. When it becomes selected again, old controls will be reloaded and visible, keeping state.

Unfortunately, WPF does not provide nice and easy way to figure out when a control is removed forever (nor it can really know about it). To properly dispose of your WinForm controls you can either try to intercept when the tabs are closed (e.g. make your data source an observable collection and watch for Deleted events), and/or tie it to unloading of the tab control itself, if you know it won't be reloaded again.
QuestionGreat thanks Pin
ArsenMkrt11-Apr-13 2:22
ArsenMkrt11-Apr-13 2:22 
GeneralMy vote of 5 Pin
Eugene Sadovoi27-Feb-13 7:33
Eugene Sadovoi27-Feb-13 7:33 
GeneralRe: My vote of 5 Pin
Ivan Krivyakov28-Feb-13 15:41
Ivan Krivyakov28-Feb-13 15:41 
QuestionDatagrid Recreates Columns Pin
cahhunter18-Jan-13 6:34
cahhunter18-Jan-13 6:34 
AnswerRe: Datagrid Recreates Columns Pin
Ivan Krivyakov28-Feb-13 15:41
Ivan Krivyakov28-Feb-13 15:41 
GeneralRe: Datagrid Recreates Columns Pin
cahhunter5-Mar-13 7:59
cahhunter5-Mar-13 7:59 
GeneralThank you Pin
shameenkp4-Dec-12 20:41
shameenkp4-Dec-12 20:41 
GeneralMy vote of 5 Pin
Jf Beaulac23-Nov-12 3:34
Jf Beaulac23-Nov-12 3:34 
GeneralRe: My vote of 5 Pin
Ivan Krivyakov2-Dec-12 17:57
Ivan Krivyakov2-Dec-12 17:57 
QuestionHow about ListViews? Pin
Member 948647129-Oct-12 2:33
Member 948647129-Oct-12 2:33 
AnswerRe: How about ListViews? Pin
Ivan Krivyakov26-Nov-12 10:32
Ivan Krivyakov26-Nov-12 10:32 
Bug[Fixed] Bug when bound to ObservableCollection Pin
Simon Brydon1-Oct-12 17:46
Simon Brydon1-Oct-12 17:46 
GeneralRe: Bug when bound to ObservableCollection Pin
Ivan Krivyakov1-Oct-12 17:56
Ivan Krivyakov1-Oct-12 17:56 
GeneralRe: Bug when bound to ObservableCollection Pin
Ivan Krivyakov2-Oct-12 9:29
Ivan Krivyakov2-Oct-12 9:29 
GeneralMessage Closed Pin
2-Oct-12 9:52
Simon Brydon2-Oct-12 9:52 
GeneralRe: Bug when bound to ObservableCollection Pin
Ivan Krivyakov3-Oct-12 4:07
Ivan Krivyakov3-Oct-12 4:07 
GeneralRe: Bug when bound to ObservableCollection Pin
Simon Brydon3-Oct-12 20:03
Simon Brydon3-Oct-12 20:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.