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

WPF TabControl:关闭选项卡虚拟化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (37投票s)

2012年9月17日

CPOL

5分钟阅读

viewsIcon

192644

downloadIcon

3913

这是“在 WPF TabControl 中持久化选项卡切换时的视觉树(已优化)”的替代方案。

背景

WPF TabControl 以“虚拟化”其选项卡而闻名,当它们通过数据绑定创建时。只有可见的选项卡实际存在并绑定到选定的数据项。当选择更改时,现有控件会被重用并绑定到新的数据上下文。程序必须完全依赖数据绑定来重绘选项卡:如果选项卡上的任何控件未进行数据绑定,其状态将不受选择更改的影响。

虽然众所周知,但我认为此行为并未在 MSDN 中正式记录。如果存在此类文档,并且有人能给我一个链接,我将不胜感激。

现有技术

此行为自 2007 年以来引起了疑问。提出了许多方法来规避它(示例 1示例 2示例 3示例 4),大多数都围绕为每个选项卡创建一个唯一的 ContentControl,并以某种方式将正确的控件植入选定的 TabItem

不幸的是,到目前为止我发现的所有方法都存在以下一个或多个缺点:

  • 子类化选项卡控件,例如创建 class TabControlEx: TabControl
  • “劫持”ItemsSource 和/或 SelectedItem 属性,它们不再能像平常一样使用。
  • 要求用户应用冗长的 XAML 样式。
  • 要求用户应用一个以上的附加属性来关闭虚拟化行为

子类化可能看起来不是什么大问题,但如果出于任何原因您需要使用 TabControl 的现有子类,例如您无法修改的 MyCompanyTabControl,那么它就会带来麻烦。

设计目标

对现有解决方案不满意,我试图创建一种方法:

  1. 将虚拟化关闭,只需一个简单的附加属性,例如 TabContent.IsCached="True"
  2. 不会改变 ItemsSourceSelectedItem 的含义。
  3. 不会要求创建 TabControl 的子类。
  4. 允许使用自定义内容模板。
  5. 无需在 XAML 或代码隐藏中添加冗余代码片段。

设计概述

我解决方案背后的主要思想是“劫持”ContentTemplate 属性而不是 ItemsSource。我允许选项卡控件正常创建模板化项,但我提供了一个特殊的 ContentTemplate,它包含一个单一的 Border 控件。此 Border 将被创建一次,并一直保留在屏幕上,无论选择哪个项。

与其他方法一样,我们为每个选项卡创建一个唯一的 ContentControl。当选项卡选择更改时,我们访问 Border 并将其 Child 更改为当前选定选项卡对应的内容控件。

此方法与以前解决方案的区别在于,我们不尝试用我们自己的控件替换自动生成的 TabItem。相反,我们允许常规数据模板化过程正常进行,然后操作从内容模板创建的 Border

此方法的缺点是 TabControl.ContentTemplate 属性被“劫持”了,不能正常使用。为缓解此问题,我们:

  1. 提供一个替代属性:TabContent.Template
  2. 仔细检查 TabControl.ContentTemplate 属性的“非法”使用,并在检测到时抛出描述性异常,告知程序员如何正确操作。这使得用户可以及早发现问题并快速修复。

设计细节

所有与选项卡控件虚拟化相关的附加属性都位于 TabContent 类中。TabContent.IsCached 属性充当“引导程序”,激活整个选项卡内容管理系统。假设我们有以下 XAML:

<TabControl ikriv:TabContent.IsCached="True" />

这会触发以下事件链:

  1. XAML 解析器创建一个新的 TabControl 对象。

  2. Tab 控件的附加属性 TabContent.IsCached 被设置为 True

  3. 属性更改处理程序 TabContent.OnIsCachedChanged() 在代码中创建一个数据模板 并将其分配给 TabControl.ContentTemplate

  4. <DataTemplate>
        <Border ikriv:TabContent.InternalTabControl=
                "{Binding RelativeSource={RelativeSource AncestorType=TabControl}}" />
    </DataTemplate>
  5. WPF 模板化系统从模板创建一个 Border 元素。

  6. WPF 绑定系统查找类型为 TabControl 的 Border 的祖先,并将其分配给 TabContent.InternalTabControl 附加属性。

  7. TabContent.InternalTabControl 的属性更改处理程序创建一个 TabContent.ContentManager 类的新实例。

  8. ContentManager 对象引用选项卡控件和 Border 元素,并监听选项卡控件的 SelectionChanged 事件。

  9. 当选择更改时,内容管理器会检查选定的 TabItem

  10. 如果选定的 TabItem 尚未关联 ContentControl,则内容管理器将生成一个新的 ContentControl,并将其分配给选项卡的 TabContent.InternalCachedContent 属性。

  11. 当前选定 TabItem 关联的 ContentControl 将成为 border 的 Child,并在屏幕上显示。

以下是生成的对象图:

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 属性。尝试同时使用 ContentTemplateTabContent.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。

© . All rights reserved.