65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (8投票s)

2014年12月12日

CPOL

3分钟阅读

viewsIcon

18036

downloadIcon

254

理解 Tabcontrol 右侧可以实现轻量级解决方案。

CachedContentPresenterDemo.zip

问题

数据绑定的TabControl的Tab页根据TabControl.ContentTemplate属性中定义的Datatemplate呈现TabItem的详细视图。

此详细视图可能是“可定制的”,例如通过Splitter控件或可调整大小的数据网格列等。

那么Tab页的行为可能出乎意料
假设一个Tab页上有一个Gridsplitter,你将其移到右侧。现在,当您切换到另一个Tab页时,该Tab页的Gridsplitter也会出现在右侧!

普遍的误解

这个问题很常见,并且有多种解决方法,例如,请参阅Article2011Article2012-04Article2012-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允许“定制”视图的视觉状态。

© . All rights reserved.