From 1d4a7923f1799934df70175d252881cf316a637f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 25 Nov 2019 22:31:22 +0100 Subject: [PATCH 1/4] Remove OperationsPerInvoke here. It was incorrect: invoking the method doesn't create 1000 buttons. --- .../Base/AvaloniaObjectInitializationBenchmark.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs index 06716f71023..d2e38850f20 100644 --- a/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs @@ -6,7 +6,7 @@ namespace Avalonia.Benchmarks.Base [MemoryDiagnoser] public class AvaloniaObjectInitializationBenchmark { - [Benchmark(OperationsPerInvoke = 1000)] + [Benchmark] public Button InitializeButton() { return new Button(); From 35bd69f23c24878f7534160b1bd6b6b7d4a12612 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 25 Nov 2019 23:01:27 +0100 Subject: [PATCH 2/4] Remove AvaloniaProperty.Initialized. It was only needed for pseudoclasses. Move pseudoclass registration to an engine that doesn't require `AvaloniaProperty.Initialized`, implemented in `PseudoclassEngine`. --- src/Avalonia.Base/AvaloniaObject.cs | 1 - src/Avalonia.Base/AvaloniaProperty.cs | 39 ------ src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 45 ------- src/Avalonia.Base/DirectPropertyBase.cs | 15 --- src/Avalonia.Base/StyledPropertyBase.cs | 15 --- src/Avalonia.Styling/PseudoclassEngine.cs | 120 ++++++++++++++++++ src/Avalonia.Styling/StyledElement.cs | 19 +-- .../AvaloniaObjectTests_Attached.cs | 17 --- .../AvaloniaObjectTests_Direct.cs | 16 --- .../AvaloniaPropertyTests.cs | 23 ---- .../DirectPropertyTests.cs | 22 ---- 11 files changed, 126 insertions(+), 206 deletions(-) create mode 100644 src/Avalonia.Styling/PseudoclassEngine.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 5bddf956162..e427c649454 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -34,7 +34,6 @@ public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyProp public AvaloniaObject() { VerifyAccess(); - AvaloniaPropertyRegistry.Instance.NotifyInitialized(this); } /// diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 8e5716a5bf6..cd9b40f94a7 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -22,7 +22,6 @@ public abstract class AvaloniaProperty : IEquatable public static readonly object UnsetValue = new UnsetValueType(); private static int s_nextId; - private readonly Subject _initialized; private readonly Subject _changed; private readonly PropertyMetadata _defaultMetadata; private readonly Dictionary _metadata; @@ -55,7 +54,6 @@ protected AvaloniaProperty( throw new ArgumentException("'name' may not contain periods."); } - _initialized = new Subject(); _changed = new Subject(); _metadata = new Dictionary(); @@ -83,7 +81,6 @@ protected AvaloniaProperty( Contract.Requires(source != null); Contract.Requires(ownerType != null); - _initialized = source._initialized; _changed = source._changed; _metadata = new Dictionary(); @@ -138,22 +135,6 @@ protected AvaloniaProperty( /// public virtual bool IsReadOnly => false; - /// - /// Gets an observable that is fired when this property is initialized on a - /// new instance. - /// - /// - /// This observable is fired each time a new is constructed - /// for all properties registered on the object's type. The default value of the property - /// for the object is passed in the args' NewValue (OldValue will always be - /// . - /// - /// - /// An observable that is fired when this property is initialized on a new - /// instance. - /// - public IObservable Initialized => _initialized; - /// /// Gets an observable that is fired when this property changes on any /// instance. @@ -482,26 +463,6 @@ public override string ToString() return Name; } - /// - /// True if has any observers. - /// - internal bool HasNotifyInitializedObservers => _initialized.HasObservers; - - /// - /// Notifies the observable. - /// - /// The object being initialized. - internal abstract void NotifyInitialized(IAvaloniaObject o); - - /// - /// Notifies the observable. - /// - /// The observable arguments. - internal void NotifyInitialized(AvaloniaPropertyChangedEventArgs e) - { - _initialized.OnNext(e); - } - /// /// Notifies the observable. /// diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 0734b64721f..50ffb515503 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -407,51 +407,6 @@ public void RegisterAttached(Type type, AvaloniaProperty property) _inheritedCache.Clear(); } - internal void NotifyInitialized(AvaloniaObject o) - { - Contract.Requires(o != null); - - var type = o.GetType(); - - if (!_initializedCache.TryGetValue(type, out var initializationData)) - { - var visited = new HashSet(); - - initializationData = new List(); - - foreach (AvaloniaProperty property in GetRegistered(type)) - { - if (property.IsDirect) - { - initializationData.Add(new PropertyInitializationData(property, (IDirectPropertyAccessor)property)); - } - else - { - initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type)); - } - - visited.Add(property); - } - - foreach (AvaloniaProperty property in GetRegisteredAttached(type)) - { - if (!visited.Contains(property)) - { - initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type)); - - visited.Add(property); - } - } - - _initializedCache.Add(type, initializationData); - } - - foreach (PropertyInitializationData data in initializationData) - { - data.Property.NotifyInitialized(o); - } - } - private readonly struct PropertyInitializationData { public AvaloniaProperty Property { get; } diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 7a0be065eb6..39ed3b084f6 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -101,21 +101,6 @@ public TValue GetUnsetValue(Type type) return (DirectPropertyMetadata)base.GetMetadata(type); } - /// - internal override void NotifyInitialized(IAvaloniaObject o) - { - if (HasNotifyInitializedObservers) - { - var e = new AvaloniaPropertyChangedEventArgs( - o, - this, - default, - InvokeGetter(o), - BindingPriority.Unset); - NotifyInitialized(e); - } - } - /// internal override void RouteClearValue(IAvaloniaObject o) { diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 8c4d683ae00..d1f961a567c 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -181,21 +181,6 @@ public override string ToString() /// object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); - /// - internal override void NotifyInitialized(IAvaloniaObject o) - { - if (HasNotifyInitializedObservers) - { - var e = new AvaloniaPropertyChangedEventArgs( - o, - this, - default, - o.GetValue(this), - BindingPriority.Unset); - NotifyInitialized(e); - } - } - /// internal override void RouteClearValue(IAvaloniaObject o) { diff --git a/src/Avalonia.Styling/PseudoclassEngine.cs b/src/Avalonia.Styling/PseudoclassEngine.cs new file mode 100644 index 00000000000..6c8f8a832ac --- /dev/null +++ b/src/Avalonia.Styling/PseudoclassEngine.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; + +namespace Avalonia +{ + internal static class PseudoclassEngine + { + private static Dictionary> _entries = new Dictionary>(); + private static Dictionary> _properties = new Dictionary>(); + private static Dictionary> _cache = new Dictionary>(); + + public static void Created(StyledElement element) + { + var t = element.GetType(); + + if (!_cache.TryGetValue(t, out var list)) + { + list = new List(); + + foreach (var i in _entries) + { + if (i.Key.IsAssignableFrom(t)) + { + list.AddRange(i.Value); + } + } + + _cache.Add(t, list); + } + + foreach (var i in list) + { + i.Update(element); + } + } + + public static void Register( + Type type, + AvaloniaProperty property, + Func selector, + string className) + { + if (!_entries.TryGetValue(type, out var list)) + { + list = new List(); + _entries.Add(type, list); + } + + var entry = new Entry(type, property, selector, className); + list.Add(entry); + + if (!_properties.TryGetValue(property, out list)) + { + list = new List(); + _properties.Add(property, list); + property.Changed.Subscribe(ValueChanged); + } + + list.Add(entry); + } + + private static void ValueChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is StyledElement element && + _properties.TryGetValue(e.Property, out var list)) + { + var t = e.Sender.GetType(); + + foreach (var i in list) + { + if (i.Type.IsAssignableFrom(t)) + { + i.Update(element); + } + } + } + } + + private interface IEntry + { + Type Type { get; } + void Update(StyledElement element); + } + + private class Entry : IEntry + { + public Entry( + Type type, + AvaloniaProperty property, + Func selector, + string className) + { + Type = type; + Property = property; + Selector = selector; + ClassName = className; + } + + public Type Type { get; } + public AvaloniaProperty Property { get; } + public Func Selector { get; } + public string ClassName { get; } + + public void Update(StyledElement element) + { + var value = element.GetValue(Property); + + if (Selector(value)) + { + ((IPseudoClasses)element.Classes).Add(ClassName); + } + else + { + ((IPseudoClasses)element.Classes).Remove(ClassName); + } + } + } + } +} diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index cc7da2b4369..90e1259ce64 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -82,6 +82,7 @@ static StyledElement() public StyledElement() { _isAttachedToLogicalTree = this is IStyleRoot; + PseudoclassEngine.Created(this); } /// @@ -557,19 +558,11 @@ protected static void PseudoClass( throw new ArgumentException("Cannot supply an empty className."); } - property.Changed.Merge(property.Initialized) - .Where(e => e.Sender is TOwner) - .Subscribe(e => - { - if (selector((TProperty)e.NewValue)) - { - ((StyledElement)e.Sender).PseudoClasses.Add(className); - } - else - { - ((StyledElement)e.Sender).PseudoClasses.Remove(className); - } - }); + PseudoclassEngine.Register( + typeof(TOwner), + property, + selector, + className); } protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs index 44e2976e038..474ef9abec4 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs @@ -16,19 +16,6 @@ public void AddOwnered_Property_Retains_Default_Value() Assert.Equal("foodefault", target.GetValue(Class2.FooProperty)); } - [Fact] - public void AvaloniaProperty_Initialized_Is_Called_For_Attached_Property() - { - bool raised = false; - - using (Class1.FooProperty.Initialized.Subscribe(x => raised = true)) - { - new Class3(); - } - - Assert.True(raised); - } - private class Base : AvaloniaObject { } @@ -46,9 +33,5 @@ private class Class2 : Base public static readonly AttachedProperty FooProperty = Class1.FooProperty.AddOwner(); } - - private class Class3 : Base - { - } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 4110c3771f7..b1a5b5ae921 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -424,22 +424,6 @@ public void Bind_Binds_AddOwnered_Property_Value_NonGeneric() Assert.Equal("second", target.Foo); } - [Fact] - public void Property_Notifies_Initialized() - { - bool raised = false; - - Class1.FooProperty.Initialized.Subscribe(e => - raised = e.Property == Class1.FooProperty && - e.OldValue == AvaloniaProperty.UnsetValue && - (string)e.NewValue == "initial" && - e.Priority == BindingPriority.Unset); - - var target = new Class1(); - - Assert.True(raised); - } - [Fact] public void Binding_Error_Reverts_To_Default_Value() { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 90b8bcff63d..6bb8dfe1f5a 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -78,24 +78,6 @@ public void OverrideMetadata_Should_Merge_Values() Assert.Equal(BindingMode.TwoWay, result.DefaultBindingMode); } - [Fact] - public void Initialized_Observable_Fired() - { - bool invoked = false; - - Class1.FooProperty.Initialized.Subscribe(x => - { - Assert.Equal(AvaloniaProperty.UnsetValue, x.OldValue); - Assert.Equal("default", x.NewValue); - Assert.Equal(BindingPriority.Unset, x.Priority); - invoked = true; - }); - - var target = new Class1(); - - Assert.True(invoked); - } - [Fact] public void Changed_Observable_Fired() { @@ -141,11 +123,6 @@ public void OverrideMetadata(PropertyMetadata metadata) OverrideMetadata(typeof(T), metadata); } - internal override void NotifyInitialized(IAvaloniaObject o) - { - throw new NotImplementedException(); - } - internal override IDisposable RouteBind( IAvaloniaObject o, IObservable> source, diff --git a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs index c2a8b03f150..8f62dc91368 100644 --- a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs @@ -1,33 +1,12 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Reactive.Subjects; -using Avalonia.Data; using Xunit; namespace Avalonia.Base.UnitTests { public class DirectPropertyTests { - [Fact] - public void Initialized_Observable_Fired() - { - bool invoked = false; - - Class1.FooProperty.Initialized.Subscribe(x => - { - Assert.Equal(AvaloniaProperty.UnsetValue, x.OldValue); - Assert.Equal("foo", x.NewValue); - Assert.Equal(BindingPriority.Unset, x.Priority); - invoked = true; - }); - - var target = new Class1(); - - Assert.True(invoked); - } - [Fact] public void IsDirect_Property_Returns_True() { @@ -69,7 +48,6 @@ public void AddOwnered_Properties_Should_Share_Observables() var p2 = p1.AddOwner(o => null, (o, v) => { }); Assert.Same(p1.Changed, p2.Changed); - Assert.Same(p1.Initialized, p2.Initialized); } private class Class1 : AvaloniaObject From b89305d708ac67a9c31270bfda8e6028d6527e3f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 26 Nov 2019 01:11:10 +0100 Subject: [PATCH 3/4] Set pseudoclasses manually. It makes creating new controls a _lot_ faster. --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 8 +- src/Avalonia.Controls/Button.cs | 25 +++- src/Avalonia.Controls/ButtonSpinner.cs | 28 +++- src/Avalonia.Controls/ControlExtensions.cs | 20 --- src/Avalonia.Controls/Expander.cs | 41 ++++-- .../WindowNotificationManager.cs | 30 ++++- src/Avalonia.Controls/Primitives/ScrollBar.cs | 24 +++- .../Primitives/ToggleButton.cs | 21 ++- src/Avalonia.Controls/Primitives/Track.cs | 28 +++- src/Avalonia.Controls/ProgressBar.cs | 44 ++++++- src/Avalonia.Controls/Slider.cs | 24 +++- src/Avalonia.Input/InputElement.cs | 42 +++++- .../Controls/PseudoClassesExtensions.cs | 28 ++++ src/Avalonia.Styling/PseudoclassEngine.cs | 120 ------------------ src/Avalonia.Styling/StyledElement.cs | 70 ---------- 15 files changed, 302 insertions(+), 251 deletions(-) create mode 100644 src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs delete mode 100644 src/Avalonia.Styling/PseudoclassEngine.cs diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 86133d5fdb2..a2b8544787b 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -373,7 +373,11 @@ private void OnAreRowGroupHeadersFrozenChanged(AvaloniaPropertyChangedEventArgs public bool IsValid { get { return _isValid; } - internal set { SetAndRaise(IsValidProperty, ref _isValid, value); } + internal set + { + SetAndRaise(IsValidProperty, ref _isValid, value); + PseudoClasses.Set(":invalid", !value); + } } public static readonly StyledProperty MaxColumnWidthProperty = @@ -656,8 +660,6 @@ static DataGrid() HorizontalScrollBarVisibilityProperty, VerticalScrollBarVisibilityProperty); - PseudoClass(IsValidProperty, x => !x, ":invalid"); - ItemsProperty.Changed.AddClassHandler((x, e) => x.OnItemsPropertyChanged(e)); CanUserResizeColumnsProperty.Changed.AddClassHandler((x, e) => x.OnCanUserResizeColumnsChanged(e)); ColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnColumnWidthChanged(e)); diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 2e115463ac8..1e0fa01cc38 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -91,7 +91,11 @@ static Button() CommandProperty.Changed.Subscribe(CommandChanged); IsDefaultProperty.Changed.Subscribe(IsDefaultChanged); IsCancelProperty.Changed.Subscribe(IsCancelChanged); - PseudoClass