【问题标题】:WPF TabControl - Preventing Unload on Tab Change?WPF TabControl - 防止在选项卡更改时卸载?
【发布时间】:2010-08-30 14:12:18
【问题描述】:

当 WPF 选项卡控件中的选项卡更改时,有没有办法防止选项卡卸载/重新加载?或者,如果这不可能,是否有推荐的方法来缓存选项卡内容,以便不必在每次选项卡更改时重新生成它们?

例如,一个选项卡的 UI 是完全可定制的并存储在数据库中。当用户选择要处理的对象时,自定义布局中的项目将填充该对象的数据。用户预计初始加载或检索数据时会出现轻微延迟,但在选项卡之间来回切换时不会出现延迟,并且切换选项卡时的延迟非常明显。

【问题讨论】:

  • 我认为 TabItems 不会在选项卡控件中的选定项目发生更改时被卸载/重新加载。我不确定,但可能需要更改 TabControl 的 SelectionChanged 逻辑,以便它不会每次都重新查询数据库?
  • 每次更改选项卡时都会运行 DataTemplates 的 Loaded/Unloaded 事件(我使用的是 MVVM 设计模式)
  • So, in your application, whenever the selected tab changes, it triggers a connection to the database to retrieve the object's data?
  • 是的,我想缓存选项卡,这样它就不必重建它,或者做一些变通方法来防止它在选项卡更改时卸载/重新加载内容。跨度>

标签: wpf caching tabcontrol


【解决方案1】:

我在这里找到了解决方法:https://web.archive.org/web/20120429044747/http://eric.burke.name/dotnetmania/2009/04/26/22.09.28

编辑:这是更正后的链接: http://web.archive.org/web/20110825185059/http://eric.burke.name/dotnetmania/2009/04/26/22.09.28

它基本上存储选项卡的 ContentPresenter 并在切换选项卡而不是重绘它时加载它。拖放选项卡时它仍然会导致延迟,因为这是一个删除/添加操作,但是通过一些修改我也得到了它(以较低的调度程序优先级运行删除代码然后添加代码,所以添加操作有机会取消 Remove 操作并使用旧的 ContentPresenter 而不是绘制新的)

编辑:上面的链接似乎不再有效,所以我将在此处粘贴代码的副本。它进行了一些修改以允许拖放,但它应该仍然以相同的方式工作。

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

// Extended TabControl which saves the displayed item so you don't get the performance hit of 
// unloading and reloading the VisualTree when switching tabs

// Obtained from http://eric.burke.name/dotnetmania/2009/04/26/22.09.28
// and made a some modifications so it reuses a TabItem's ContentPresenter when doing drag/drop operations

[TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
public class TabControlEx : System.Windows.Controls.TabControl
{
    // Holds all items, but only marks the current tab's item as visible
    private Panel _itemsHolder = null;

    // Temporaily holds deleted item in case this was a drag/drop operation
    private object _deletedObject = null;

    public TabControlEx()
        : base()
    {
        // this is necessary so that we get the initial databound selected item
        this.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
    }

    /// <summary>
    /// if containers are done, generate the selected item
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
    {
        if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
        {
            this.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
            UpdateSelectedItem();
        }
    }

    /// <summary>
    /// get the ItemsHolder and generate any children
    /// </summary>
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        _itemsHolder = 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"></param>
    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnItemsChanged(e);

        if (_itemsHolder == null)
        {
            return;
        }

        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Reset:
                _itemsHolder.Children.Clear();

                if (base.Items.Count > 0)
                {
                    base.SelectedItem = base.Items[0];
                    UpdateSelectedItem();
                }

                break;

            case NotifyCollectionChangedAction.Add:
            case NotifyCollectionChangedAction.Remove:

                // Search for recently deleted items caused by a Drag/Drop operation
                if (e.NewItems != null && _deletedObject != null)
                {
                    foreach (var item in e.NewItems)
                    {
                        if (_deletedObject == item)
                        {
                            // If the new item is the same as the recently deleted one (i.e. a drag/drop event)
                            // then cancel the deletion and reuse the ContentPresenter so it doesn't have to be 
                            // redrawn. We do need to link the presenter to the new item though (using the Tag)
                            ContentPresenter cp = FindChildContentPresenter(_deletedObject);
                            if (cp != null)
                            {
                                int index = _itemsHolder.Children.IndexOf(cp);

                                (_itemsHolder.Children[index] as ContentPresenter).Tag =
                                    (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
                            }
                            _deletedObject = null;
                        }
                    }
                }

                if (e.OldItems != null)
                {
                    foreach (var item in e.OldItems)
                    {

                        _deletedObject = item;

                        // We want to run this at a slightly later priority in case this
                        // is a drag/drop operation so that we can reuse the template
                        this.Dispatcher.BeginInvoke(DispatcherPriority.DataBind,
                            new Action(delegate()
                        {
                            if (_deletedObject != null)
                            {
                                ContentPresenter cp = FindChildContentPresenter(_deletedObject);
                                if (cp != null)
                                {
                                    this._itemsHolder.Children.Remove(cp);
                                }
                            }
                        }
                        ));
                    }
                }

                UpdateSelectedItem();
                break;

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

    /// <summary>
    /// update the visible child in the ItemsHolder
    /// </summary>
    /// <param name="e"></param>
    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);
        UpdateSelectedItem();
    }

    /// <summary>
    /// generate a ContentPresenter for the selected item
    /// </summary>
    void UpdateSelectedItem()
    {
        if (_itemsHolder == null)
        {
            return;
        }

        // generate a ContentPresenter if necessary
        TabItem item = GetSelectedTabItem();
        if (item != null)
        {
            CreateChildContentPresenter(item);
        }

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

    /// <summary>
    /// create the child ContentPresenter for the given item (could be data or a TabItem)
    /// </summary>
    /// <param name="item"></param>
    /// <returns></returns>
    ContentPresenter CreateChildContentPresenter(object item)
    {
        if (item == null)
        {
            return null;
        }

        ContentPresenter cp = FindChildContentPresenter(item);

        if (cp != null)
        {
            return cp;
        }

        // the actual child to be added.  cp.Tag is a reference to the TabItem
        cp = new ContentPresenter();
        cp.Content = (item is TabItem) ? (item as TabItem).Content : item;
        cp.ContentTemplate = this.SelectedContentTemplate;
        cp.ContentTemplateSelector = this.SelectedContentTemplateSelector;
        cp.ContentStringFormat = this.SelectedContentStringFormat;
        cp.Visibility = Visibility.Collapsed;
        cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
        _itemsHolder.Children.Add(cp);
        return cp;
    }

    /// <summary>
    /// Find the CP for the given object.  data could be a TabItem or a piece of data
    /// </summary>
    /// <param name="data"></param>
    /// <returns></returns>
    ContentPresenter FindChildContentPresenter(object data)
    {
        if (data is TabItem)
        {
            data = (data as TabItem).Content;
        }

        if (data == null)
        {
            return null;
        }

        if (_itemsHolder == null)
        {
            return null;
        }

        foreach (ContentPresenter cp in _itemsHolder.Children)
        {
            if (cp.Content == data)
            {
                return cp;
            }
        }

        return null;
    }

    /// <summary>
    /// copied from TabControl; wish it were protected in that class instead of private
    /// </summary>
    /// <returns></returns>
    protected TabItem GetSelectedTabItem()
    {
        object selectedItem = base.SelectedItem;
        if (selectedItem == null)
        {
            return null;
        }

        if (_deletedObject == selectedItem)
        { 

        }

        TabItem item = selectedItem as TabItem;
        if (item == null)
        {
            item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
        }
        return item;
    }
}

【讨论】:

  • 我认为 StackOverflow 只是错误地解析了链接的降价。如果您复制/粘贴整个 URL(或不使用 [] 降价),它会起作用。 web.archive.org/web/20110825185059/http://eric.burke.name/…
  • 我正在尝试使用您的解决方案,因为它似乎可以解决我面临的确切问题。但是,我不知道如何完成这项工作......例如,变量_itemsHolder 始终是null。控件在我的 XAML 中,一切都正确显示,但是 XAML 中是否有我需要弥合差距的特定引用?
  • @DonBoitnott 在什么时候它是空的?你说它工作正常,所以它必须在某个时候被填充。
  • 好吧,“正确”只是在一切都正确显示的意义上。它通过调用GetTemplateChildOnApplyTemplate 中实例化,并且在该点及以后为空。值得一提的是,我是 WPF 新手,我不知道“PART_ItemsHolder”的用途。
  • @Rachel 我的问题的答案最终是在this answer 中定义的ControlTemplate
【解决方案2】:

补充一点,我遇到了一个类似的问题,并设法通过缓存一个用户控件来解决它,该控件表示后面代码中的选项卡项的内容。

在我的项目中,我有一个绑定到集合 (MVVM) 的选项卡控件。但是,第一个选项卡是一个概览,它在列表视图中显示所有其他选项卡的摘要。我遇到的问题是,每当用户将他们的选择从项目选项卡移动到概览选项卡时,都会使用所有摘要数据重新绘制概览,这可能需要 10-15 秒,具体取决于集合中的项目数量。 (请注意,它们不会从数据库或任何东西重新加载实际数据,它纯粹是花费时间绘制摘要视图)。

我想要的是只在第一次加载数据上下文时才加载摘要视图,并且随后在选项卡之间进行任何即时切换。

解决方案:

涉及的课程: MainWindow.xaml - 包含选项卡控件的主页。 MainWindow.xaml.cs - 上面的代码。 MainWindowViewModel.cs - 上述视图的视图模型,包含集合。 Overview.xaml - 绘制概览选项卡项内容的用户控件。 OverviewViewModel.cs - 上述视图的视图模型。

步骤:

  1. 用名为“OverviewPlaceholder”的空白用户控件替换“MainWindow.xaml”中绘制概览选项卡项的数据模板

  2. 在“MainWindowViewModel.cs”中公开对“OverviewViewModel”的引用

  3. 在“MainWindow.xaml.cs”中添加对“Overview”的静态引用

  4. 将事件处理程序添加到用户控件“OverviewPlaceholder”的加载事件,在此方法中仅当静态引用为“Overview”时实例化“Overview”,将此引用的数据上下文设置为“OverviewViewModel”引用在当前数据上下文(即“MainWindowViewModel”)中,并将占位符的内容设置为“Overview”的静态引用。

现在概览页面只绘制一次,因为每次加载(即用户单击概览选项卡)时,它会将已呈现的静态用户控件放回页面上。

【讨论】:

    【解决方案3】:

    我有一个非常简单的解决方案来避免标签更改时标签重新加载, 在 tabItem 中使用 contentPresenter 而不是 content 属性。

    例如(MVVM 风格)

    替换

          <TabItem Header="Tab1" Content="{Binding Tab1ViewModel}" />
    

            <TabItem Header="Tab1">
                <ContentPresenter Content="{Binding Tab1ViewModel}" />
            </TabItem>
    

    【讨论】:

    • 为什么这个答案被赞成? ContentPresenter 将在选项卡切换时卸载,包括其内容。这无济于事。
    猜你喜欢
    • 2011-06-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-28
    相关资源
    最近更新 更多