【问题标题】:TabItem doesn't load immediatelyTabItem 不会立即加载
【发布时间】:2017-02-01 09:27:00
【问题描述】:

我有一个 MVVM 设置,其中包含 TabControl 和一个 ObservableCollection<ViewModel> 的 tabitems。

我打开一个文件并将由该文件制成的模型加载到TabItem

var model = new ViewModel(data, filename);
ViewModels.Tabs.Add(model);

TabItem 有一个 DataTemplate,它是 HeaderContent
Content在单独的UserControl中定义,Header在主文件本身中。

当我运行时会显示标题,但只有当我单击标题时,才会触发 tabitem 的加载事件并显示内容。

我希望它会立即加载,为什么不呢?

当我同时添加两个标签时:

var model = new ViewModel(data, filename);
ViewModels.Tabs.Add(model);
ViewModels.Tabs.Add(model);

然后第一个选项卡的加载事件会触发并显示其内容。

我怎样才能达到预期的行为?

【问题讨论】:

  • 这是一个虚拟化的东西。另一个常见的问题是取消选择 TabItem 时会丢失视觉状态。解决方案是摆脱虚拟化;实际上,编写代码以便将ObservableCollection 绑定到ItemsSource 时,它将为集合中的每个项目添加TabItem。 codeproject 上有一个我不能亲自担保的版本:stackoverflow.com/a/36209166/424129
  • 更正:我可以亲自担保。我记得去年也做过同样的事情,刚刚检查了那个项目,结果发现我只是使用了他的代码。它工作正常。
  • 我明白了,复杂的东西。好吧,我可能只是添加 ViewModels.Tabs.Add(null); ViewModels.Tabs.Remove(null); 作为解决方法。

标签: wpf tabitem loaded


【解决方案1】:

注意 AddValueChanged 是内存泄漏,我在写这个答案时并没有意识到这一点。根据您的情况,在下面的代码中省略 EnsureContentTemplateIsNotModified 可能是明智之举。我目前没有时间修复和重新测试该更改,因此我将保持原样。有关更多详细信息,请参阅下面的 eih 评论。

这是由于虚拟化。与任何Selector 子类一样,只有可见项实际存在。而在TabControl 中,唯一可见的项目是选定的项目。我不认为这是选项卡控件最常见用途的理想设计选择,但我们在这里。

我发现的最佳解决方法是添加一个附加属性,该属性介入并为ItemsSource 中的每个项目创建一个实际的TabItem。从this woefully unappreciated answer,我找到了this CodeProject thing by Ivan Krivyakov。我已经使用它并且它有效。

<TabControl
    xmlns:ikriv="clr-namespace:IKriv.Windows.Controls.Behaviors"
    ikriv:TabContent.IsCached="True"

这是 285 行 C# 代码,但互联网上的东西消失了。这里是:

// TabContent.cs, version 1.2
// The code in this file is Copyright (c) Ivan Krivyakov
// See http://www.ikriv.com/legal.php for more information
//
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Markup;

/// <summary>
/// http://www.codeproject.com/Articles/460989/WPF-TabControl-Turning-Off-Tab-Virtualization
/// </summary>
namespace IKriv.Windows.Controls.Behaviors
{
    /// <summary>
    /// Attached properties for persistent tab control
    /// </summary>
    /// <remarks>By default WPF TabControl bound to an ItemsSource destroys visual state of invisible tabs. 
    /// Set ikriv:TabContent.IsCached="True" to preserve visual state of each tab.
    /// </remarks>
    public static class TabContent
    {
        public static bool GetIsCached(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsCachedProperty);
        }

        public static void SetIsCached(DependencyObject obj, bool value)
        {
            obj.SetValue(IsCachedProperty, value);
        }

        /// <summary>
        /// Controls whether tab content is cached or not
        /// </summary>
        /// <remarks>When TabContent.IsCached is true, visual state of each tab is preserved (cached), even when the tab is hidden</remarks>
        public static readonly DependencyProperty IsCachedProperty =
            DependencyProperty.RegisterAttached("IsCached", typeof(bool), typeof(TabContent), new UIPropertyMetadata(false, OnIsCachedChanged));


        public static DataTemplate GetTemplate(DependencyObject obj)
        {
            return (DataTemplate)obj.GetValue(TemplateProperty);
        }

        public static void SetTemplate(DependencyObject obj, DataTemplate value)
        {
            obj.SetValue(TemplateProperty, value);
        }

        /// <summary>
        /// Used instead of TabControl.ContentTemplate for cached tabs
        /// </summary>
        public static readonly DependencyProperty TemplateProperty =
            DependencyProperty.RegisterAttached("Template", typeof(DataTemplate), typeof(TabContent), new UIPropertyMetadata(null));


        public static DataTemplateSelector GetTemplateSelector(DependencyObject obj)
        {
            return (DataTemplateSelector)obj.GetValue(TemplateSelectorProperty);
        }

        public static void SetTemplateSelector(DependencyObject obj, DataTemplateSelector value)
        {
            obj.SetValue(TemplateSelectorProperty, value);
        }

        /// <summary>
        /// Used instead of TabControl.ContentTemplateSelector for cached tabs
        /// </summary>
        public static readonly DependencyProperty TemplateSelectorProperty =
            DependencyProperty.RegisterAttached("TemplateSelector", typeof(DataTemplateSelector), typeof(TabContent), new UIPropertyMetadata(null));

        [EditorBrowsable(EditorBrowsableState.Never)]
        public static TabControl GetInternalTabControl(DependencyObject obj)
        {
            return (TabControl)obj.GetValue(InternalTabControlProperty);
        }

        [EditorBrowsable(EditorBrowsableState.Never)]
        public static void SetInternalTabControl(DependencyObject obj, TabControl value)
        {
            obj.SetValue(InternalTabControlProperty, value);
        }

        // Using a DependencyProperty as the backing store for InternalTabControl.  This enables animation, styling, binding, etc...
        [EditorBrowsable(EditorBrowsableState.Never)]
        public static readonly DependencyProperty InternalTabControlProperty =
            DependencyProperty.RegisterAttached("InternalTabControl", typeof(TabControl), typeof(TabContent), new UIPropertyMetadata(null, OnInternalTabControlChanged));


        [EditorBrowsable(EditorBrowsableState.Never)]
        public static ContentControl GetInternalCachedContent(DependencyObject obj)
        {
            return (ContentControl)obj.GetValue(InternalCachedContentProperty);
        }

        [EditorBrowsable(EditorBrowsableState.Never)]
        public static void SetInternalCachedContent(DependencyObject obj, ContentControl value)
        {
            obj.SetValue(InternalCachedContentProperty, value);
        }

        // Using a DependencyProperty as the backing store for InternalCachedContent.  This enables animation, styling, binding, etc...
        [EditorBrowsable(EditorBrowsableState.Never)]
        public static readonly DependencyProperty InternalCachedContentProperty =
            DependencyProperty.RegisterAttached("InternalCachedContent", typeof(ContentControl), typeof(TabContent), new UIPropertyMetadata(null));

        [EditorBrowsable(EditorBrowsableState.Never)]
        public static object GetInternalContentManager(DependencyObject obj)
        {
            return (object)obj.GetValue(InternalContentManagerProperty);
        }

        [EditorBrowsable(EditorBrowsableState.Never)]
        public static void SetInternalContentManager(DependencyObject obj, object value)
        {
            obj.SetValue(InternalContentManagerProperty, value);
        }

        // Using a DependencyProperty as the backing store for InternalContentManager.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty InternalContentManagerProperty =
            DependencyProperty.RegisterAttached("InternalContentManager", typeof(object), typeof(TabContent), new UIPropertyMetadata(null));

        private static void OnIsCachedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            if (obj == null) return;

            var tabControl = obj as TabControl;
            if (tabControl == null)
            {
                throw new InvalidOperationException("Cannot set TabContent.IsCached on object of type " + args.NewValue.GetType().Name +
                    ". Only objects of type TabControl can have TabContent.IsCached property.");
            }

            bool newValue = (bool)args.NewValue;

            if (!newValue)
            {
                if (args.OldValue != null && ((bool)args.OldValue))
                {
                    throw new NotImplementedException("Cannot change TabContent.IsCached from True to False. Turning tab caching off is not implemented");
                }

                return;
            }

            EnsureContentTemplateIsNull(tabControl);
            tabControl.ContentTemplate = CreateContentTemplate();
            EnsureContentTemplateIsNotModified(tabControl);
        }

        private static DataTemplate CreateContentTemplate()
        {
            const string xaml =
                "<DataTemplate><Border b:TabContent.InternalTabControl=\"{Binding RelativeSource={RelativeSource AncestorType=TabControl}}\" /></DataTemplate>";

            var context = new ParserContext();

            context.XamlTypeMapper = new XamlTypeMapper(new string[0]);
            context.XamlTypeMapper.AddMappingProcessingInstruction("b", typeof(TabContent).Namespace, typeof(TabContent).Assembly.FullName);

            context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
            context.XmlnsDictionary.Add("b", "b");

            var template = (DataTemplate)XamlReader.Parse(xaml, context);
            return template;
        }

        private static void OnInternalTabControlChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            if (obj == null) return;
            var container = obj as Decorator;

            if (container == null)
            {
                var message = "Cannot set TabContent.InternalTabControl on object of type " + obj.GetType().Name +
                    ". Only controls that derive from Decorator, such as Border can have a TabContent.InternalTabControl.";
                throw new InvalidOperationException(message);
            }

            if (args.NewValue == null) return;
            if (!(args.NewValue is TabControl))
            {
                throw new InvalidOperationException("Value of TabContent.InternalTabControl cannot be of type " + args.NewValue.GetType().Name +", it must be of type TabControl");
            }

            var tabControl = (TabControl)args.NewValue;
            var contentManager = GetContentManager(tabControl, container);
            contentManager.UpdateSelectedTab();
        }

        private static ContentManager GetContentManager(TabControl tabControl, Decorator container)
        {
            var contentManager = (ContentManager)GetInternalContentManager(tabControl);
            if (contentManager != null)
            {
                /*
                 * Content manager already exists for the tab control. This means that tab content template is applied 
                 * again, and new instance of the Border control (container) has been created. The old container 
                 * referenced by the content manager is no longer visible and needs to be replaced
                 */
                contentManager.ReplaceContainer(container);
            }
            else
            {
                // create content manager for the first time
                contentManager = new ContentManager(tabControl, container);
                SetInternalContentManager(tabControl, contentManager);
            }

            return contentManager;
        }

        private static void EnsureContentTemplateIsNull(TabControl tabControl)
        {
            if (tabControl.ContentTemplate != null)
            {
                throw new InvalidOperationException("TabControl.ContentTemplate value is not null. If TabContent.IsCached is True, use TabContent.Template instead of ContentTemplate");
            }
        }

        private static void EnsureContentTemplateIsNotModified(TabControl tabControl)
        {
            var descriptor = DependencyPropertyDescriptor.FromProperty(TabControl.ContentTemplateProperty, typeof(TabControl));
            descriptor.AddValueChanged(tabControl, (sender, args) =>
                {
                    throw new InvalidOperationException("Cannot assign to TabControl.ContentTemplate when TabContent.IsCached is True. Use TabContent.Template instead");
                });
        }

        public class ContentManager
        {
            TabControl _tabControl;
            Decorator _border;

            public ContentManager(TabControl tabControl, Decorator border)
            {
                _tabControl = tabControl;
                _border = border;
                _tabControl.SelectionChanged += (sender, args) => { UpdateSelectedTab(); };
            }

            public void ReplaceContainer(Decorator newBorder)
            {
                if (Object.ReferenceEquals(_border, newBorder)) return;

                _border.Child = null; // detach any tab content that old border may hold
                _border = newBorder;
            }

            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 
                    { 
                        DataContext = item,
                        ContentTemplate = TabContent.GetTemplate(_tabControl), 
                        ContentTemplateSelector = TabContent.GetTemplateSelector(_tabControl)
                    };

                    cachedContent.SetBinding(ContentControl.ContentProperty, new Binding());
                    TabContent.SetInternalCachedContent(tabItem, cachedContent);
                }

                return cachedContent;
            }
        }
    }
}

【讨论】:

  • 我认为这是一个很棒的解决方案,它帮助解决了我面临的一些性能问题。然而,我确实发现了一个内存泄漏,它阻止了使用此行为的 TabControl 实例被收集。我使用 .Net Memory Profiler 追踪到 EnsureContentTemplateIsNotModified 中发生的情况。搜索 AddValueChanged,您会发现有关此的警告。您可以通过在正确的时间分离事件或使用弱事件模式来解决此问题,但我只是删除了该方法,因为它感觉是多余的。
  • @eih 是的,因为我发布(并忘记了)这个答案,我们在我们自己的应用程序中发现了关于 AddValueChanged 的​​相同发现。我会更新答案。谢谢。
【解决方案2】:

要实现您想要的,您必须绑定到 TabControl 的 SelectedItem 属性,并且在您的视图模型中,您应该将其指向集合中的任何元素。它应该如下所示: XAML

<TabControl ItemsSource="{Binding Items}" SelectedItem="{Binding Item}">
</TabControl>

视图模型

public ViewModel() {
       SelectedItem = Items.First();
    }

public ObservableCollection<Item> Items { get; set; } = new ObservableCollection<Item> {
        new Item("test1", 5),
        new Item("test2", 2)
    };

public Item SelectedItem { get; set; } //don't forget to implement ChangeNotifications for it

【讨论】:

  • 我已经这样做了,它适用于选择。但同样,至少在我的情况下,直到我点击标题才会加载 tabitem。
  • 不确定我理解你的意思。如果您绑定到 SelectedItem 的设置正确,它应该会立即显示您选择的选项卡,而无需单击它的标题。尝试创建一个新的 WPF 项目并在那里进行测试。也许您的代码中还有其他内容阻止加载...
  • 我的意思是,虽然选择的选项卡确实会立即显示,但无需单击它的标题,其余选项卡控件的内容是空的。我必须单击标题才能加载该内容。所以当你的tabitem有datagrids这样的内容时试试看,这些datagrids是否立即显示?
  • 好吧,如果您有一些复杂的内容,例如需要显式初始化可能会出现问题,因此您必须在将其设置为 SelectedItem 之前对其进行初始化。从您的示例代码中无法了解这些问题。我设法复制的是this。它清楚地表明,如果 SelectedItem 未正确绑定,则选项卡控件确实没有选择选项卡。
猜你喜欢
  • 2021-06-04
  • 2020-10-29
  • 1970-01-01
  • 2012-05-20
  • 1970-01-01
  • 2017-10-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多