Как сохранить состояние элемента управления в элементах вкладки в TabControl

Я новичок в WPF и пытаюсь создать проект, который следует рекомендациям отличной статьи Джоша Смита, описывающей Шаблон проектирования модель-представление-модель просмотра.

Используя образец кода Джоша в качестве основы, я создал простое приложение, которое содержит ряд «рабочих пространств», каждое из которых представлено вкладкой в ​​TabControl. В моем приложении рабочая область - это редактор документов, который позволяет управлять иерархическим документом с помощью элемента управления TreeView.

Хотя мне удалось открыть несколько рабочих пространств и просмотреть содержимое их документов в связанном элементе управления TreeView, я обнаружил, что TreeView «забывает» свое состояние при переключении между вкладками. Например, если TreeView в Tab1 частично развернут, он будет отображаться как полностью свернутый после переключения на Tab2 и возврата на Tab1. Такое поведение, по-видимому, применимо ко всем аспектам состояния элемента управления для всех элементов управления.

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

Полагаю, мне не хватает чего-то простого, но я не уверен, где искать ответ. Любое руководство будет очень признательно.

Спасибо, Тим

Обновлять:

По запросу я попытаюсь опубликовать код, демонстрирующий эту проблему. Однако, поскольку данные, лежащие в основе 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 правильно привязывается к ObservableCollection из DocumentViewModel, благодаря чему каждый член представлен через DocumentView.

Для простоты этого примера я удалил TreeView (упомянутый выше) из DocumentView и заменил его TabControl, содержащим 3 фиксированных вкладки:

<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; }

... внутренняя вкладка может запомнить свой выбор.

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


person Tim Coulter    schedule 17.01.2010    source источник
comment
Элементы управления в WPF «запоминают» свое состояние по умолчанию, тот факт, что элементы управления в ваших элементах вкладки «забывают» свое состояние, является результатом некоторых явных действий с вашей стороны. Покажите XAML для элементов вкладки и соответствующий код модели представления для содержащихся в них привязок.   -  person Aviad P.    schedule 17.01.2010
comment
Я согласен с Aviad, трудно сказать, что не так, не видя вашего кода. Чтобы найти несколько отличных статей об элементе управления TreeView в WPF, я предлагаю взглянуть на блог Беа Штольниц ... bea.stollnitz.com/blog/index.php?s=treeview   -  person Chris Nicol    schedule 17.01.2010
comment
Я беру свой комментарий назад, это настоящая неприятная проблема, я просто попробовал ее со всех сторон, и если элемент управления вкладкой использует ItemsSource с DataTemplate, похоже, что визуальное состояние элементов управления в шаблоне данных общий (!!!) среди элементов вкладки!   -  person Aviad P.    schedule 18.01.2010
comment
У меня была эта проблема не только с древовидным представлением ... если у вас нет двусторонней ViewModel, поддерживающей состояние элемента управления, он его потеряет. Причина этого в том, что при переключении вкладок элементы управления больше не являются частью визуального дерева. Событие Unload фактически запускается для этих элементов управления, и они больше не имеют визуального представления, пока вы не переключитесь обратно. Это форма виртуализации, которую реализует элемент управления вкладками для экономии памяти при большом количестве вкладок. Очень интересно узнать, придумаете ли вы для этого решение ... Я только что создавал виртуальные машины.   -  person Anderson Imes    schedule 18.01.2010
comment
@Anderson Imes: На самом деле, этот комментарий должен быть ответом.   -  person Scott Whitlock    schedule 19.01.2010
comment
@Scott Whitlock - да, я согласен, комментарий Андерсона Аймса, кажется, настолько близок, насколько я могу подойти к решению этой проблемы. +1.   -  person Tim Coulter    schedule 19.01.2010
comment
@ Скотт Уитлок, спасибо ... Мне кажется, что все еще может быть какое-то обходное решение. Может быть, придет какой-нибудь знаток WPF и даст нам реальный способ сохранить визуальное состояние.   -  person Anderson Imes    schedule 19.01.2010
comment
Есть ли способ просмотреть визуальное дерево, чтобы увидеть, есть ли изменения? Я пытаюсь применить ValidationRules ко всем привязкам к FrameworkElements в моем визуальном дереве. Поскольку дерево меняется, у меня возникают проблемы с применением привязок к элементам.   -  person Frinavale    schedule 04.03.2011
comment
Не могли бы вы показать нам, как вы это решили? Я считаю, что приведенный ниже ответ не отвечает на вопрос ...   -  person MoonKnight    schedule 11.10.2013
comment
@Killercam: IIRC, образец WAF, существовавший в январе 2010 года, очень помог мне в поиске работоспособного решения, но впоследствии претерпел значительные изменения, поскольку он пытается делать многие вещи и уже не очень ясен в этом вопросе. . Я недавно повторно посетил его, чтобы освежить свою память (не работал с WPF почти 3 года) и был разочарован, обнаружив, что не могу воспроизвести то, что достиг ранее. В конце концов, я прибег к простому созданию дополнительных свойств визуального состояния в модели представления. Извините, что я не могу предложить что-то более полезное.   -  person Tim Coulter    schedule 14.10.2013
comment
Спасибо за ваше время.   -  person MoonKnight    schedule 14.10.2013


Ответы (6)


Пример приложения Writer для WPF Application Framework (WAF) показывает, как решить вашу проблема. Он создает новый UserControl для каждого элемента TabItem. Таким образом, состояние сохраняется, когда пользователь меняет активную вкладку.

person jbe    schedule 25.01.2010
comment
Я не думаю, что это вообще хороший ответ. Он указывает на большое приложение - как решить проблему, описанную выше? - person MoonKnight; 11.10.2013
comment
Упоминание о создании нового UserControl для каждого TabItem - вот что мне помогло. Я просто обернул свой объект данных в UserControl, установив для объекта данных свойство UserControl.Content, и это решило мою проблему с производительностью. - person E-rich; 30.07.2014
comment
Как установить объект данных в UserControl.Content? - person LineloDude; 13.08.2014
comment
Я тоже не считаю этот ответ очень полезным. Мне потребовалось довольно много времени, чтобы выяснить, как это действительно было сделано в примере приложения Writer. Я заметил следующее: они хранят ссылку на View в своей ViewModel. Я предполагаю, что это предотвращает потерю представления и его состояния. Однако я не проверял, действительно ли это решает мою конкретную проблему (которая в точности совпадает с OP). Я бы предпочел решение, в котором ViewModel не должен знать View (а вместо этого использовать DataTemplates, как в OP). - person Robert Hegner; 29.06.2015
comment
@LineloDude Я думаю, он имеет в виду обратное? Итак, установив UserControl.Content в объект данных. (т.е. myUserControl.Content = myDataObject) - person Peter; 18.08.2015

Я решил эту проблему с помощью этого совета WPF TabControl: Turning Off Tab Virtualization на странице http://www.codeproject.com/Articles/460989/WPF-TabControl-Turning-Off-Tab-Virtualization это класс TabContent со свойством IsCached.

person Alexsandro    schedule 24.03.2016

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

Здесь Код на случай, если Ссылка не работает:

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://stackoverflow.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.
person WiiMaxx    schedule 15.01.2016

Основываясь на ответе @ Arsen выше, вот еще одно поведение, которое:

  1. Не требует дополнительных ссылок. (если вы не поместите код во внешнюю библиотеку)
  2. Он не использует базовый класс.
  3. Он обрабатывает как сбросить, так и добавить изменения коллекции.

Чтобы использовать

Объявите пространство имен в 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 };
                }
            }
        }
    }
}
person Martin Lottering    schedule 17.06.2017
comment
Мне нравится это решение, как изменить его, чтобы использовать ContentTemplate? - person ScottFoster1000; 23.03.2019

Используя идею 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>
person Arsen Mkrtchyan    schedule 18.04.2016
comment
Я пробую ваше решение, но вижу, что ti.Content - это модель представления, а не элемент управления из шаблона. Любые идеи? - person Walter Williams; 24.06.2016
comment
Проверить, вызываете ли вы метод ItemFromContainer вместо ContainerFromItem? - person Arsen Mkrtchyan; 26.06.2016
comment
а, у вас есть DataTemplate для этой модели просмотра? - person Arsen Mkrtchyan; 27.06.2016
comment
TabControl имеет DataTemplate, определенный в свойстве TabControl.ContentTemplate. В этом шаблоне данных есть сетка, которая отображает данные в модели представления. - person Walter Williams; 27.06.2016
comment
не могли бы вы прислать мне образец, который не работает, на mkrtchyan.arsen в gmail com, я проверю - person Arsen Mkrtchyan; 29.06.2016

Я отправил ответ на аналогичный вопрос. В моем случае ручное создание TabItems решило проблему создания View снова и снова. Проверьте здесь

person Rahul W    schedule 08.01.2014