WPF-TabControl,保存其视觉TabItem状态(替代方案)






4.74/5 (8投票s)
理解 Tabcontrol 右侧可以实现轻量级解决方案。
CachedContentPresenterDemo.zip
问题
数据绑定的TabControl的Tab页根据TabControl.ContentTemplate
属性中定义的Datatemplate呈现TabItem的详细视图。
此详细视图可能是“可定制的”,例如通过Splitter控件或可调整大小的数据网格列等。
那么Tab页的行为可能出乎意料
假设一个Tab页上有一个Gridsplitter,你将其移到右侧。现在,当您切换到另一个Tab页时,该Tab页的Gridsplitter也会出现在右侧!
普遍的误解
这个问题很常见,并且有多种解决方法,例如,请参阅Article2011,Article2012-04,Article2012-12。
这些文章提到了(已弃用的)“TabItems的虚拟化”。但那不是实际发生的情况!
TabControl的TabItems没有进行虚拟化,而是根据TabControl的ItemTemplate
属性以页眉的形式呈现。
惊喜:在数据绑定的TabControl中根本没有Tab页! - TabControl的工作方式更智能
内部只有一个Contentpresenter
用于Tab页区域。TabControl只创建一次该Contentpresenter的ContentTemplate
(它创建VisualTree)!
然后,一切照旧:在选择Tab页眉时,ContentPresenter的DataContext会发生变化,并且绑定会发挥作用来呈现更改后的数据。
但是ContentPresenter本身以及其中的VisualTree不会改变 - 它保持不变。
这就是数据绑定的TabControl的奇迹(和问题根源),它与虚拟化无关。
这与将数据绑定的Listbox
与多个单独的数据控件结合起来构成“选择器详细信息视图”的情况相同。
并且,与所有选择器详细信息视图一样,详细信息控件一直保持不变,无论其DataContext更改多少次。
这就是为什么“Tab页”不保存其视觉状态的原因 - 因为数据绑定的详细信息视图在更改DataContext时从不存储VisualTrees。
解决方案
上面最后一句话告诉您该怎么做:创建一个ContentPresenter,它在更改DataContext时存储VisualTrees:
[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);
}
}
完成 :) - VB版本也一样
<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
这很简单,对吧?
通过一个附加的Datatemplate
属性扩展一个Decorator
,并处理其DataContext_Changed
事件。请参见UpdatePresentation(item)
:存储/恢复一个完全展开的ContentPresenter
(带有VisualTree)。
将此ContentPresenter
设置为Decorator.Child
,以便将其呈现给用户。
用法
在TabControl.ContentTemplate
中放置一个CachedContentPresenter
,并将“真正需要的”DataTemplate嵌套在其中。
<TabControl.ContentTemplate>
<DataTemplate>
<my:CachedContentPresenter >
<DataTemplate>
<my:uclPerson/>
</DataTemplate>
</my:CachedContentPresenter>
</DataTemplate>
</TabControl.ContentTemplate>
一个特殊功能是:您也可以在其他选择器详细信息视图中使用CachedContentPresenter
,例如将列表框与详细信息视图结合。
<ListBox ItemsSource="{Binding Persons}"
IsSynchronizedWithCurrentItem="True"
DisplayMemberPath="Name"/>
<my:CachedContentPresenter DataContext="{Binding Persons/}" >
<DataTemplate>
<my:uclPerson/>
</DataTemplate>
</my:CachedContentPresenter>
关注点
唯一值得关注的是精美的缓存,它为每个DataContext关联其自己的ContentPresenter。
ConditionalWeakTable(Of Object, ContentPresenter)
是一种字典,但它不会阻止其元素的垃圾回收。
相反,当条目键项被垃圾回收时,条目会自行消失。因此,条目值实际上表现得就像是键项的附加属性——它是一个“附加”属性。
实际上,ConditionalWeakTable
是依赖属性(DependancyProperties)背后魔力的一部分。
注意:与为解决“TabItem-Virtualisation”问题而发布的任何解决方案一样,此方法不适用于大量的ViewModel项列表,因为它为每个项存储了完全展开的VisualTree。但对于TabControl来说,这无论如何都不会发生,因为TabControl不适用于大量的ViewModel项列表。
演示应用程序
演示(VS-2010)包含上述代码(C#和VB版本)。ViewModel是一个Person
对象列表,每个Person有3个属性。作为详细视图,我创建了uclPerson
类 - 一个相当愚蠢的用户控件,但它的Grid-Splitter允许“定制”视图的视觉状态。