WPF TabControl - предотвращение выгрузки при изменении вкладки? - PullRequest
17 голосов
/ 30 августа 2010

Есть ли способ предотвратить разгрузку / перезагрузку вкладки при изменении вкладки в элементе управления вкладки WPF? Или, если это невозможно, есть ли рекомендуемый способ кэширования содержимого вкладок, чтобы их не приходилось обновлять при каждом изменении вкладок?

Например, пользовательский интерфейс одной вкладки полностью настраивается и сохраняется в базе данных. Когда пользователь выбирает объект для работы, элементы в настроенном макете заполняются данными этого объекта. Пользователи ожидают небольшую задержку при начальной загрузке или при получении данных, но не при переходе назад и вперед между вкладками, и задержка при смене вкладок очень заметна.

Ответы [ 3 ]

17 голосов
/ 02 сентября 2010

Я нашел обходной путь здесь: 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 вкладки и загружает его при переключении вкладок вместо перерисовки. Это по-прежнему вызывало задержку при перетаскивании вкладок, так как это была операция удаления / добавления, однако с некоторыми изменениями я получил это, чтобы убрать (запущенный код удаления с более низким приоритетом диспетчера, чем код добавления, поэтому добавление операция смогла отменить операцию удаления и использовать старый ContentPresenter вместо рисования нового)

Редактировать: Ссылка выше, похоже, больше не работает, поэтому я вставлю здесь копию кода. Он был немного изменен, чтобы позволить перетаскивание, но он все равно должен работать так же.

// 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;
    }
}
2 голосов
/ 20 августа 2012

Просто чтобы добавить к этому, у меня была похожая проблема, и мне удалось ее решить путем кэширования пользовательского элемента управления, который представлял содержимое элемента вкладки в коде позади.

В моем проекте у меня есть вкладкаэлемент управления, связанный с коллекцией (MVVM).Однако первая вкладка представляет собой обзор, который показывает сводку всех других вкладок в виде списка.Проблема, с которой я столкнулся, заключалась в том, что всякий раз, когда пользователь перемещает свой выбор со вкладки элемента на вкладку обзора, обзор перерисовывается со всеми сводными данными, что может занять 10-15 секунд в зависимости от количества элементов в коллекции.(обратите внимание, что это не перезагрузка фактических данных из БД или чего-либо еще, это просто рисунок сводного представления, который занимал время).

То, что я хотел, было для этой загрузки сводного представления толькопроисходят один раз, когда контекст данных загружается впервые, и любое последующее переключение между вкладками должно быть мгновенным.

Решение:

Используемые классы: MainWindow.xaml - главная страница, содержащая элемент управления вкладкой.MainWindow.xaml.cs - код для выше.MainWindowViewModel.cs - представление модели для представленного выше представления, содержит коллекцию.Overview.xaml - пользовательский элемент управления, который отображает содержимое элемента вкладки обзора.OverviewViewModel.cs - просмотр модели для представленного выше представления.

Шаги:

  1. Замените табличку данных в «MainWindow.xaml», которая рисует элемент вкладки обзора, с пустым пользовательским элементом управления с именем «OverviewPlaceholder»

  2. Сделать ссылку на «OverviewViewModel» общедоступной в «MainWindowViewModel.cs»

  3. Добавить статическую ссылку на «Overview» в «MainWindow.xaml.cs»

  4. Добавить обработчик события к загруженному событию пользовательского элемента управления «OverviewPlaceholder», в этом методе создавать статическую ссылку на «Overview» только в том случае, если она пуста, установить текст данных этой ссылки наСсылка «OverviewViewModel» в текущем текстовом тексте данных (то есть «MainWindowViewModel») и установка содержимого заполнителя в качестве статической ссылки на «Overview».

Теперь страница обзора отображается толькоодин раз, потому что каждый раз, когда он загружается (т. е. пользователь нажимает на вкладку обзора), он возвращает уже визуализированный статический пользовательский элемент управления обратно на страницу.

0 голосов
/ 16 апреля 2015

У меня есть действительно простое решение, чтобы избежать перезагрузки вкладок при смене вкладок, используйте contentPresenter в tabItem вместо свойства content.

например (в стиле MVVM)

replace

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

от

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