如何在 TabControl 中的选项卡项中保留控件状态

Posted

技术标签:

【中文标题】如何在 TabControl 中的选项卡项中保留控件状态【英文标题】:How to preserve control state within tab items in a TabControl 【发布时间】:2011-01-06 01:11:44 【问题描述】:

我是 WPF 的新手,尝试构建一个遵循 Josh Smith 描述 The Model-View-ViewModel Design Pattern 的优秀文章的建议的项目。

使用 Josh 的示例代码作为基础,我创建了一个简单的应用程序,其中包含许多“工作区”,每个工作区由 TabControl 中的一个选项卡表示。在我的应用程序中,工作区是一个文档编辑器,它允许通过 TreeView 控件操作分层文档。

虽然我已成功打开多个工作区并在绑定的 TreeView 控件中查看其文档内容,但我发现 TreeView 在选项卡之间切换时会“忘记”其状态。例如,如果Tab1中的TreeView部分展开,切换到Tab2返回Tab1后会显示为完全折叠。此行为似乎适用于所有控件的控件状态的所有方面。

经过一些实验,我意识到我可以通过将每个控件状态属性显式绑定到底层 ViewModel 上的专用属性来保留 TabItem 中的状态。但是,这似乎需要做很多额外的工作,因为我只想让所有控件在工作区之间切换时记住它们的状态。

我假设我遗漏了一些简单的东西,但我不确定在哪里寻找答案。任何指导将不胜感激。

谢谢, 蒂姆

更新:

根据要求,我将尝试发布一些演示此问题的代码。但是,由于 TreeView 基础的数据很复杂,我将发布一个简化的示例,展示相同的症状。这是主窗口中的 XAML:

<TabControl IsSynchronizedWithCurrentItem="True" ItemsSource="Binding Path=Docs">
    <TabControl.ItemTemplate>
        <DataTemplate>
            <ContentPresenter Content="Binding Path=Name" />
        </DataTemplate>
    </TabControl.ItemTemplate>

    <TabControl.ContentTemplate>
        <DataTemplate>
            <view:DocumentView />
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>

上述 XAML 正确绑定到 DocumentViewModel 的 ObservableCollection,每个成员都通过 DocumentView 呈现。

为简单起见,我从 DocumentView 中删除了 TreeView(上面提到过),并将其替换为包含 3 个固定选项卡的 TabControl:

<TabControl>
    <TabItem Header="A" />
    <TabItem Header="B" />
    <TabItem Header="C" />
</TabControl>

在这种情况下,DocumentView 和 DocumentViewModel 之间没有绑定。代码运行时,切换外层TabControl时,内层TabControl无法记住自己的选择。

但是,如果我显式绑定内部 TabControl 的 SelectedIndex 属性...

<TabControl SelectedIndex="Binding Path=SelectedDocumentIndex">
    <TabItem Header="A" />
    <TabItem Header="B" />
    <TabItem Header="C" />
</TabControl>

...到 DocumentViewModel 上的相应虚拟属性...

public int SelecteDocumentIndex  get; set; 

...内部选项卡能够记住它的选择。

我知道我可以通过将这种技术应用于每个控件的每个视觉属性来有效地解决我的问题,但我希望有一个更优雅的解决方案。

【问题讨论】:

默认情况下,WPF 中的控件“记住”它们的状态,选项卡项中的控件“忘记”它们的状态这一事实是您的某些显式操作的结果。显示选项卡项的 XAML,以及它们包含的绑定的相关视图模型代码。 我同意 Aviad 的观点,如果没有看到您的代码,很难判断出了什么问题。对于 WPF 中 TreeView 控件的一些精彩文章,我建议看看 Bea Stollnitz 的博客...bea.stollnitz.com/blog/index.php?s=treeview 我收回我的评论,这是一个真正令人讨厌的问题,我只是从各个角度尝试过,如果选项卡控件使用 ItemsSourceDataTemplate数据模板中控件的状态在选项卡项之间共享 (!!!) 我遇到的问题不仅仅是树视图...如果您没有支持控件状态的双向 ViewModel,它将丢失它。这样做的原因是,当您切换选项卡时,控件不再是可视化树的一部分。 Unload 事件实际上是为这些控件触发的,并且在您切换回来之前,它们不再具有任何视觉表示。这是选项卡控件实现的一种虚拟化形式,可在您有很多选项卡时帮助节省内存。很想知道您是否为此提出了解决方案……我刚刚创建了虚拟机。 @Killercam:IIRC,2010 年 1 月存在的 WAF 样本对指导我找到可行的解决方案非常有帮助,但随后发生了很大的变化,以至于它试图做很多事情,并且关于这一点已经不是很清楚了。我最近重新访问它以刷新我的记忆(近 3 年没有使用 WPF)并且很失望地发现我无法重现我以前取得的成就。最后,我只是在视图模型上创建了额外的视觉状态属性。抱歉,我无法提出更有帮助的建议。 【参考方案1】:

我通过http://www.codeproject.com/Articles/460989/WPF-TabControl-Turning-Off-Tab-Virtualizationhttp://www.codeproject.com/Articles/460989/WPF-TabControl-Turning-Off-Tab-Virtualization 的提示 WPF TabControl: Turning Off Tab Virtualization 解决了这个问题,这是一个具有 IsCached 属性的 TabContent 类。

【讨论】:

【参考方案2】:

我遇到了同样的问题,发现了一个nice solution,就我测试而言,你可以像普通的TabControl 一样使用它。如果这里对你很重要,Current License

这里是链接断开时的代码:

using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace CefSharp.Wpf.Example.Controls

    /// <summary>
    /// Extended TabControl which saves the displayed item so you don't get the performance hit of
    /// unloading and reloading the VisualTree when switching tabs
    /// </summary>
    /// <remarks>
    /// Based on example from http://***.com/a/9802346, which in turn is based on
    /// http://www.pluralsight-training.net/community/blogs/eburke/archive/2009/04/30/keeping-the-wpf-tab-control-from-destroying-its-children.aspx
    /// with some modifications so it reuses a TabItem's ContentPresenter when doing drag/drop operations
    /// </remarks>
    [TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
    public class NonReloadingTabControl : TabControl
    
        private Panel itemsHolderPanel;

        public NonReloadingTabControl()
        
            // This is necessary so that we get the initial databound selected item
            ItemContainerGenerator.StatusChanged += ItemContainerGeneratorStatusChanged;
        

        /// <summary>
        /// If containers are done, generate the selected item
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
        private void ItemContainerGeneratorStatusChanged(object sender, EventArgs e)
        
            if (ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
            
                ItemContainerGenerator.StatusChanged -= ItemContainerGeneratorStatusChanged;
                UpdateSelectedItem();
            
        

        /// <summary>
        /// Get the ItemsHolder and generate any children
        /// </summary>
        public override void OnApplyTemplate()
        
            base.OnApplyTemplate();
            itemsHolderPanel = GetTemplateChild("PART_ItemsHolder") as Panel;
            UpdateSelectedItem();
        

        /// <summary>
        /// When the items change we remove any generated panel children and add any new ones as necessary
        /// </summary>
        /// <param name="e">The <see cref="NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
        protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
        
            base.OnItemsChanged(e);

            if (itemsHolderPanel == null)
                return;

            switch (e.Action)
            
                case NotifyCollectionChangedAction.Reset:
                itemsHolderPanel.Children.Clear();
                break;

                case NotifyCollectionChangedAction.Add:
                case NotifyCollectionChangedAction.Remove:
                if (e.OldItems != null)
                
                    foreach (var item in e.OldItems)
                    
                        var cp = FindChildContentPresenter(item);
                        if (cp != null)
                            itemsHolderPanel.Children.Remove(cp);
                    
                

                // Don't do anything with new items because we don't want to
                // create visuals that aren't being shown

                UpdateSelectedItem();
                break;

                case NotifyCollectionChangedAction.Replace:
                throw new NotImplementedException("Replace not implemented yet");
            
        

        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        
            base.OnSelectionChanged(e);
            UpdateSelectedItem();
        

        private void UpdateSelectedItem()
        
            if (itemsHolderPanel == null)
                return;

            // Generate a ContentPresenter if necessary
            var item = GetSelectedTabItem();
            if (item != null)
                CreateChildContentPresenter(item);

            // show the right child
            foreach (ContentPresenter child in itemsHolderPanel.Children)
                child.Visibility = ((child.Tag as TabItem).IsSelected) ? Visibility.Visible : Visibility.Collapsed;
        

        private ContentPresenter CreateChildContentPresenter(object item)
        
            if (item == null)
                return null;

            var cp = FindChildContentPresenter(item);

            if (cp != null)
                return cp;

            var tabItem = item as TabItem;
            cp = new ContentPresenter
            
                Content = (tabItem != null) ? tabItem.Content : item,
                ContentTemplate = this.SelectedContentTemplate,
                ContentTemplateSelector = this.SelectedContentTemplateSelector,
                ContentStringFormat = this.SelectedContentStringFormat,
                Visibility = Visibility.Collapsed,
                Tag = tabItem ?? (this.ItemContainerGenerator.ContainerFromItem(item))
            ;
            itemsHolderPanel.Children.Add(cp);
            return cp;
        

        private ContentPresenter FindChildContentPresenter(object data)
        
            if (data is TabItem)
                data = (data as TabItem).Content;

            if (data == null)
                return null;

            if (itemsHolderPanel == null)
                return null;

            foreach (ContentPresenter cp in itemsHolderPanel.Children)
            
                if (cp.Content == data)
                    return cp;
            

            return null;
        

        protected TabItem GetSelectedTabItem()
        
            var selectedItem = SelectedItem;
            if (selectedItem == null)
                return null;

            var item = selectedItem as TabItem ?? ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as TabItem;

            return item;
        
    

Copietime 许可

// Copyright © 2010-2016 The CefSharp Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//    * Redistributions of source code must retain the above copyright
//      notice, this list of conditions and the following disclaimer.
//
//    * Redistributions in binary form must reproduce the above
//      copyright notice, this list of conditions and the following disclaimer
//      in the documentation and/or other materials provided with the
//      distribution.
//
//    * Neither the name of Google Inc. nor the name Chromium Embedded
//      Framework nor the name CefSharp nor the names of its contributors
//      may be used to endorse or promote products derived from this software
//      without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

【讨论】:

【参考方案3】:

根据上面@Arsen 的回答,这是另一种行为:

    不需要任何其他参考。 (除非您将代码放在外部库中) 它不使用基类。 它处理重置和添加集合更改。

使用它

在 xaml 中声明命名空间:

<ResourceDictionary
    ...
    xmlns:behaviors="clr-namespace:My.Behaviors;assembly=My.Wpf.Assembly"
    ...
    >

更新样式:

<Style TargetType="TabControl" x:Key="TabControl">
    ...
    <Setter Property="behaviors:TabControlBehavior.DoNotCacheControls" Value="True" />
    ...
</Style>

或者直接更新 TabControl:

<TabControl behaviors:TabControlBehavior.DoNotCacheControls="True" ItemsSource="Binding Tabs" SelectedItem="Binding SelectedTab">

这是行为的代码:

using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;

namespace My.Behaviors

    /// <summary>
    /// Wraps tab item contents in UserControl to prevent TabControl from re-using its content
    /// </summary>
    public class TabControlBehavior
    
        private static readonly HashSet<TabControl> _tabControls = new HashSet<TabControl>();
        private static readonly Dictionary<ItemCollection, TabControl> _tabControlItemCollections = new Dictionary<ItemCollection, TabControl>();

        public static bool GetDoNotCacheControls(TabControl tabControl)
        
            return (bool)tabControl.GetValue(DoNotCacheControlsProperty);
        

        public static void SetDoNotCacheControls(TabControl tabControl, bool value)
        
            tabControl.SetValue(DoNotCacheControlsProperty, value);
        

        public static readonly DependencyProperty DoNotCacheControlsProperty = DependencyProperty.RegisterAttached(
            "DoNotCacheControls",
            typeof(bool),
            typeof(TabControlBehavior),
            new UIPropertyMetadata(false, OnDoNotCacheControlsChanged));

        private static void OnDoNotCacheControlsChanged(
            DependencyObject depObj,
            DependencyPropertyChangedEventArgs e)
        
            var tabControl = depObj as TabControl;
            if (null == tabControl)
                return;
            if (e.NewValue is bool == false)
                return;

            if ((bool)e.NewValue)
                Attach(tabControl);
            else
                Detach(tabControl);
        

        private static void Attach(TabControl tabControl)
        
            if (!_tabControls.Add(tabControl))
                return;
            _tabControlItemCollections.Add(tabControl.Items, tabControl);
            ((INotifyCollectionChanged)tabControl.Items).CollectionChanged += TabControlUcWrapperBehavior_CollectionChanged;
        

        private static void Detach(TabControl tabControl)
        
            if (!_tabControls.Remove(tabControl))
                return;
            _tabControlItemCollections.Remove(tabControl.Items);
            ((INotifyCollectionChanged)tabControl.Items).CollectionChanged -= TabControlUcWrapperBehavior_CollectionChanged;
        

        private static void TabControlUcWrapperBehavior_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        
            var itemCollection = (ItemCollection)sender;
            var tabControl = _tabControlItemCollections[itemCollection];
            IList items;
            if (e.Action == NotifyCollectionChangedAction.Reset)
               /* our ObservableArray<T> swops out the whole collection */
                items = (ItemCollection)sender;
            
            else
            
                if (e.Action != NotifyCollectionChangedAction.Add)
                    return;

                items = e.NewItems;
            

            foreach (var newItem in items)
            
                var ti = tabControl.ItemContainerGenerator.ContainerFromItem(newItem) as TabItem;
                if (ti != null)
                
                    var userControl = ti.Content as UserControl;
                    if (null == userControl)
                        ti.Content = new UserControl  Content = ti.Content ;
                
            
        
    

【讨论】:

我喜欢这个解决方案,你如何修改它以使用 ContentTemplate?【参考方案4】:

WPF Application Framework (WAF) 的 Writer 示例应用程序展示了如何解决您的问题。它为每个 TabItem 创建一个新的 UserControl。因此,当用户更改活动选项卡时,状态会被保留。

【讨论】:

我认为这根本不是一个好的答案。它将您指向一个大型应用程序 - 您如何明确解决上述问题?? 提到为每个TabItem 创建一个新的UserControl 对我有帮助。我基本上只是通过将数据对象设置为UserControl.Content 属性将我的数据对象包装在UserControl 中,这解决了我的性能问题。 如何“将数据对象设置为 UserControl.Content”? 我也不觉得这个答案很有帮助。我花了很长时间试图找出它是如何在 Writer 示例应用程序中真正完成的。我注意到的是:他们在 ViewModel 中保留了对 View 的引用。我想这可以防止 View 及其状态丢失。但是我没有检查这是否真的解决了我的具体问题(这与 OP 完全相同)。我更喜欢 ViewModel 不需要知道 View 的解决方案(而是使用 DataTemplates,就像在 OP 中一样)。 @LineloDude 我认为他的意思是相反的?因此将 UserControl.Content 设置为数据对象。 (即myUserControl.Content = myDataObject【参考方案5】:

使用WAF的想法,我得出了这个似乎解决问题的简单解决方案。

我使用交互行为,但如果未引用交互库,则可以对附加属性进行相同操作

/// <summary>
/// Wraps tab item contents in UserControl to prevent TabControl from re-using its content
/// </summary>
public class TabControlUcWrapperBehavior 
    : Behavior<UIElement>

    private TabControl AssociatedTabControl  get  return (TabControl) AssociatedObject;  

    protected override void OnAttached()
    
        ((INotifyCollectionChanged)AssociatedTabControl.Items).CollectionChanged += TabControlUcWrapperBehavior_CollectionChanged;
        base.OnAttached();
    

    protected override void OnDetaching()
    
        ((INotifyCollectionChanged)AssociatedTabControl.Items).CollectionChanged -= TabControlUcWrapperBehavior_CollectionChanged;
        base.OnDetaching();
    

    void TabControlUcWrapperBehavior_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    
        if (e.Action != NotifyCollectionChangedAction.Add) 
            return;

        foreach (var newItem in e.NewItems)
        
            var ti = AssociatedTabControl.ItemContainerGenerator.ContainerFromItem(newItem) as TabItem;

            if (ti != null && !(ti.Content is UserControl)) 
                ti.Content = new UserControl  Content = ti.Content ;
        
    

及用法

<TabControl ItemsSource="...">
    <i:Interaction.Behaviors>
        <controls:TabControlUcWrapperBehavior/>
    </i:Interaction.Behaviors>
</TabControl>

【讨论】:

我正在尝试您的解决方案,但我看到的是 ti.Content 是视图模型,而不是模板中的控件。有什么想法吗? 检查你是否调用了 ItemFromContainer 方法而不是 ContainerFromItem ? 啊,你有那个视图模型的 DataTemplate 吗? TabControl 在 TabControl.ContentTemplate 属性中定义了一个 DataTemplate。该 DataTemplate 有一个在视图模型中显示数据的网格。 你能不能把不工作的样本发给我 gmail com 的 mkrtchyan.arsen 我会检查【参考方案6】:

我已经发布了类似问题的答案。在我的情况下,手动创建 TabItems 已经解决了一次又一次创建视图的问题。检查here

【讨论】:

以上是关于如何在 TabControl 中的选项卡项中保留控件状态的主要内容,如果未能解决你的问题,请参考以下文章

Win32 选项卡控件中的右对齐选项卡项

带有添加新选项卡按钮 (+) 的 TabControl

WPF多行TabControl而不重新排列行

2021-08-19 WPF控件专题 TabControl 控件详解

如何知道何时从子视图中单击了超级视图中的选项卡项

TabControl选项卡