Как сохранить состояние управления в элементах табуляции в TabControl
Я новичок в WPF, пытаясь построить проект, который следует рекомендациям превосходной статьи Джоша Смита, описывающей шаблон дизайна Model-View-ViewModel .
Используя пример кода Джоша в качестве базы, я создал простое приложение, которое содержит ряд «рабочих пространств», каждый из которых представлен вкладкой TabControl. В моем приложении рабочая область – это редактор документов, который позволяет управлять иерархическим документом через элемент управления TreeView.
Хотя мне удалось открыть несколько рабочих областей и просмотреть их содержимое в связанном элементе управления TreeView, я обнаружил, что TreeView «забывает» его состояние при переключении между вкладками. Например, если TreeView в Tab1 частично расширен, он будет показан как полностью свернутый после перехода на Tab2 и возврата к Tab1. Это поведение, по-видимому, относится ко всем аспектам состояния управления для всех элементов управления.
- Как получить список коллекции значений из app.config в WPF?
- Есть ли хороший способ конвертировать между BitmapSource и Bitmap?
- Установите элемент управления веб-браузером WPF для использования режима IE10
- WPF показывает диалог перед основным окном
- Bubbling прокручивает события из ListView в родительский
После некоторых экспериментов я понял, что я могу сохранить состояние в TabItem, явно привязывая каждое свойство состояния управления к выделенному свойству в базовом ViewModel. Однако это похоже на дополнительную работу, когда я просто хочу, чтобы все мои элементы управления запоминали их состояние при переключении между рабочими пространствами.
Я предполагаю, что мне не хватает чего-то простого, но я не уверен, где искать ответ. Любое руководство будет высоко оценено.
Спасибо, Тим
Обновить:
В соответствии с просьбой я попытаюсь опубликовать код, демонстрирующий эту проблему. Однако, поскольку данные, лежащие в основе TreeView, сложны, я опубликую упрощенный пример, демонстрирующий те же самые симтомы. Вот XAML из главного windows:
Вышеупомянутый XAML корректно связывается с ObservableCollection DocumentViewModel, посредством чего каждый член представляется через DocumentView.
Для простоты этого примера я удалил TreeView (упомянутый выше) из DocumentView и заменил его TabControl, содержащим 3 фиксированных вкладки:
В этом случае между DocumentView и DocumentViewModel нет привязки. Когда код запускается, внутренний TabControl не может запомнить его выбор при переключении внешнего TabControl.
Однако, если я явно привязываю внутреннее свойство SelectedIndex TabControl …
… к соответствующему фиктивному свойству в DocumentViewModel …
public int SelecteDocumentIndex { get; set; }
… внутренняя вкладка может запомнить ее выбор.
Я понимаю, что могу эффективно решить свою проблему, применив эту технику к каждому визуальному свойству каждого элемента управления, но я надеюсь, что есть более элегантное решение.
- ComboBox.SelectedValue не обновляется из источника привязки
- Как должна ViewModel закрыть форму?
- В чем разница между Width и ActualWidth в WPF?
- Динамическое добавление элементов управления в WPF MVVM
- Загрузите BitmapImage WPF из System.Drawing.Bitmap
- Вызов события двойного щелчка из элемента списка элементов WPF с использованием MVVM
- Каков наилучший подход к печати / отчетности из WPF?
- Использование диспетчера C #
Пример использования Writer приложения WPF Application Framework (WAF) показывает, как решить вашу проблему. Он создает новый UserControl для каждого TabItem. Таким образом, состояние сохраняется, когда пользователь меняет активную вкладку.
Я получил решение с этим советом WPF TabControl: Turning Off Tab Virtualization
на http://www.codeproject.com/Articles/460989/WPF-TabControl-Turning-Off-Tab-Virtualization это class TabContent с свойством IsCached.
У меня была та же проблема, и я нашел хорошее решение, которое вы можете использовать, как обычный TabControl
насколько я его тестировал. В случае, если для вас здесь важна Текущая лицензия
Здесь Код на случай, когда ссылка идет вниз:
using System; using System.Collections.Specialized; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; namespace CefSharp.Wpf.Example.Controls { /// /// Extended TabControl which saves the displayed item so you don't get the performance hit of /// unloading and reloading the VisualTree when switching tabs /// /// /// 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 /// [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; } /// /// If containers are done, generate the selected item /// /// The sender. /// The instance containing the event data. private void ItemContainerGeneratorStatusChanged(object sender, EventArgs e) { if (ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) { ItemContainerGenerator.StatusChanged -= ItemContainerGeneratorStatusChanged; UpdateSelectedItem(); } } /// /// Get the ItemsHolder and generate any children /// public override void OnApplyTemplate() { base.OnApplyTemplate(); itemsHolderPanel = GetTemplateChild("PART_ItemsHolder") as Panel; UpdateSelectedItem(); } /// /// When the items change we remove any generated panel children and add any new ones as necessary /// /// The instance containing the event data. 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.
Используя идею WAF, я прихожу к этому простому решению, которое, похоже, решает проблему.
Я использую Interactivity Behavior, но то же самое можно сделать с прикрепленным свойством, если библиотека Interactivity не указана
/// /// Wraps tab item contents in UserControl to prevent TabControl from re-using its content /// public class TabControlUcWrapperBehavior : Behavior { 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 }; } } }
И использование
Основываясь на ответе @ Arsen выше, вот еще одно поведение:
- Не требует дополнительных ссылок. (если вы не поместили код во внешнюю библиотеку)
- Он не использует базовый class.
- Он обрабатывает как сброс, так и добавление изменений коллекции.
Использовать его
Объявите пространство имен в xaml:
Обновите стиль:
Или напрямую обновите TabControl:
И вот код для поведения:
using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Windows; using System.Windows.Controls; namespace My.Behaviors { /// /// Wraps tab item contents in UserControl to prevent TabControl from re-using its content /// public class TabControlBehavior { private static readonly HashSet _tabControls = new HashSet (); private static readonly Dictionary _tabControlItemCollections = new Dictionary(); 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 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 }; } } } } }
Я отправил ответ на аналогичный вопрос. В моем случае вручную создание TabItems решило проблему создания представления снова и снова. Проверить здесь