From d8191bc80b2210bb2278e3b396db44b19c1c10cc Mon Sep 17 00:00:00 2001 From: Jonas Arnold Date: Wed, 10 May 2023 14:06:22 +0200 Subject: [PATCH] implemented caching behaviour for tab, disables virtualization on tab controls, cleaned up format of MultiFormatTextBox --- .../Behaviours/TabContent.cs | 281 ++++++++++++++++++ .../MultiFormatTextBox/Format.cs | 8 +- .../MultiFormatTextBox/MultiFormatTextBox.cs | 40 ++- .../MultiTerm.Wpf.CustomControl.csproj | 4 + MultiTerm.Wpf/View/ShellView.xaml | 15 +- 5 files changed, 324 insertions(+), 24 deletions(-) create mode 100644 MultiTerm.Wpf.CustomControl/Behaviours/TabContent.cs diff --git a/MultiTerm.Wpf.CustomControl/Behaviours/TabContent.cs b/MultiTerm.Wpf.CustomControl/Behaviours/TabContent.cs new file mode 100644 index 0000000..ee6b3c2 --- /dev/null +++ b/MultiTerm.Wpf.CustomControl/Behaviours/TabContent.cs @@ -0,0 +1,281 @@ +// TabContent.cs, version 1.2 +// The code in this file is Copyright (c) Ivan Krivyakov +// See http://www.ikriv.com/legal.php for more information +// +using System; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Markup; + +namespace MultiTerm.Wpf.CustomControl; + +/// +/// Attached properties for persistent tab control +/// +/// By default WPF TabControl bound to an ItemsSource destroys visual state of invisible tabs. +/// Set ikriv:TabContent.IsCached="True" to preserve visual state of each tab. +/// +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(); + EnsureContentTemplateIsNotModified(tabControl); + } + + 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"); + } + } + + private static void EnsureContentTemplateIsNotModified(TabControl tabControl) + { + var descriptor = DependencyPropertyDescriptor.FromProperty(TabControl.ContentTemplateProperty, typeof(TabControl)); + descriptor.AddValueChanged(tabControl, (sender, args) => + { + throw new InvalidOperationException("Cannot assign to TabControl.ContentTemplate when TabContent.IsCached is True. Use TabContent.Template instead"); + }); + } + + 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; + } + } +} diff --git a/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/Format.cs b/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/Format.cs index e42b3fe..9e29a0f 100644 --- a/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/Format.cs +++ b/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/Format.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Windows.Input; using System.Windows.Media; using System.Windows; using MultiTerm.Core.Types; @@ -15,21 +14,18 @@ internal class Format public string Name { get; set; } public Brush BackgroundBrush { get; set; } public FormatType AssociatedFormatType { get; set; } - public Predicate IsKeyValid { get; set; } - public Format(string name, Brush backgroundBrush, FormatType associatedFormatType, Predicate keyValidator) + public Format(string name, Brush backgroundBrush, FormatType associatedFormatType) { this.Name = name; this.BackgroundBrush = backgroundBrush; this.AssociatedFormatType = associatedFormatType; - this.IsKeyValid = keyValidator; } - public Format(string name, string backgroundColorResourceName, FormatType associatedFormatType, Predicate keyValidator) + public Format(string name, string backgroundColorResourceName, FormatType associatedFormatType) { this.Name = name; this.backgroundColorResourceName = backgroundColorResourceName; this.BackgroundBrush = Brushes.White; // set background brush to white this.AssociatedFormatType = associatedFormatType; - this.IsKeyValid = keyValidator; } public static List GetListOfNames(IEnumerable formats) { diff --git a/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/MultiFormatTextBox.cs b/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/MultiFormatTextBox.cs index 626ea8f..b25b70d 100644 --- a/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/MultiFormatTextBox.cs +++ b/MultiTerm.Wpf.CustomControl/MultiFormatTextBox/MultiFormatTextBox.cs @@ -19,15 +19,12 @@ public class MultiFormatTextBox : Control private static readonly Brush defaultBackgroundBrush = Brushes.White; private static readonly List formats = new() { - // character input, accepts all keys - new Format("CHAR", "MultiFormatTextBox.CHAR.Background", FormatType.Character, - delegate(Key k) { return true; }), - // hex input, ignores all keys that are not inbetween 0 and F - new Format("HEX", "MultiFormatTextBox.HEX.Background", FormatType.Hexadecimal, - delegate(Key k) { return (k >= Key.D0 && k <= Key.F); }), - // binary input, ignores all keys except 0 and 1 - new Format("BIN", "MultiFormatTextBox.BIN.Background", FormatType.Binary, - delegate(Key k) { return (k == Key.D0 || k == Key.D1); }) + // character input + new Format("CHAR", "MultiFormatTextBox.CHAR.Background", FormatType.Character), + // hex input + new Format("HEX", "MultiFormatTextBox.HEX.Background", FormatType.Hexadecimal), + // binary input + new Format("BIN", "MultiFormatTextBox.BIN.Background", FormatType.Binary) }; #endregion @@ -46,9 +43,11 @@ public class MultiFormatTextBox : Control #region Dependency Properties public static readonly DependencyProperty CurrentMultiFormatStringProperty = - DependencyProperty.Register("CurrentMultiFormatString", - typeof(MultiFormatString), typeof(MultiFormatTextBox), - new PropertyMetadata(null, OnCurrentMultiFormatStringChanged)); + DependencyProperty.Register( + name: "CurrentMultiFormatString", + propertyType: typeof(MultiFormatString), + ownerType: typeof(MultiFormatTextBox), + typeMetadata: new FrameworkPropertyMetadata(null, OnCurrentMultiFormatStringChanged)); public static readonly RoutedEvent EnterPressedEvent; @@ -79,7 +78,13 @@ public class MultiFormatTextBox : Control EnterPressedEvent = EventManager.RegisterRoutedEvent("EnterPressed", RoutingStrategy.Bubble, typeof(RoutedEventArgs), typeof(MultiFormatTextBox)); + } + ~MultiFormatTextBox() + { + // unregister events + var incc = this.CurrentMultiFormatString as INotifyCollectionChanged; + incc.CollectionChanged -= this.MultiFormatString_CollectionChanged; } public override void OnApplyTemplate() @@ -126,12 +131,17 @@ public class MultiFormatTextBox : Control { // extract instance and guard null if (d is not MultiFormatTextBox mftb) { return; } - if (e.NewValue is not MultiFormatString newString) { return; } // register to collection changed event if (mftb.CurrentMultiFormatString is INotifyCollectionChanged incc) - { - incc.CollectionChanged += mftb.MultiFormatString_CollectionChanged; + { + // remove handler + incc.CollectionChanged -= mftb.MultiFormatString_CollectionChanged; + // add new handler if not null + if(e.NewValue != null) + { + incc.CollectionChanged += mftb.MultiFormatString_CollectionChanged; + } } } diff --git a/MultiTerm.Wpf.CustomControl/MultiTerm.Wpf.CustomControl.csproj b/MultiTerm.Wpf.CustomControl/MultiTerm.Wpf.CustomControl.csproj index e29b1fc..b8f9a24 100644 --- a/MultiTerm.Wpf.CustomControl/MultiTerm.Wpf.CustomControl.csproj +++ b/MultiTerm.Wpf.CustomControl/MultiTerm.Wpf.CustomControl.csproj @@ -14,4 +14,8 @@ + + + + diff --git a/MultiTerm.Wpf/View/ShellView.xaml b/MultiTerm.Wpf/View/ShellView.xaml index 6a6a6a9..9063ad0 100644 --- a/MultiTerm.Wpf/View/ShellView.xaml +++ b/MultiTerm.Wpf/View/ShellView.xaml @@ -131,18 +131,27 @@ - - + + + + + + + + + + -