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

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

Rate me:
Please Sign up or sign in to vote.
4.74/5 (8 votes)
12 Dec 2014CPOL3 min read 16.7K   193   3   1
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)


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

Comments and Discussions

 
GeneralMy vote of 5 Pin
Henrik Jonsson12-Oct-19 10:34
Henrik Jonsson12-Oct-19 10:34 
Thanks for sharing this simple solution to make it possible to create unique Visual instances for each data bound item in TabControl

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.