Страницы

Поиск по вопросам

пятница, 20 декабря 2019 г.

Повторная установка свойств в DataTemplate при смене активной вкладки в TabControl

#c_sharp #wpf


В программе используется TabControl и каждая его вкладка представлена юзерконтролом
с таблицей DataGrid. 

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

В коде при этом ничего не изменяется. 

В чем проблема?

MainWindow:


    
        
    



UserControl:


    
        
    


    


Ответы

Ответ 1



Дело в том, что TabControl старается использовать один и тот же контрол для контента всех табов. В связи с этим, конечно, визуальное состояние теряется. Я для себя эту проблему решал так: У TabControl убрал контент, оставил только табы. Поскольку стандартный TabControl оставляет место, пришлось добавить отрицательный отступ снизу. Под TabControl'ом расположил Grid, в котором хранились все View для контента табов, показ нужного проводился через переключение Visibility. Чтобы не делать это вручную, я использовал стандартный трюк с ItemsControl'ом ListBox'ом. Вот полный код: VM-классы: class VM { } class VM1 : VM { public string Header { get { Debug.Print("VM1.Header.get"); return "Что-то"; } } public string Content { get { Debug.Print("VM1.Content.get"); return "В лесу родилась ёлочка"; } } } class VM2 : VM { public string Header { get { Debug.Print("VM2.Header.get"); return "Кто-то"; } } public string Content { get { Debug.Print("VM2.Content.get"); return "И Гена-крокодил"; } } } DataContext: new VM[] { new VM1(), new VM2() } Результат: При этом геттеры срабатывают только по разу, что можно видеть в логе.

Ответ 2



Проблема решена благодаря этому ответу на английской версии SO. Спасибо @Monk. XAML: Code-behind: public static class TabContent { public static bool GetIsCached(DependencyObject obj) { return (bool)obj.GetValue(IsCachedProperty); } public static void SetIsCached(DependencyObject obj, bool value) { obj.SetValue(IsCachedProperty, value); } /// /// Controls whether tab content is cached or not /// /// When TabContent.IsCached is true, visual state of each tab is preserved (cached), even when the tab is hidden public static readonly DependencyProperty IsCachedProperty = DependencyProperty.RegisterAttached("IsCached", typeof(bool), typeof(TabContent), new UIPropertyMetadata(false, OnIsCachedChanged)); public static DataTemplate GetTemplate(DependencyObject obj) { return (DataTemplate)obj.GetValue(TemplateProperty); } public static void SetTemplate(DependencyObject obj, DataTemplate value) { obj.SetValue(TemplateProperty, value); } /// /// Used instead of TabControl.ContentTemplate for cached tabs /// public static readonly DependencyProperty TemplateProperty = DependencyProperty.RegisterAttached("Template", typeof(DataTemplate), typeof(TabContent), new UIPropertyMetadata(null)); public static DataTemplateSelector GetTemplateSelector(DependencyObject obj) { return (DataTemplateSelector)obj.GetValue(TemplateSelectorProperty); } public static void SetTemplateSelector(DependencyObject obj, DataTemplateSelector value) { obj.SetValue(TemplateSelectorProperty, value); } /// /// Used instead of TabControl.ContentTemplateSelector for cached tabs /// public static readonly DependencyProperty TemplateSelectorProperty = DependencyProperty.RegisterAttached("TemplateSelector", typeof(DataTemplateSelector), typeof(TabContent), new UIPropertyMetadata(null)); [EditorBrowsable(EditorBrowsableState.Never)] public static TabControl GetInternalTabControl(DependencyObject obj) { return (TabControl)obj.GetValue(InternalTabControlProperty); } [EditorBrowsable(EditorBrowsableState.Never)] public static void SetInternalTabControl(DependencyObject obj, TabControl value) { obj.SetValue(InternalTabControlProperty, value); } // Using a DependencyProperty as the backing store for InternalTabControl. This enables animation, styling, binding, etc... [EditorBrowsable(EditorBrowsableState.Never)] public static readonly DependencyProperty InternalTabControlProperty = DependencyProperty.RegisterAttached("InternalTabControl", typeof(TabControl), typeof(TabContent), new UIPropertyMetadata(null, OnInternalTabControlChanged)); [EditorBrowsable(EditorBrowsableState.Never)] public static ContentControl GetInternalCachedContent(DependencyObject obj) { return (ContentControl)obj.GetValue(InternalCachedContentProperty); } [EditorBrowsable(EditorBrowsableState.Never)] public static void SetInternalCachedContent(DependencyObject obj, ContentControl value) { obj.SetValue(InternalCachedContentProperty, value); } // Using a DependencyProperty as the backing store for InternalCachedContent. This enables animation, styling, binding, etc... [EditorBrowsable(EditorBrowsableState.Never)] public static readonly DependencyProperty InternalCachedContentProperty = DependencyProperty.RegisterAttached("InternalCachedContent", typeof(ContentControl), typeof(TabContent), new UIPropertyMetadata(null)); [EditorBrowsable(EditorBrowsableState.Never)] public static object GetInternalContentManager(DependencyObject obj) { return (object)obj.GetValue(InternalContentManagerProperty); } [EditorBrowsable(EditorBrowsableState.Never)] public static void SetInternalContentManager(DependencyObject obj, object value) { obj.SetValue(InternalContentManagerProperty, value); } // Using a DependencyProperty as the backing store for InternalContentManager. This enables animation, styling, binding, etc... public static readonly DependencyProperty InternalContentManagerProperty = DependencyProperty.RegisterAttached("InternalContentManager", typeof(object), typeof(TabContent), new UIPropertyMetadata(null)); private static void OnIsCachedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { if (obj == null) return; var tabControl = obj as TabControl; if (tabControl == null) { throw new InvalidOperationException("Cannot set TabContent.IsCached on object of type " + args.NewValue.GetType().Name + ". Only objects of type TabControl can have TabContent.IsCached property."); } bool newValue = (bool)args.NewValue; if (!newValue) { if (args.OldValue != null && ((bool)args.OldValue)) { throw new NotImplementedException("Cannot change TabContent.IsCached from True to False. Turning tab caching off is not implemented"); } return; } EnsureContentTemplateIsNull(tabControl); tabControl.ContentTemplate = CreateContentTemplate(); } private static DataTemplate CreateContentTemplate() { const string xaml = ""; var context = new ParserContext(); context.XamlTypeMapper = new XamlTypeMapper(new string[0]); context.XamlTypeMapper.AddMappingProcessingInstruction("b", typeof(TabContent).Namespace, typeof(TabContent).Assembly.FullName); context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation"); context.XmlnsDictionary.Add("b", "b"); var template = (DataTemplate)XamlReader.Parse(xaml, context); return template; } private static void OnInternalTabControlChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { if (obj == null) return; var container = obj as Decorator; if (container == null) { var message = "Cannot set TabContent.InternalTabControl on object of type " + obj.GetType().Name + ". Only controls that derive from Decorator, such as Border can have a TabContent.InternalTabControl."; throw new InvalidOperationException(message); } if (args.NewValue == null) return; if (!(args.NewValue is TabControl)) { throw new InvalidOperationException("Value of TabContent.InternalTabControl cannot be of type " + args.NewValue.GetType().Name + ", it must be of type TabControl"); } var tabControl = (TabControl)args.NewValue; var contentManager = GetContentManager(tabControl, container); contentManager.UpdateSelectedTab(); } private static ContentManager GetContentManager(TabControl tabControl, Decorator container) { var contentManager = (ContentManager)GetInternalContentManager(tabControl); if (contentManager != null) { /* * Content manager already exists for the tab control. This means that tab content template is applied * again, and new instance of the Border control (container) has been created. The old container * referenced by the content manager is no longer visible and needs to be replaced */ contentManager.ReplaceContainer(container); } else { // create content manager for the first time contentManager = new ContentManager(tabControl, container); SetInternalContentManager(tabControl, contentManager); } return contentManager; } private static void EnsureContentTemplateIsNull(TabControl tabControl) { if (tabControl.ContentTemplate != null) { throw new InvalidOperationException("TabControl.ContentTemplate value is not null. If TabContent.IsCached is True, use TabContent.Template instead of ContentTemplate"); } } public class ContentManager { TabControl _tabControl; Decorator _border; public ContentManager(TabControl tabControl, Decorator border) { _tabControl = tabControl; _border = border; _tabControl.SelectionChanged += (sender, args) => { UpdateSelectedTab(); }; } public void ReplaceContainer(Decorator newBorder) { if (Object.ReferenceEquals(_border, newBorder)) return; _border.Child = null; // detach any tab content that old border may hold _border = newBorder; } public void UpdateSelectedTab() { _border.Child = GetCurrentContent(); } private ContentControl GetCurrentContent() { var item = _tabControl.SelectedItem; if (item == null) return null; var tabItem = _tabControl.ItemContainerGenerator.ContainerFromItem(item); if (tabItem == null) return null; var cachedContent = TabContent.GetInternalCachedContent(tabItem); if (cachedContent == null) { cachedContent = new ContentControl { DataContext = item, ContentTemplate = TabContent.GetTemplate(_tabControl), ContentTemplateSelector = TabContent.GetTemplateSelector(_tabControl) }; cachedContent.SetBinding(ContentControl.ContentProperty, new Binding()); TabContent.SetInternalCachedContent(tabItem, cachedContent); } return cachedContent; } } }

Ответ 3



Дополнение к ответу @VladD. Чтобы при старте приложения первая вкладка по умолчанию была выбрана, в MainWindow делаем установку Loaded += (s, e) => TabControlAbove.SelectedIndex = 0;. Также нужно обработать Ctrl+Click по содержимому вкладки, иначе будет срабатывать снятие выделения с текущего ListBoxItem и содержимое пропадет. Для этого вешаем на ListBox обработчик SelectionChanged="ListBox_SelectionChanged", в котором проверяем, что добавляемый элемент есть: private bool cancellingTabSelectionChange; private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (e.AddedItems.Count == 0 && !cancellingTabSelectionChange) { cancellingTabSelectionChange = true; ((ListBox)sender).SelectedItem = e.RemovedItems[0]; cancellingTabSelectionChange = false; } }

Комментариев нет:

Отправить комментарий