From c5e4996da22376c9bc2c16c683b6f9ca60de48f8 Mon Sep 17 00:00:00 2001 From: mstr2 Date: Wed, 2 Jan 2019 21:16:08 +0100 Subject: [PATCH 1/9] Improved performance of value lookup in AvaloniaObject's ValueStore --- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 16 +- src/Avalonia.Base/ValueStore.cs | 156 ++++++++++++++++-- 2 files changed, 156 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index e29e7339ae..7beab5f497 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -13,6 +13,8 @@ namespace Avalonia /// public class AvaloniaPropertyRegistry { + private readonly Dictionary _allProperties = + new Dictionary(); private readonly Dictionary> _registered = new Dictionary>(); private readonly Dictionary> _attached = @@ -148,6 +150,16 @@ namespace Avalonia return FindRegistered(o.GetType(), name); } + /// + /// Finds a registered property by Id. + /// + /// The property Id. + /// The registered property or null if no matching property found. + public AvaloniaProperty FindRegistered(int id) + { + return _allProperties.TryGetValue(id, out var value) ? value : null; + } + /// /// Checks whether a is registered on a type. /// @@ -202,7 +214,8 @@ namespace Avalonia { inner.Add(property.Id, property); } - + + _allProperties[property.Id] = property; _registeredCache.Clear(); } @@ -238,6 +251,7 @@ namespace Avalonia inner.Add(property.Id, property); } + _allProperties[property.Id] = property; _attachedCache.Clear(); } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index adbe89aceb..f78aaab221 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -7,13 +7,21 @@ namespace Avalonia { internal class ValueStore : IPriorityValueOwner { + struct Entry + { + internal int PropertyId; + internal object Value; + } + private readonly AvaloniaObject _owner; - private readonly Dictionary _values = - new Dictionary(); + private Entry[] _entries; public ValueStore(AvaloniaObject owner) { _owner = owner; + + // The last item in the list is always int.MaxValue + _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } }; } public IDisposable AddBinding( @@ -23,7 +31,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (_values.TryGetValue(property, out var v)) + if (TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -31,13 +39,13 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _values[property] = priorityValue; + SetValueInternal(property, priorityValue); } } else { priorityValue = CreatePriorityValue(property); - _values.Add(property, priorityValue); + AddValueInternal(property, priorityValue); } return priorityValue.Add(source, (int)priority); @@ -47,7 +55,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (_values.TryGetValue(property, out var v)) + if (TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -55,7 +63,7 @@ namespace Avalonia { if (priority == (int)BindingPriority.LocalValue) { - _values[property] = Validate(property, value); + SetValueInternal(property, Validate(property, value)); Changed(property, priority, v, value); return; } @@ -63,7 +71,7 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _values[property] = priorityValue; + SetValueInternal(property, priorityValue); } } } @@ -76,14 +84,14 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - _values.Add(property, Validate(property, value)); + AddValueInternal(property, Validate(property, value)); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } else { priorityValue = CreatePriorityValue(property); - _values.Add(property, priorityValue); + AddValueInternal(property, priorityValue); } } @@ -100,13 +108,22 @@ namespace Avalonia _owner.PriorityValueChanged(property, priority, oldValue, newValue); } - public IDictionary GetSetValues() => _values; + public IDictionary GetSetValues() + { + var dict = new Dictionary(_entries.Length - 1); + for (int i = 0; i < _entries.Length - 1; ++i) + { + dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); + } + + return dict; + } public object GetValue(AvaloniaProperty property) { var result = AvaloniaProperty.UnsetValue; - if (_values.TryGetValue(property, out var value)) + if (TryGetValue(property, out var value)) { result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; } @@ -116,12 +133,12 @@ namespace Avalonia public bool IsAnimating(AvaloniaProperty property) { - return _values.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; + return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; } public bool IsSet(AvaloniaProperty property) { - if (_values.TryGetValue(property, out var value)) + if (TryGetValue(property, out var value)) { return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; } @@ -131,7 +148,7 @@ namespace Avalonia public void Revalidate(AvaloniaProperty property) { - if (_values.TryGetValue(property, out var value)) + if (TryGetValue(property, out var value)) { (value as PriorityValue)?.Revalidate(); } @@ -178,5 +195,114 @@ namespace Avalonia (_deferredSetter = new DeferredSetter()); } } + + private bool TryGetValue(AvaloniaProperty property, out object value) + { + (int index, bool found) = TryFindEntry(property.Id); + if (!found) + { + value = null; + return false; + } + + value = _entries[index].Value; + return true; + } + + private void AddValueInternal(AvaloniaProperty property, object value) + { + Entry[] entries = new Entry[_entries.Length + 1]; + + for (int i = 0; i < _entries.Length; ++i) + { + if (_entries[i].PropertyId > property.Id) + { + if (i > 0) + { + Array.Copy(_entries, 0, entries, 0, i); + } + + entries[i] = new Entry { PropertyId = property.Id, Value = value }; + Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); + break; + } + } + + _entries = entries; + } + + private void SetValueInternal(AvaloniaProperty property, object value) + { + _entries[TryFindEntry(property.Id).Item1].Value = value; + } + + private (int, bool) TryFindEntry(int propertyId) + { + if (_entries.Length <= 20) + { + // For small lists, we use an optimized linear search. Since the last item in the list + // is always int.MaxValue, we can skip a conditional branch in each iteration. + // By unrolling the loop, we can skip another unconditional branch in each iteration. + + if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId); + if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId); + if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId); + if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId); + if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId); + if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId); + if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId); + if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId); + if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId); + if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId); + if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId); + if (_entries[11].PropertyId >= propertyId) return (11, _entries[11].PropertyId == propertyId); + if (_entries[12].PropertyId >= propertyId) return (12, _entries[12].PropertyId == propertyId); + if (_entries[13].PropertyId >= propertyId) return (13, _entries[13].PropertyId == propertyId); + if (_entries[14].PropertyId >= propertyId) return (14, _entries[14].PropertyId == propertyId); + if (_entries[15].PropertyId >= propertyId) return (15, _entries[15].PropertyId == propertyId); + if (_entries[16].PropertyId >= propertyId) return (16, _entries[16].PropertyId == propertyId); + if (_entries[17].PropertyId >= propertyId) return (17, _entries[17].PropertyId == propertyId); + if (_entries[18].PropertyId >= propertyId) return (18, _entries[18].PropertyId == propertyId); + } + else + { + int low = 0; + int high = _entries.Length; + int id; + + if (high > 0) + { + while (high - low > 3) + { + int pivot = (high + low) / 2; + id = _entries[pivot].PropertyId; + + if (propertyId == id) + return (pivot, true); + + if (propertyId <= id) + high = pivot; + else + low = pivot + 1; + } + + do + { + id = _entries[low].PropertyId; + + if (id == propertyId) + return (low, true); + + if (id > propertyId) + break; + + ++low; + } + while (low < high); + } + } + + return (0, false); + } } } From 814222a15dadda3442b8c970da70e401ad313d87 Mon Sep 17 00:00:00 2001 From: mstr2 Date: Wed, 9 Jan 2019 22:21:40 +0100 Subject: [PATCH 2/9] Reduced linear search, made FindRegistered method internal --- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 2 +- src/Avalonia.Base/ValueStore.cs | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 7beab5f497..dc75727941 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -155,7 +155,7 @@ namespace Avalonia /// /// The property Id. /// The registered property or null if no matching property found. - public AvaloniaProperty FindRegistered(int id) + internal AvaloniaProperty FindRegistered(int id) { return _allProperties.TryGetValue(id, out var value) ? value : null; } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index f78aaab221..7dad72b551 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -7,7 +7,7 @@ namespace Avalonia { internal class ValueStore : IPriorityValueOwner { - struct Entry + private struct Entry { internal int PropertyId; internal object Value; @@ -238,7 +238,7 @@ namespace Avalonia private (int, bool) TryFindEntry(int propertyId) { - if (_entries.Length <= 20) + if (_entries.Length <= 16) { // For small lists, we use an optimized linear search. Since the last item in the list // is always int.MaxValue, we can skip a conditional branch in each iteration. @@ -259,10 +259,6 @@ namespace Avalonia if (_entries[12].PropertyId >= propertyId) return (12, _entries[12].PropertyId == propertyId); if (_entries[13].PropertyId >= propertyId) return (13, _entries[13].PropertyId == propertyId); if (_entries[14].PropertyId >= propertyId) return (14, _entries[14].PropertyId == propertyId); - if (_entries[15].PropertyId >= propertyId) return (15, _entries[15].PropertyId == propertyId); - if (_entries[16].PropertyId >= propertyId) return (16, _entries[16].PropertyId == propertyId); - if (_entries[17].PropertyId >= propertyId) return (17, _entries[17].PropertyId == propertyId); - if (_entries[18].PropertyId >= propertyId) return (18, _entries[18].PropertyId == propertyId); } else { From 5cbe89e9d6d6ba51acfb4aa7b3881f2f8158119b Mon Sep 17 00:00:00 2001 From: mstr2 Date: Wed, 16 Jan 2019 20:01:38 +0100 Subject: [PATCH 3/9] Switched AvaloniaPropertyRegistry._properties from Dictionary to List --- src/Avalonia.Base/AvaloniaProperty.cs | 2 +- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 10 ++-- src/Avalonia.Base/ValueStore.cs | 47 ++++++++----------- 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 4b0116a536..953132116c 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -21,7 +21,7 @@ namespace Avalonia /// public static readonly object UnsetValue = new Unset(); - private static int s_nextId = 1; + private static int s_nextId; private readonly Subject _initialized; private readonly Subject _changed; private readonly PropertyMetadata _defaultMetadata; diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index dc75727941..11b1096052 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -13,8 +13,8 @@ namespace Avalonia /// public class AvaloniaPropertyRegistry { - private readonly Dictionary _allProperties = - new Dictionary(); + private readonly IList _properties = + new List(); private readonly Dictionary> _registered = new Dictionary>(); private readonly Dictionary> _attached = @@ -157,7 +157,7 @@ namespace Avalonia /// The registered property or null if no matching property found. internal AvaloniaProperty FindRegistered(int id) { - return _allProperties.TryGetValue(id, out var value) ? value : null; + return id < _properties.Count ? _properties[id] : null; } /// @@ -215,7 +215,7 @@ namespace Avalonia inner.Add(property.Id, property); } - _allProperties[property.Id] = property; + _properties.Add(property); _registeredCache.Clear(); } @@ -251,7 +251,7 @@ namespace Avalonia inner.Add(property.Id, property); } - _allProperties[property.Id] = property; + _properties.Add(property); _attachedCache.Clear(); } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 7dad72b551..d520e2b80a 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -238,7 +238,7 @@ namespace Avalonia private (int, bool) TryFindEntry(int propertyId) { - if (_entries.Length <= 16) + if (_entries.Length <= 12) { // For small lists, we use an optimized linear search. Since the last item in the list // is always int.MaxValue, we can skip a conditional branch in each iteration. @@ -255,10 +255,6 @@ namespace Avalonia if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId); if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId); if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId); - if (_entries[11].PropertyId >= propertyId) return (11, _entries[11].PropertyId == propertyId); - if (_entries[12].PropertyId >= propertyId) return (12, _entries[12].PropertyId == propertyId); - if (_entries[13].PropertyId >= propertyId) return (13, _entries[13].PropertyId == propertyId); - if (_entries[14].PropertyId >= propertyId) return (14, _entries[14].PropertyId == propertyId); } else { @@ -266,36 +262,33 @@ namespace Avalonia int high = _entries.Length; int id; - if (high > 0) + while (high - low > 3) { - while (high - low > 3) - { - int pivot = (high + low) / 2; - id = _entries[pivot].PropertyId; + int pivot = (high + low) / 2; + id = _entries[pivot].PropertyId; - if (propertyId == id) - return (pivot, true); + if (propertyId == id) + return (pivot, true); - if (propertyId <= id) - high = pivot; - else - low = pivot + 1; - } + if (propertyId <= id) + high = pivot; + else + low = pivot + 1; + } - do - { - id = _entries[low].PropertyId; + do + { + id = _entries[low].PropertyId; - if (id == propertyId) - return (low, true); + if (id == propertyId) + return (low, true); - if (id > propertyId) - break; + if (id > propertyId) + break; - ++low; - } - while (low < high); + ++low; } + while (low < high); } return (0, false); From 81846e87ece5f0a6c5e7a9146a46a941fdf79a29 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 24 Jan 2019 19:11:39 +0100 Subject: [PATCH 4/9] Added TopLevel.Opened event. And raise the event when a window is opened. --- src/Avalonia.Controls/TopLevel.cs | 11 +++++++++++ src/Avalonia.Controls/Window.cs | 2 ++ src/Avalonia.Controls/WindowBase.cs | 3 ++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 5ca3647da7..32c40847c5 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -136,6 +136,11 @@ namespace Avalonia.Controls } } + /// + /// Fired when the window is opened. + /// + public event EventHandler Opened; + /// /// Fired when the window is closed. /// @@ -311,6 +316,12 @@ namespace Avalonia.Controls $"Control '{GetType().Name}' is a top level control and cannot be added as a child."); } + /// + /// Raises the event. + /// + /// The event args. + protected virtual void OnOpened(EventArgs e) => Opened?.Invoke(this, e); + /// /// Tries to get a service from an , logging a /// warning if not found. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 53f727900b..f5af6774b5 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -389,6 +389,7 @@ namespace Avalonia.Controls Renderer?.Start(); } SetWindowStartupLocation(Owner?.PlatformImpl); + OnOpened(EventArgs.Empty); } /// @@ -458,6 +459,7 @@ namespace Avalonia.Controls owner.Activate(); result.SetResult((TResult)(_dialogResult ?? default(TResult))); }); + OnOpened(EventArgs.Empty); } SetWindowStartupLocation(owner); diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 2fba8619c6..56ffd315f1 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -163,7 +163,7 @@ namespace Avalonia.Controls } /// - /// Shows the popup. + /// Shows the window. /// public virtual void Show() { @@ -181,6 +181,7 @@ namespace Avalonia.Controls } PlatformImpl?.Show(); Renderer?.Start(); + OnOpened(EventArgs.Empty); } finally { From 6a167a688271e37b0e3c5ce34655dc3699ccc8bc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 24 Jan 2019 19:11:57 +0100 Subject: [PATCH 5/9] Use Window.Opened event for rxui activation. --- src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs b/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs index cf386a235e..e1db604e95 100644 --- a/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs +++ b/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs @@ -29,8 +29,8 @@ namespace Avalonia { var windowLoaded = Observable .FromEventPattern( - x => window.Initialized += x, - x => window.Initialized -= x) + x => window.Opened += x, + x => window.Opened -= x) .Select(args => true); var windowUnloaded = Observable .FromEventPattern( @@ -59,4 +59,4 @@ namespace Avalonia .DistinctUntilChanged(); } } -} \ No newline at end of file +} From 068acb63769a2a8636715d7a2a4396b93d19db8c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 24 Jan 2019 19:18:12 +0100 Subject: [PATCH 6/9] Added TopLevel.Opened unit tests. --- .../WindowBaseTests.cs | 16 ++++++++++++++ .../WindowTests.cs | 21 +++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 51a4d21392..6d00409ae0 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -199,6 +199,22 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Showing_Should_Raise_Opened() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new TestWindowBase(); + var raised = false; + + target.Opened += (s, e) => raised = true; + + target.Show(); + + Assert.True(raised); + } + } + [Fact] public void Hiding_Should_Stop_Renderer() { diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index c0b5342934..8221dadc86 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -228,18 +228,35 @@ namespace Avalonia.Controls.UnitTests [Fact] public void ShowDialog_Should_Start_Renderer() { - using (UnitTestApplication.Start(TestServices.StyledWindow)) { + var parent = Mock.Of(); var renderer = new Mock(); var target = new Window(CreateImpl(renderer)); - target.Show(); + target.ShowDialog(parent); renderer.Verify(x => x.Start(), Times.Once); } } + [Fact] + public void ShowDialog_Should_Raise_Opened() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = Mock.Of(); + var target = new Window(); + var raised = false; + + target.Opened += (s, e) => raised = true; + + target.ShowDialog(parent); + + Assert.True(raised); + } + } + [Fact] public void Hiding_Should_Stop_Renderer() { From 163abb8322baa55741a00a477422e0b21879208a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 25 Jan 2019 12:54:29 +0100 Subject: [PATCH 7/9] Added Avalonia.ReactiveUI.UnitTests to solution. --- Avalonia.sln | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index 2f7560049c..d6472503fe 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -196,9 +196,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "nukebuild\_build. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Animation.UnitTests", "tests\Avalonia.Animation.UnitTests\Avalonia.Animation.UnitTests.csproj", "{AF227847-E65C-4BE9-BCE9-B551357788E0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{41B02319-965D-4945-8005-C1A3D1224165}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{41B02319-965D-4945-8005-C1A3D1224165}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -1819,6 +1821,30 @@ Global {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhone.Build.0 = Release|Any CPU {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhone.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhone.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.Build.0 = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhone.ActiveCfg = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhone.Build.0 = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1875,6 +1901,7 @@ Global {AF227847-E65C-4BE9-BCE9-B551357788E0} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} From 653fa458c16d8734b6d918c14a9e810bad76e026 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 25 Jan 2019 12:55:16 +0100 Subject: [PATCH 8/9] Call InitializeComponent in RxUI activation tests. To make sure activation works after loading XAML. --- .../AvaloniaActivationForViewFetcherTest.cs | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index b782311729..70a5504a7d 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -10,6 +10,7 @@ using ReactiveUI; using DynamicData; using Xunit; using Splat; +using Avalonia.Markup.Xaml; namespace Avalonia { @@ -70,12 +71,40 @@ namespace Avalonia public class ActivatableWindow : ReactiveWindow { - public ActivatableWindow() => this.WhenActivated(disposables => { }); + public ActivatableWindow() + { + InitializeComponent(); + Assert.IsType(Content); + this.WhenActivated(disposables => { }); + } + + private void InitializeComponent() + { + var loader = new AvaloniaXamlLoader(); + loader.Load(@" + + +", null, this); + } } public class ActivatableUserControl : ReactiveUserControl { - public ActivatableUserControl() => this.WhenActivated(disposables => { }); + public ActivatableUserControl() + { + InitializeComponent(); + Assert.IsType(Content); + this.WhenActivated(disposables => { }); + } + + private void InitializeComponent() + { + var loader = new AvaloniaXamlLoader(); + loader.Load(@" + + +", null, this); + } } public AvaloniaActivationForViewFetcherTest() @@ -183,4 +212,4 @@ namespace Avalonia Assert.False(viewModel.IsActivated); } } -} \ No newline at end of file +} From 233adc9ca5253e16ea5bc76f8984d922e5770e09 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 25 Jan 2019 17:51:48 +0100 Subject: [PATCH 9/9] Added `:not()` style selector. --- src/Avalonia.Styling/Styling/NotSelector.cs | 72 +++++++++++ src/Avalonia.Styling/Styling/Selectors.cs | 11 ++ .../Markup/Parsers/SelectorGrammar.cs | 51 +++++++- .../Markup/Parsers/SelectorParser.cs | 13 +- .../Parsers/SelectorGrammarTests.cs | 73 +++++++++++ .../Xaml/StyleTests.cs | 28 +++++ .../SelectorTests_Not.cs | 114 ++++++++++++++++++ 7 files changed, 355 insertions(+), 7 deletions(-) create mode 100644 src/Avalonia.Styling/Styling/NotSelector.cs create mode 100644 tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs diff --git a/src/Avalonia.Styling/Styling/NotSelector.cs b/src/Avalonia.Styling/Styling/NotSelector.cs new file mode 100644 index 0000000000..bcf76620be --- /dev/null +++ b/src/Avalonia.Styling/Styling/NotSelector.cs @@ -0,0 +1,72 @@ +// 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.Linq; + +namespace Avalonia.Styling +{ + /// + /// The `:not()` style selector. + /// + internal class NotSelector : Selector + { + private readonly Selector _previous; + private readonly Selector _argument; + private string _selectorString; + + /// + /// Initializes a new instance of the class. + /// + /// The previous selector. + /// The selector to be not-ed. + public NotSelector(Selector previous, Selector argument) + { + _previous = previous; + _argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument."); + } + + /// + public override bool InTemplate => _argument.InTemplate; + + /// + public override bool IsCombinator => false; + + /// + public override Type TargetType => _previous?.TargetType; + + /// + public override string ToString() + { + if (_selectorString == null) + { + _selectorString = ":not(" + _argument.ToString() + ")"; + } + + return _selectorString; + } + + protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + { + var innerResult = _argument.Match(control, subscribe); + + switch (innerResult.Result) + { + case SelectorMatchResult.AlwaysThisInstance: + return SelectorMatch.NeverThisInstance; + case SelectorMatchResult.AlwaysThisType: + return SelectorMatch.NeverThisType; + case SelectorMatchResult.NeverThisInstance: + return SelectorMatch.AlwaysThisInstance; + case SelectorMatchResult.NeverThisType: + return SelectorMatch.AlwaysThisType; + case SelectorMatchResult.Sometimes: + return new SelectorMatch(innerResult.Activator.Select(x => !x)); + default: + throw new InvalidOperationException("Invalid SelectorMatchResult."); + } + } + + protected override Selector MovePrevious() => _previous; + } +} diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs index c91cc7af04..4284c7e798 100644 --- a/src/Avalonia.Styling/Styling/Selectors.cs +++ b/src/Avalonia.Styling/Styling/Selectors.cs @@ -94,6 +94,17 @@ namespace Avalonia.Styling } } + /// + /// Returns a selector which inverts the results of selector argument. + /// + /// The previous selector. + /// The selector to be not-ed. + /// The selector. + public static Selector Not(this Selector previous, Func argument) + { + return new NotSelector(previous, argument(null)); + } + /// /// Returns a selector which matches a type. /// diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index f66d3e51fc..55c3aab81f 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Data.Core; using Avalonia.Utilities; @@ -32,6 +33,11 @@ namespace Avalonia.Markup.Parsers public static IEnumerable Parse(string s) { var r = new CharacterReader(s.AsSpan()); + return Parse(ref r, null); + } + + private static IEnumerable Parse(ref CharacterReader r, char? end) + { var state = State.Start; var selector = new List(); while (!r.End && state != State.End) @@ -43,7 +49,7 @@ namespace Avalonia.Markup.Parsers state = ParseStart(ref r); break; case State.Middle: - state = ParseMiddle(ref r); + state = ParseMiddle(ref r, end); break; case State.CanHaveType: state = ParseCanHaveType(ref r); @@ -107,7 +113,7 @@ namespace Avalonia.Markup.Parsers return State.TypeName; } - private static State ParseMiddle(ref CharacterReader r) + private static State ParseMiddle(ref CharacterReader r, char? end) { if (r.TakeIf(':')) { @@ -129,6 +135,10 @@ namespace Avalonia.Markup.Parsers { return State.Name; } + else if (end.HasValue && !r.End && r.Peek == end.Value) + { + return State.End; + } return State.TypeName; } @@ -151,16 +161,23 @@ namespace Avalonia.Markup.Parsers } const string IsKeyword = "is"; + const string NotKeyword = "not"; + if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('(')) { var syntax = ParseType(ref r, new IsSyntax()); - if (r.End || !r.TakeIf(')')) - { - throw new ExpressionParseException(r.Position, $"Expected ')', got {r.Peek}"); - } + Expect(ref r, ')'); return (State.CanHaveType, syntax); } + if (identifier.SequenceEqual(NotKeyword.AsSpan()) && r.TakeIf('(')) + { + var argument = Parse(ref r, ')'); + Expect(ref r, ')'); + + var syntax = new NotSyntax { Argument = argument }; + return (State.Middle, syntax); + } else { return ( @@ -282,6 +299,18 @@ namespace Avalonia.Markup.Parsers return syntax; } + private static void Expect(ref CharacterReader r, char c) + { + if (r.End) + { + throw new ExpressionParseException(r.Position, $"Expected '{c}', got end of selector."); + } + else if (!r.TakeIf(')')) + { + throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.Peek}'."); + } + } + public interface ISyntax { } @@ -376,5 +405,15 @@ namespace Avalonia.Markup.Parsers return obj is TemplateSyntax; } } + + public class NotSyntax : ISyntax + { + public IEnumerable Argument { get; set; } + + public override bool Equals(object obj) + { + return (obj is NotSyntax not) && Argument.SequenceEqual(not.Argument); + } + } } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index bf5b396bec..8d1216e1dc 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Globalization; using Avalonia.Styling; using Avalonia.Utilities; @@ -25,7 +26,7 @@ namespace Avalonia.Markup.Parsers /// public SelectorParser(Func typeResolver) { - this._typeResolver = typeResolver; + _typeResolver = typeResolver; } /// @@ -36,6 +37,11 @@ namespace Avalonia.Markup.Parsers public Selector Parse(string s) { var syntax = SelectorGrammar.Parse(s); + return Create(syntax); + } + + private Selector Create(IEnumerable syntax) + { var result = default(Selector); foreach (var i in syntax) @@ -97,6 +103,11 @@ namespace Avalonia.Markup.Parsers case SelectorGrammar.TemplateSyntax template: result = result.Template(); break; + case SelectorGrammar.NotSyntax not: + result = result.Not(x => Create(not.Argument)); + break; + default: + throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'."); } } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 88fe5a2a12..e3ce4b0968 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -200,6 +200,67 @@ namespace Avalonia.Markup.UnitTests.Parsers result); } + [Fact] + public void Not_OfType() + { + var result = SelectorGrammar.Parse(":not(Button)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NotSyntax + { + Argument = new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + }, + } + }, + result); + } + + [Fact] + public void OfType_Not_Class() + { + var result = SelectorGrammar.Parse("Button:not(.foo)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.NotSyntax + { + Argument = new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + } + }, + result); + } + + [Fact] + public void Is_Descendent_Not_OfType_Class() + { + var result = SelectorGrammar.Parse(":is(Control) :not(Button.foo)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.IsSyntax { TypeName = "Control" }, + new SelectorGrammar.DescendantSyntax { }, + new SelectorGrammar.NotSyntax + { + Argument = new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + } + }, + result); + } + [Fact] public void Namespace_Alone_Fails() { @@ -223,5 +284,17 @@ namespace Avalonia.Markup.UnitTests.Parsers { Assert.Throws(() => SelectorGrammar.Parse(".%foo")); } + + [Fact] + public void Not_Without_Argument_Fails() + { + Assert.Throws(() => SelectorGrammar.Parse(":not()")); + } + + [Fact] + public void Not_Without_Closing_Parenthesis_Fails() + { + Assert.Throws(() => SelectorGrammar.Parse(":not(Button")); + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index beaf7477d0..a84ce74a88 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -198,5 +198,33 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml ex.InnerException.Message); } } + + [Fact] + public void Style_Can_Use_Not_Selector() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var foo = window.FindControl("foo"); + var notFoo = window.FindControl("notFoo"); + + Assert.Null(foo.Background); + Assert.Equal(Colors.Red, ((ISolidColorBrush)notFoo.Background).Color); + } + } } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs new file mode 100644 index 0000000000..2f3e2b8f34 --- /dev/null +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs @@ -0,0 +1,114 @@ +// 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.Reactive.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Xunit; + +namespace Avalonia.Styling.UnitTests +{ + public class SelectorTests_Not + { + [Fact] + public void Not_Selector_Should_Have_Correct_String_Representation() + { + var target = default(Selector).Not(x => x.Class("foo")); + + Assert.Equal(":not(.foo)", target.ToString()); + } + + [Fact] + public void Not_OfType_Matches_Control_Of_Incorrect_Type() + { + var control = new Control1(); + var target = default(Selector).Not(x => x.OfType()); + + Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(control).Result); + } + + [Fact] + public void Not_OfType_Doesnt_Match_Control_Of_Correct_Type() + { + var control = new Control2(); + var target = default(Selector).Not(x => x.OfType()); + + Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result); + } + + [Fact] + public async Task Not_Class_Doesnt_Match_Control_With_Class() + { + var control = new Control1 + { + Classes = new Classes { "foo" }, + }; + + var target = default(Selector).Not(x => x.Class("foo")); + var match = target.Match(control); + + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + Assert.False(await match.Activator.Take(1)); + } + + [Fact] + public async Task Not_Class_Matches_Control_Without_Class() + { + var control = new Control1 + { + Classes = new Classes { "bar" }, + }; + + var target = default(Selector).Not(x => x.Class("foo")); + var match = target.Match(control); + + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + Assert.True(await match.Activator.Take(1)); + } + + [Fact] + public async Task OfType_Not_Class_Matches_Control_Without_Class() + { + var control = new Control1 + { + Classes = new Classes { "bar" }, + }; + + var target = default(Selector).OfType().Not(x => x.Class("foo")); + var match = target.Match(control); + + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + Assert.True(await match.Activator.Take(1)); + } + + [Fact] + public void OfType_Not_Class_Doesnt_Match_Control_Of_Wrong_Type() + { + var control = new Control2 + { + Classes = new Classes { "foo" }, + }; + + var target = default(Selector).OfType().Not(x => x.Class("foo")); + var match = target.Match(control); + + Assert.Equal(SelectorMatchResult.NeverThisType, match.Result); + } + + [Fact] + public void Returns_Correct_TargetType() + { + var target = default(Selector).OfType().Not(x => x.Class("foo")); + + Assert.Equal(typeof(Control1), target.TargetType); + } + + public class Control1 : TestControlBase + { + } + + public class Control2 : TestControlBase + { + } + } +}