Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Wpf-Tabcontrol, saving its visual TabItem-States (Alternative)

4.74/5 (8 votes)
13 Dec 2014CPOL3 min read 17.6K   195  
Understanding Tabcontrol right enables a lightweigth solution

CachedContentPresenterDemo.zip

The Problem

The Tabpages of a databound Tabcontrol present a Detailview of the TabItem, according to the Datatemplate, defined in the TabControl.ContentTemplate-Property.

This Detailview may be "customizable", eg by Splitter-Controls or resizeable Datagrid-Columns or stuff.

Then the Tabpages behave in a maybe unexpected manner:
Assume a Gridsplitter on a Tab, and you move it to the right. Now when you change to another Tab - that ones Gridsplitter also will appear on the right!

The common Misunderstanding

The issue is well-known and solved in different ways, eg. refer to Article2011, Article2012-04, Article2012-12.

These articles talk about a (deprecated) "Virtualization of TabItems". But that is not, what happens!

Tabcontrols Tabitems are not virtualisized, but pretty presented (according to Tabcontrols ItemTemplate - Property): as Tabpage-Headers.
Surprise: On a databount Tabcontrol there Is no Tabpage at all! - Tabcontrol works kind of smarter:

Internally there is only, and only one(!) Contentpresenter on the Tabpage-Area. And TabControl sets that Contentpresenters ContentTemplate (that creates the VisualTree) - only once!
Then business as usual: ContentPresenters Datacontext changes on selecting Tab-Headers, and Bindings do their job to present changed Data.
But the ContentPresenter itself, and the VisualTree in it - does not change - it remains the same.
That's the miracle (and the issue-reason) of databound Tabcontrol, and it has nothing to do with Virtualization.

Its the same, as when you combine a databound Listbox with several Single-Data-Controls, to compose a "Selector-DetailView".
And as in every Selector-DetailView, the Detail-Controls remain the same - all the time, no matter how often their DataContext changes.

And thats why a "Tabpage" doesn't save its visual state - because databound Detailviews never store VisualTrees on changing Datacontexts.

Solution

The last sentence above tells what to do: Create a ContentPresenter, which stores VisualTrees on changing Datacontext:

C#
[ContentProperty("DataTemplate")]
public class CachedContentPresenter : Decorator {

   //ConditionalWeakTable is a special Dictionary, which doesn't prohibit garbage-collection of its keys. Instead it automatically removes garbaged Elements
   private ConditionalWeakTable<object, ContentPresenter> _PresenterCache = new ConditionalWeakTable<object, ContentPresenter>();

   public CachedContentPresenter() {
      DataContextChanged += (s, e) => UpdatePresentation(e.NewValue);
   }
   private void UpdatePresentation(object item) {
      ContentPresenter ctp = null;
      if (item != null) {
         if (!_PresenterCache.TryGetValue(item, out ctp)) {
            ctp = new ContentPresenter { ContentTemplate = DataTemplate };
            ctp.SetBinding(ContentPresenter.ContentProperty, new Binding());
            _PresenterCache.Add(item, ctp);
         }
      }
      this.Child = ctp;
   }

   public static readonly DependencyProperty DataTemplateProperty = DependencyProperty.Register("DataTemplate", typeof(DataTemplate), typeof(CachedContentPresenter), new FrameworkPropertyMetadata(DataTemplate_Changed));
   public DataTemplate DataTemplate {
      get { return (DataTemplate)this.GetValue(DataTemplateProperty); }
      set { SetValue(DataTemplateProperty, value); }
   }
   private static void DataTemplate_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e) {
      //clear cache before update Presentation
      var ccp = (CachedContentPresenter)sender;
      ccp._PresenterCache = new ConditionalWeakTable<object, ContentPresenter>();
      ccp.UpdatePresentation(ccp.DataContext);
   }

}

Done :) - same in VB:

VB.NET
<ContentProperty("DataTemplate")> _
Public Class CachedContentPresenter : Inherits Decorator

   'ConditionalWeakTable is a special Dictionary, which doesn't prohibit garbage-collection of its keys. Instead it automatically removes garbaged Elements
   Private _PresenterCache As New ConditionalWeakTable(Of Object, ContentPresenter)

   Private Sub DataContext_Changed(sender As Object, e As DependencyPropertyChangedEventArgs) Handles Me.DataContextChanged
      UpdatePresentation(e.NewValue)
   End Sub
   Private Sub UpdatePresentation(item As Object)
      Dim ctp As ContentPresenter = Nothing
      If item IsNot Nothing Then
         If Not _PresenterCache.TryGetValue(item, ctp) Then
            ctp = New ContentPresenter With {.ContentTemplate = DataTemplate}
            ctp.SetBinding(ContentPresenter.ContentProperty, New Binding())
            _PresenterCache.Add(item, ctp)
         End If
      End If
      Me.Child = ctp
   End Sub

   Public Shared ReadOnly DataTemplateProperty As DependencyProperty = DependencyProperty.Register("DataTemplate", GetType(DataTemplate), GetType(CachedContentPresenter), New FrameworkPropertyMetadata(AddressOf DataTemplate_Changed))
   Public Property DataTemplate As DataTemplate
      Get
         Return DirectCast(Me.GetValue(DataTemplateProperty), DataTemplate)
      End Get
      Set(value As DataTemplate)
         SetValue(DataTemplateProperty, value)
      End Set
   End Property
   Private Shared Sub DataTemplate_Changed(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
      'clear cache before update Presentation
      Dim ccp = DirectCast(sender, CachedContentPresenter)
      ccp._PresenterCache = New ConditionalWeakTable(Of Object, ContentPresenter)
      ccp.UpdatePresentation(ccp.DataContext)
   End Sub

End Class

No rocket-science, is it?
Extend a Decorator with an additional Datatemplate-Property and handle its DataContext_Changed-Event. See UpdatePresentation(item): Store/Restore a full  expanded ContentPresenter (with the VisualTree on it).
Set this ContentPresenter as Decorator.Child, to present it to the User.

Usage

Place a CachedContentPresenter in the TabControl.ContentTemplate, and nest the "real meant" DataTemplate inside it:

XML
<TabControl.ContentTemplate>
  <DataTemplate>
    <my:CachedContentPresenter >
      <DataTemplate>
        <my:uclPerson/>
      </DataTemplate>
    </my:CachedContentPresenter>
  </DataTemplate>
</TabControl.ContentTemplate>

A special-feature is: You can use CachedContentPresenter in other Selector-DetailViews as well, eg combine a Listbox with a DetailView:

XML
<ListBox ItemsSource="{Binding Persons}"
         IsSynchronizedWithCurrentItem="True"
         DisplayMemberPath="Name"/>
<my:CachedContentPresenter  DataContext="{Binding Persons/}" >
  <DataTemplate>
    <my:uclPerson/>
  </DataTemplate>
</my:CachedContentPresenter>

Points of Interest

The only Point of Interest is the fancy cache, which associates to each DataContext its own ContentPresenter:
ConditionalWeakTable(Of Object, ContentPresenter) is a kind of Dictionary, but it does not prevent garbage-collection of its Elements.
Instead an Entry vanishes automatically, when the entry-key-item is garbage-collected. So the Entry-Value behaves actually just like an additional Property of the key-item - it is an "attached" Property.
In fact, ConditionalWeakTable is a basic part of the magic behind DependancyProperties.

Note: Like every solution published to workaround the "TabItem-Virtualisation" this approach is not advisable on large lists of ViewModel-items, since it stores a full expanded VisualTree for each item. But on Tabcontrol this can't happen anyway, since Tabcontrol isn't applicable to large lists of ViewModel-items.

Demo-Application

The Demo (VS-2010) contains the code above (both versions c# and VB). Viewmodel is a List of Person-Objects, each Person with 3 Properties. As DetailView i created the uclPerson-class - a rather stupid UserControl, but its Grid-Splitter enables a little "customization" of the Views visual-state.

License

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