Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / WPF
Article

Persist the Visual Tree when switching tabs in the WPF TabControl

Rate me:
Please Sign up or sign in to vote.
4.76/5 (14 votes)
16 Jun 2011CPOL5 min read 84.3K   3.1K   31   23
Using attached behavior pattern to create a tab persist TabControl in WPF when ViewModel is used.

Sample Image

Introduction

There are two different behaviors in the WPF TabControl. When you are binding the tab items from the ViewModel to the ItemsSource property, the TabControl will create a new visual tree every time you switch between tabs. However, when you are adding a TabItem to the Items collection directly, the visual tree will be persisted when the tab is not active. While the databinding behavior is expected, this does create some issues:

  • Slow rendering. You can obviously see the delay when you have a heavy tab.
  • Visual change not persisted. If you change a column width in the tab, you will lose it after the visual tree is recreated.

In this article, I assume you have a basic understand about MVVM and attached properties. There is lots of material you can find on the Internet about these topics. To understand what attached behaviors is, check out Josh Smith's article.

Workaround

Don't use ItemsSource. Instead, have a wrapper convert your source to a TabItem and add it to the TabControl. In this case, you will still have all the cool things from data binding, while keeping the desired behavior.

The idea is simple, and there are lots of ways to implement this. You can extend the TabControl class, or just do it in code-behind. The way I have done is using the attached behaviour pattern. In this way, we can keep the code out from XAML and code-behind, which means it can be reused. Also, doing this as an attached pattern would be more flexible than overriding the TabControl class.

The Demo

The idea of this demo is to show the differences between using PersistTabBehavior and the original ItemsSource.

This demo contains a WPF window with two TabControls. Each TabControl has two tabs, and each tab has a big DataGrid for the purpose of slowing down the rendering. The top control uses attached properties explained in this article. The bottom control is just a regular TabControl. You can see that the performance of the top TabControl is better when switching between tabs.

Here is the differences between the two controls in XAML.

The Regular TabControl - Bottom
XML
<TabControl 
    Grid.Row="2"
    DataContext="{Binding Tab1}"
    ItemsSource="{Binding Pages}"
    SelectedItem="{Binding SelectedPage}"
    ItemTemplate="{StaticResource templateForTheHeader}" />
The TabControl with PersistTabBehavior - Top
XML
<TabControl 
    Grid.Row="1"
    DataContext="{Binding Tab2}"
    b:PersistTabBehavior.ItemsSource="{Binding Pages}"
    b:PersistTabBehavior.SelectedItem="{Binding SelectedPage}"
    ItemContainerStyle="{StaticResource PersistTabItemStyle}" />

As you can see, we have replaced ItemsSource and SelectedItem with the new attached properties: PersistTabBehavior.ItemsSource and PersistTabBehavior.SelectedItem. This is the beauty of attached behaviors. It is not necessary to create an extended TabControl class. We have attached our customized behavior to the original TabControl, and all the implementation is done in a separate class.

Part 1 - PersistTabBehavior.ItemsSource

Since the nature of attached properties is static, it gets complicated to manage all the events and objects when you have more than one TabControl, and that TabControl may be removed from the windows before the application exits. Therefore, I have created a separate class, PersistTabItemsSourceHandler, to handle that.

All the PersistTabItemsSourceHandler instances will be saved to the ItemSourceHandlers dictionary. And it will be disposed after the TabControl is unloaded from the UI.

C#
private static readonly Dictionary<TabControl, PersistTabItemsSourceHandler> 
    ItemSourceHandlers = new Dictionary<TabControl, PersistTabItemsSourceHandler>();

PersistTabItemsSourceHandler

A new instance of PersistTabItemsSourceHandler will be created for each TabControl. This object is responsible for the following two tasks:

  • Add all TabItems when the TabControl is loaded on the screen.
  • Add or remove tab when the collection has changed.

Add all TabItems when the Tabcontrol is loaded

The tab loading logic will be handled by the PersistTabItemsSourceHandler object.

C#
private void Load(IEnumerable sourceItems)
{
    Tab.Items.Clear();

    foreach (var page in sourceItems)
    AddTabItem(page);

    // If there is selected item,
    // select it after setting the initial tabitem collection
    SelectItem();
}

private void AddTabItem(object view)
{
    var contentControl = new ContentControl();
    contentControl.SetBinding(ContentControl.ContentProperty, new Binding());
    var item = new TabItem { DataContext = view, Content = contentControl };

    Tab.Items.Add(item);

    // When there is only 1 Item, the tab can't be rendered without have it selected
    // Don't do Refresh(). This may clear
    // the Selected item, causing issue in the ViewModel
    if (Tab.SelectedItem == null)
        item.IsSelected = true;
}

Adding and removing tab while changing the collection

If your enumerable object has implemented the INotifyPropertyChanged interface, PersistTabItemsSourceHandler will listen to the CollectionChanged event. It will keep the enumerable object in sync with the tab items in the TabControl.

In the demo, you can see how this works by clicking the Add Page and Remove Page buttons on top of the window.

C#
private void AttachCollectionChangedEvent()
{
    var source = Tab.GetValue(PersistTabBehavior.ItemsSourceProperty) 
                              as INotifyCollectionChanged;

    // This property is not necessary to implement INotifyCollectionChanged.
    // Everything else will still work. We just can't add or remove tab.
    if (source == null)
        return;

    source.CollectionChanged += SourceCollectionChanged;
}

private void SourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    switch (e.Action)
    {
        case NotifyCollectionChangedAction.Add:
            foreach (var view in e.NewItems)
                AddTabItem(view);
            break;
        case NotifyCollectionChangedAction.Remove:
            foreach (var view in e.OldItems)
                RemoveTabItem(view);
            break;
    }
}

private void AddTabItem(object view)
{
    var contentControl = new ContentControl();
    contentControl.SetBinding(ContentControl.ContentProperty, new Binding());
    var item = new TabItem { DataContext = view, Content = contentControl };

    Tab.Items.Add(item);

    // When there is only 1 Item, the tab can't be rendered without have it selected
    // Don't do Refresh(). This may clear
    // the Selected item, causing issue in the ViewModel
    if (Tab.SelectedItem == null)
        item.IsSelected = true;
}

private void RemoveTabItem(object view)
{
    var foundItem = Tab.Items.Cast<tabitem />().FirstOrDefault(t => t.DataContext == view);

    if (foundItem != null)
        Tab.Items.Remove(foundItem);
}

The header

In a regular TabControl, you can simply override the ItemTemplate as follow, where Header is your string property that contains the name of the tab header.

XML
<DataTemplate x:Key="templateForTheHeader" DataType="{x:Type vm:TabPageViewModel}">
    <TextBlock Text="{Binding Header}" />
</DataTemplate>

Unfortunately, you can't do this in PersistTabBehavior because you are not binding your ViewModel to the tab. You are now adding a real TabItem to the tab.

One of the solutions is to override the default template of the TabItem, and you can do the binding there. In this demo, I am using the default template from MSDN. In the ContentPresenter, I have assigned the Content to the Header property.

XML
<ContentPresenter x:Name="ContentSite"
    VerticalAlignment="Center"
    HorizontalAlignment="Center"
    Content="{Binding Header}"
    Margin="12,2,12,2"
    RecognizesAccessKey="True"/>

There is quite a lot of style code you need to copy, but this is the best way I can think of.

Disposing the object

In order for the TabControl to be garbage collected after it is released from the UI, we have to make sure we clear all the references. Unlike Windows Forms controls, WPF controls have no Disposed event (because there is nothing to dispose). What we have to do is to listen for the Unloaded event. When the control is gone from the UI, this event will be triggered. At this point, we can dump our PersistTabItemsSourceHandler object.

C#
private static void RemoveFromItemSourceHandlers(TabControl tabControl)
{
    if (!ItemSourceHandlers.ContainsKey(tabControl))
        return;

    ItemSourceHandlers[tabControl].Dispose();
    ItemSourceHandlers.Remove(tabControl);
}

public void Dispose()
{
    var source = Tab.GetValue(PersistTabBehavior.ItemsSourceProperty) 
                              as INotifyCollectionChanged;

    if (source != null)
        source.CollectionChanged -= SourceCollectionChanged;

    Tab = null;
}

Part 2 - PersistTabSelectedItemHandler

After I had completed PersistTabItemsSourceHandler, I thought I was done. However, I had missed one important point - the selected tab. Since we are adding a real TabItem to the TabControl, it won't work if you just bind your ViewModel to the SelectedItem property. The SelectedItem property will just give you the selected TabItem, but not the ViewModel sitting in the TabItem's DataContext.

In the demo, I have a SelectedPage property in the TabCotnrolViewModel. This is used to keep track of which tab is currently active. This item is also bound to the top left of the window. You can see the text changing when switching tabs.

C#
public TabPageViewModel SelectedPage
{
    get { return selectedPage; }
    set
    {
        selectedPage = value;

        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs("SelectedPage"));
    }
}

TwoWay Binding

PersistTabSelectedItemHandler and PersistTabItemsSourceHandler are very similar. The main difference is that PersistTabSelectedItemHandler supports TwoWay binding. Which means that when the user selects a tab, the SelectedPage property will be updated. The other way around, when the SelectedTab property changes, the TabControl will activate the corresponding tab.

C#
public void ChangeSelectionFromProperty()
{
    var selectedObject = Tab.GetValue(PersistTabBehavior.SelectedItemProperty);

    if (selectedObject == null)
    {
        Tab.SelectedItem = null;
        return;
    }

    foreach (TabItem tabItem in Tab.Items)
    {
        if (tabItem.DataContext == selectedObject)
        {
            if (!tabItem.IsSelected)
                tabItem.IsSelected = true;

            break;
        }
    }
}

private void ChangeSelectionFromUi(object sender, SelectionChangedEventArgs e)
{
    if (e.AddedItems.Count >= 1)
    {
        var selectedObject = e.AddedItems[0];

        var selectedItem = selectedObject as TabItem;

        if (selectedItem != null)
            SelectedItemProperty(selectedItem);
    }
}

private void SelectedItemProperty(TabItem selectedTabItem)
{
    var tabObjects = Tab.GetValue(PersistTabBehavior.ItemsSourceProperty) as IEnumerable;

    if (tabObjects == null)
        return;

    foreach (var tabObject in tabObjects)
    {
        if (tabObject == selectedTabItem.DataContext)
        {
            PersistTabBehavior.SetSelectedItem(Tab, tabObject);
            return;
        }
    }
}

Please notice that we have set FrameworkPropertyMetadata to the SelectedItemProperty. This will turn on TwoWay binding by default.

C#
public static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.RegisterAttached(
            "SelectedItem", typeof(object), typeof(PersistTabBehavior),
            new FrameworkPropertyMetadata(
                null,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                OnSelectedItemPropertyChanged));

License

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


Written By
Software Developer Fidelity
Hong Kong Hong Kong
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionWhat about DataTemplates? Pin
EGD Eric18-Feb-14 10:21
EGD Eric18-Feb-14 10:21 
QuestionMmmm Pin
Sacha Barber14-Nov-13 23:56
Sacha Barber14-Nov-13 23:56 
AnswerRe: Mmmm Pin
EGD Eric6-Feb-14 9:17
EGD Eric6-Feb-14 9:17 
AnswerRe: Mmmm Pin
igurjeva13-Mar-14 2:38
igurjeva13-Mar-14 2:38 
AnswerRe: Mmmm Pin
Member 1135512610-Feb-15 0:56
Member 1135512610-Feb-15 0:56 
AnswerRe: Mmmm Pin
Michael Janulaitis17-Mar-22 5:24
Michael Janulaitis17-Mar-22 5:24 
SuggestionItems being loaded twice Pin
rogermk9-May-13 9:23
rogermk9-May-13 9:23 
GeneralMy vote of 5 Pin
rdkleine25-Feb-13 20:19
rdkleine25-Feb-13 20:19 
SuggestionSolution to being unable to use ItemTemplate Pin
RobinHSanner13-Dec-12 5:26
RobinHSanner13-Dec-12 5:26 
GeneralMy vote of 5 Pin
Ionegative17-Oct-12 3:20
Ionegative17-Oct-12 3:20 
SuggestionHeader template Pin
frank_m5-Apr-12 1:54
frank_m5-Apr-12 1:54 
QuestionGreate solution! Pin
desevg3-Apr-12 19:06
desevg3-Apr-12 19:06 
QuestionBug with DataTemplateSelector Pin
Hakan Altinbasak25-Mar-12 23:55
Hakan Altinbasak25-Mar-12 23:55 
AnswerRe: Bug with DataTemplateSelector Pin
Hakan Altinbasak1-Apr-12 23:29
Hakan Altinbasak1-Apr-12 23:29 
QuestionHelp Regarding Tab Content Pin
gauravg4uin23-Mar-12 1:34
gauravg4uin23-Mar-12 1:34 
AnswerRe: Help Regarding Tab Content Pin
JasonChing23-Mar-12 7:01
JasonChing23-Mar-12 7:01 
Hi Gaurva,

This is more about a question of how DataTemplate works. You can find tons of articles about this topic on the Internet.

You can bind different type of ViewModel to different tab pages. There is no restriction to that. As long as you have defined the DataTemplate for all ViewModels that you have, WPF will know how to display them by selecting different views.

I suggest you to create a base class called TabPageViewModel. On top of that, you will have other sub-classes like GridPageViewModel and ChartPageViewModel. They both are inherited from TabPageViewModel.

You are going to bind the collection of TagPageViewModel to the TabControl. By defining individual DataTemplate in the xaml for each sub classes, you should be able to achieve what you want to do.

For you reference: http://msdn.microsoft.com/en-us/magazine/dd419663.aspx[^]

Regards,
Jason
QuestionItemTemplate is not applied Pin
Michael Bendtsen3-Jan-12 2:45
Michael Bendtsen3-Jan-12 2:45 
AnswerRe: ItemTemplate is not applied Pin
JasonChing5-Jan-12 5:17
JasonChing5-Jan-12 5:17 
GeneralMy vote of 5 Pin
Eugene Sadovoi19-Sep-11 7:55
Eugene Sadovoi19-Sep-11 7:55 
BugNullReference Error Pin
asiriw20024-Aug-11 19:34
asiriw20024-Aug-11 19:34 
GeneralRe: NullReference Error Pin
JasonChing10-Aug-11 4:19
JasonChing10-Aug-11 4:19 
GeneralBless you - excelent article Pin
gert_d_c21-Jun-11 23:24
gert_d_c21-Jun-11 23:24 
GeneralMy vote of 5 Pin
Filip D'haene17-Jun-11 2:38
Filip D'haene17-Jun-11 2:38 

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.