WPF TabControl - 防止选项卡更改时卸载?

发布于 2024-09-16 17:00:40 字数 222 浏览 7 评论 0原文

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

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

Is there a way to prevent the Tab Unload/Reload when a tab changes in a WPF tab control? Or if that is not possible, is there a recommended method for caching the tabs contents so they don't have to be regenerated with each tab change?

For example, one tab's UI is completely customizable and stored in the database. When the user selects an object to work on, the items in the customized layout get populated with that object's data. Users expect a minor delay on initial load or when retrieving data, but not when changing back and forth between tabs, and the delay when changing tabs is very noticeable.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

儭儭莪哋寶赑 2024-09-23 17:00:40

我在这里找到了一个解决方法(web-archive-link,因为网站已关闭): https://web.archive.org/web/20120429044747/http://eric.burke.name/dotnetmania/2009/04/26/22.09。 28

它基本上存储选项卡的 ContentPresenter 并在切换选项卡时加载它而不是重绘它。拖/放选项卡时它仍然会导致延迟,因为这是一个删除/添加操作,但是经过一些修改,我也让它消失了(以较低的调度程序优先级运行删除代码,然后添加代码,所以添加操作有机会取消删除操作并使用旧的 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;
    }
}

I found a workaround here (web-archive-link, since site is down): https://web.archive.org/web/20120429044747/http://eric.burke.name/dotnetmania/2009/04/26/22.09.28

It basically stores the ContentPresenter of the tab and loads that up when switching tabs instead of redrawing it. It was still causing the delay when dragging/dropping tabs since that was an remove/add operation, however with some modifications I got that to go away as well (ran the Remove code at a lower dispatcher priority then the Add code, so the add operation had a chance to cancel the Remove operation and use the old ContentPresenter instead of drawing a new one)

Edit: The link above appears to no longer work, so I'll paste a copy of the code here. It's been modified a bit to allow dragging/dropping, but it should still work the same way.

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;
    }
}
寒尘 2024-09-23 17:00:40

除此之外,我遇到了类似的问题,并设法通过缓存表示后面代码中选项卡项内容的用户控件来解决它。

在我的项目中,我有一个绑定到集合(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”的加载事件中,仅当该方法为 null 时,才会实例化对“Overview”的静态引用,将此引用的数据上下文设置为当前数据上下文中的“OverviewViewModel”引用(即“MainWindowViewModel”),并将占位符的内容设置为静态引用向“Overview”

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

Just to add to this, I had a similar problem and managed to solve it by caching a user control that represented the contents of a tab item in the code behind.

In my project I have a tab control that is bound to a collection (MVVM). However the first tab is an overview that shows a summary of all of the other tabs in a list view. The problem I was having was that whenever a user moves their selection from an item tab to the overview tab, the overview is redrawn with all of the summary data, which can take 10-15 seconds depending on the number of items in the collection. (note their is no reloading of actual data from a db or anything, it is purely the drawing of the summary view that was taking the time).

What I wanted was for this loading of the summary view to only occur once when the data context is first loaded and any subsequent switching between tabs to be instantaneous.

Solution:

Classes involved:
MainWindow.xaml - The main page containing the tab control.
MainWindow.xaml.cs - Code behind for above.
MainWindowViewModel.cs - View model for the above view, contains the collection.
Overview.xaml - User control that draws the overview tab item content.
OverviewViewModel.cs - View model for the above view.

Steps:

  1. Replace the datatemplate in 'MainWindow.xaml' that draws the overview tab item with a blank user control named 'OverviewPlaceholder'

  2. Make the reference to 'OverviewViewModel' public within 'MainWindowViewModel.cs'

  3. Add a static reference to 'Overview' in 'MainWindow.xaml.cs'

  4. Add an event handler to the loaded event of user control 'OverviewPlaceholder', within this method instantiate the static reference to 'Overview' only if it is null, set the datacontext of this reference to the 'OverviewViewModel' reference within the current datacontext (that is 'MainWindowViewModel') and set the place holder's content to be the static reference to 'Overview'.

Now the overview page is only drawn once because each time it is loaded (i.e. the user clicks onto the overview tab), it puts the already rendered, static user control back onto the page.

对你的占有欲 2024-09-23 17:00:40

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

例如(MVVM风格)

替换

 

经过

 
            
        

I have a really simple solution to avoid the tab reload on tab change,
use a contentPresenter in the tabItem instead of the content property.

e.g.(in MVVM style)

replace

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

by

        <TabItem Header="Tab1">
            <ContentPresenter Content="{Binding Tab1ViewModel}" />
        </TabItem>
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文