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:
[ContentProperty("DataTemplate")]
public class CachedContentPresenter : Decorator {
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) {
var ccp = (CachedContentPresenter)sender;
ccp._PresenterCache = new ConditionalWeakTable<object, ContentPresenter>();
ccp.UpdatePresentation(ccp.DataContext);
}
}
Done :) - same in VB:
<ContentProperty("DataTemplate")> _
Public Class CachedContentPresenter : Inherits Decorator
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)
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:
<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:
<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.