WPF TabControl:关闭选项卡虚拟化






4.92/5 (37投票s)
这是“在 WPF TabControl 中持久化选项卡切换时的视觉树(已优化)”的替代方案。
背景
WPF TabControl 以“虚拟化”其选项卡而闻名,当它们通过数据绑定创建时。只有可见的选项卡实际存在并绑定到选定的数据项。当选择更改时,现有控件会被重用并绑定到新的数据上下文。程序必须完全依赖数据绑定来重绘选项卡:如果选项卡上的任何控件未进行数据绑定,其状态将不受选择更改的影响。
虽然众所周知,但我认为此行为并未在 MSDN 中正式记录。如果存在此类文档,并且有人能给我一个链接,我将不胜感激。
现有技术
此行为自 2007 年以来引起了疑问。提出了许多方法来规避它(示例 1、示例 2、示例 3、示例 4),大多数都围绕为每个选项卡创建一个唯一的 ContentControl
,并以某种方式将正确的控件植入选定的 TabItem
。
不幸的是,到目前为止我发现的所有方法都存在以下一个或多个缺点:
- 子类化选项卡控件,例如创建
class TabControlEx: TabControl
。 - “劫持”
ItemsSource
和/或SelectedItem
属性,它们不再能像平常一样使用。 - 要求用户应用冗长的 XAML 样式。
- 要求用户应用一个以上的附加属性来关闭虚拟化行为
子类化可能看起来不是什么大问题,但如果出于任何原因您需要使用 TabControl
的现有子类,例如您无法修改的 MyCompanyTabControl
,那么它就会带来麻烦。
设计目标
对现有解决方案不满意,我试图创建一种方法:
- 将虚拟化关闭,只需一个简单的附加属性,例如
TabContent.IsCached="True"
。 - 不会改变
ItemsSource
或SelectedItem
的含义。 - 不会要求创建
TabControl
的子类。 - 允许使用自定义内容模板。
- 无需在 XAML 或代码隐藏中添加冗余代码片段。
设计概述
我解决方案背后的主要思想是“劫持”ContentTemplate
属性而不是 ItemsSource
。我允许选项卡控件正常创建模板化项,但我提供了一个特殊的 ContentTemplate
,它包含一个单一的 Border
控件。此 Border
将被创建一次,并一直保留在屏幕上,无论选择哪个项。
与其他方法一样,我们为每个选项卡创建一个唯一的 ContentControl
。当选项卡选择更改时,我们访问 Border
并将其 Child
更改为当前选定选项卡对应的内容控件。
此方法与以前解决方案的区别在于,我们不尝试用我们自己的控件替换自动生成的 TabItem
。相反,我们允许常规数据模板化过程正常进行,然后操作从内容模板创建的 Border
。
此方法的缺点是 TabControl.ContentTemplate
属性被“劫持”了,不能正常使用。为缓解此问题,我们:
- 提供一个替代属性:
TabContent.Template
。 - 仔细检查
TabControl.ContentTemplate
属性的“非法”使用,并在检测到时抛出描述性异常,告知程序员如何正确操作。这使得用户可以及早发现问题并快速修复。
设计细节
所有与选项卡控件虚拟化相关的附加属性都位于 TabContent
类中。TabContent.IsCached
属性充当“引导程序”,激活整个选项卡内容管理系统。假设我们有以下 XAML:
<TabControl ikriv:TabContent.IsCached="True" />
这会触发以下事件链:
XAML 解析器创建一个新的
TabControl
对象。Tab 控件的附加属性
TabContent.IsCached
被设置为True
。属性更改处理程序
TabContent.OnIsCachedChanged()
在代码中创建一个数据模板 并将其分配给TabControl.ContentTemplate
。WPF 模板化系统从模板创建一个
Border
元素。WPF 绑定系统查找类型为
TabControl
的 Border 的祖先,并将其分配给TabContent.InternalTabControl
附加属性。TabContent.InternalTabControl
的属性更改处理程序创建一个TabContent.ContentManager
类的新实例。ContentManager
对象引用选项卡控件和 Border 元素,并监听选项卡控件的SelectionChanged
事件。当选择更改时,内容管理器会检查选定的
TabItem
。如果选定的
TabItem
尚未关联ContentControl
,则内容管理器将生成一个新的ContentControl
,并将其分配给选项卡的TabContent.InternalCachedContent
属性。当前选定
TabItem
关联的ContentControl
将成为 border 的Child
,并在屏幕上显示。
<DataTemplate>
<Border ikriv:TabContent.InternalTabControl=
"{Binding RelativeSource={RelativeSource AncestorType=TabControl}}" />
</DataTemplate>
以下是生成的对象图:
TabContent.ContentManager
类的代码如下:
public class ContentManager
{
TabControl _tabControl;
Decorator _border;
public ContentManager(TabControl tabControl, Decorator border)
{
_tabControl = tabControl;
_border = border;
_tabControl.SelectionChanged += (sender, args) => { UpdateSelectedTab(); };
}
public void UpdateSelectedTab()
{
_border.Child = GetCurrentContent();
}
private ContentControl GetCurrentContent()
{
var item = _tabControl.SelectedItem;
if (item == null) return null;
var tabItem = _tabControl.ItemContainerGenerator.ContainerFromItem(item);
if (tabItem == null) return null;
var cachedContent = TabContent.GetInternalCachedContent(tabItem);
if (cachedContent == null)
{
cachedContent = new ContentControl
{
ContentTemplate = TabContent.GetTemplate(_tabControl),
ContentTemplateSelector = TabContent.GetTemplateSelector(_tabControl)
};
cachedContent.SetBinding(ContentControl.ContentProperty, new Binding());
TabContent.SetInternalCachedContent(tabItem, cachedContent);
}
return cachedContent;
}
}
自定义内容模板
选项卡控件的用户仍然可以定义自定义内容模板,但他们必须使用 TabContent.Template
附加属性而不是常规的 ContentTemplate
属性。尝试同时使用 ContentTemplate
和 TabContent.IsCached
将导致异常。
<TabControl ikriv:TabContent.IsCached="True">
<ikriv:TabContent.Template>
<DataTemplate>
<!-- custom content template goes here -->
</DataTemplate>
</ikriv:TabContent.Template>
</TabControl>
优点和缺点
此设计的优点是,大部分复杂性都隐藏在一个属性后面。可以通过对 XAML 进行一个添加,并在使用 ContentTemplate
属性的情况下进行一个修改,就可以在任何现有选项卡控件上关闭虚拟化。
此设计的主要缺点是它在 Silverlight 上不起作用,因为 Silverlight 的 TabControl
版本没有 ContentTemplate
属性。
更新于 2012 年 10 月 3 日
版本 1.1 修复了在当前选定项被删除时发生的崩溃。感谢 Simon Brydon 发现此问题。
更新于 2012 年 11 月 23 日
版本 1.2 修复了一个 bug:当选项卡变得不可见时,选项卡内容的 DataContext
被设置为 null
。如果选项卡内容监控 DataContext
更改或即使在隐藏状态下对数据上下文执行某些操作,这一点就很重要。此修复是在 TabContent.cs
的末尾添加一行 DataContext = item
。感谢 Jean-François Beaulac 发现了这个 bug。