From 8e7d2e5a814f13d4f7cb5258a47a483511aadb9a Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Wed, 29 Aug 2018 21:39:51 +0200 Subject: [PATCH 1/5] Handle the case of multiple content presenters within a content control handled via ContentControlMixin --- src/Avalonia.Controls/Mixins/ContentControlMixin.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs index 95193c0432..e4204bd27f 100644 --- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs +++ b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Reactive.Disposables; using System.Runtime.CompilerServices; using Avalonia.Collections; using Avalonia.Controls.Presenters; @@ -75,6 +76,12 @@ namespace Avalonia.Controls.Mixins null, presenter.GetValue(ContentPresenter.ChildProperty)); + if (subscriptions.Value.TryGetValue(sender, out IDisposable previousSubscription)) + { + subscription = new CompositeDisposable(previousSubscription, subscription); + subscriptions.Value.Remove(sender); + } + subscriptions.Value.Add(sender, subscription); } } From a14afe5c2c61ec80fb4689d535a0466bdb75120c Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Thu, 30 Aug 2018 19:00:23 +0200 Subject: [PATCH 2/5] Unit tests for the mixin changes --- .../Mixins/ContentControlMixinTests.cs | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs new file mode 100644 index 0000000000..a0487842a9 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs @@ -0,0 +1,136 @@ +// 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.Collections.Generic; +using Avalonia.Collections; +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.LogicalTree; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Mixins +{ + public class ContentControlMixinTests + { + [Fact] + public void Multiple_Mixin_Usages_Should_Not_Throw() + { + var target = new TestControl() + { + Template = new FuncControlTemplate(_ => new Panel + { + Children = + { + new ContentPresenter { Name = "Content_1_Presenter" }, + new ContentPresenter { Name = "Content_2_Presenter" } + } + }) + }; + + var ex = Record.Exception(() => target.ApplyTemplate()); + + Assert.Null(ex); + } + + [Fact] + public void Replacing_Template_Releases_Events() + { + var p1 = new ContentPresenter { Name = "Content_1_Presenter" }; + var p2 = new ContentPresenter { Name = "Content_2_Presenter" }; + + var callIndex = -1; + var called = new bool[4]; + + void Callback() + { + if (callIndex >= 0) + called[callIndex] = true; + } + + var listMock = new Mock>(); + listMock.Setup(l => l.Contains(It.IsAny())).Returns(false).Callback(Callback); + var list = listMock.Object; + + var target = new TestControl(list) + { + Template = new FuncControlTemplate(_ => new Panel + { + Children = + { + p1, + p2 + } + }) + }; + target.ApplyTemplate(); + + callIndex = 0; + p1.Content = new Control(); + p1.UpdateChild(); + + callIndex = 1; + p2.Content = new Control(); + p2.UpdateChild(); + + target.Template = null; + + callIndex = 2; + p1.Content = new Control(); + p1.UpdateChild(); + + callIndex = 3; + p2.Content = new Control(); + p2.UpdateChild(); + + + Assert.Equal(new[] { true, true, false, false }, called); + } + + private class TestControl : TemplatedControl + { + public static readonly StyledProperty Content1Property = + AvaloniaProperty.Register(nameof(Content1)); + + public static readonly StyledProperty Content2Property = + AvaloniaProperty.Register(nameof(Content2)); + + + static TestControl() + { + ContentControlMixin.Attach(Content1Property, x => x.GetLogicalChildren(), "Content_1_Presenter"); + ContentControlMixin.Attach(Content2Property, x => x.GetLogicalChildren(), "Content_2_Presenter"); + } + + private IAvaloniaList _mock; + + public TestControl() + { + } + + public TestControl(IAvaloniaList mock) + { + _mock = mock; + } + + public IAvaloniaList GetLogicalChildren() + { + return _mock ?? LogicalChildren; + } + + public object Content1 + { + get { return GetValue(Content1Property); } + set { SetValue(Content1Property, value); } + } + + public object Content2 + { + get { return GetValue(Content2Property); } + set { SetValue(Content2Property, value); } + } + } + } +} From 01e1835ad884b4c85cca5f31c167876715099032 Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Sun, 2 Sep 2018 23:03:21 +0200 Subject: [PATCH 3/5] Corrected test implementation --- .../Mixins/ContentControlMixinTests.cs | 60 ++++++------------- 1 file changed, 18 insertions(+), 42 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs index a0487842a9..71c396b2c6 100644 --- a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Collections.Generic; +using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; @@ -41,20 +42,10 @@ namespace Avalonia.Controls.UnitTests.Mixins var p1 = new ContentPresenter { Name = "Content_1_Presenter" }; var p2 = new ContentPresenter { Name = "Content_2_Presenter" }; - var callIndex = -1; - var called = new bool[4]; + var itemsAddedThroughMixin = new List(); + var itemsNotAddedThroughMixin = new List(); - void Callback() - { - if (callIndex >= 0) - called[callIndex] = true; - } - - var listMock = new Mock>(); - listMock.Setup(l => l.Contains(It.IsAny())).Returns(false).Callback(Callback); - var list = listMock.Object; - - var target = new TestControl(list) + var target = new TestControl { Template = new FuncControlTemplate(_ => new Panel { @@ -67,26 +58,28 @@ namespace Avalonia.Controls.UnitTests.Mixins }; target.ApplyTemplate(); - callIndex = 0; - p1.Content = new Control(); + Control tc; + + p1.Content = tc = new Control(); p1.UpdateChild(); + itemsAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); - callIndex = 1; - p2.Content = new Control(); + p2.Content = tc = new Control(); p2.UpdateChild(); + itemsAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); target.Template = null; - callIndex = 2; - p1.Content = new Control(); + p1.Content = tc = new Control(); p1.UpdateChild(); + itemsNotAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); - callIndex = 3; - p2.Content = new Control(); + p2.Content = tc = new Control(); p2.UpdateChild(); + itemsNotAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); - - Assert.Equal(new[] { true, true, false, false }, called); + Assert.Equal(new[] { true, true }, itemsAddedThroughMixin); + Assert.Equal(new[] { false, false }, itemsNotAddedThroughMixin); } private class TestControl : TemplatedControl @@ -97,27 +90,10 @@ namespace Avalonia.Controls.UnitTests.Mixins public static readonly StyledProperty Content2Property = AvaloniaProperty.Register(nameof(Content2)); - static TestControl() { - ContentControlMixin.Attach(Content1Property, x => x.GetLogicalChildren(), "Content_1_Presenter"); - ContentControlMixin.Attach(Content2Property, x => x.GetLogicalChildren(), "Content_2_Presenter"); - } - - private IAvaloniaList _mock; - - public TestControl() - { - } - - public TestControl(IAvaloniaList mock) - { - _mock = mock; - } - - public IAvaloniaList GetLogicalChildren() - { - return _mock ?? LogicalChildren; + ContentControlMixin.Attach(Content1Property, x => x.LogicalChildren, "Content_1_Presenter"); + ContentControlMixin.Attach(Content2Property, x => x.LogicalChildren, "Content_2_Presenter"); } public object Content1 From 1b82998775a597e6896eba1b66a0ad6be5c35cfa Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Sun, 2 Sep 2018 23:24:50 +0200 Subject: [PATCH 4/5] Made test more readable --- .../Mixins/ContentControlMixinTests.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs index 71c396b2c6..f06553411c 100644 --- a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs @@ -41,10 +41,6 @@ namespace Avalonia.Controls.UnitTests.Mixins { var p1 = new ContentPresenter { Name = "Content_1_Presenter" }; var p2 = new ContentPresenter { Name = "Content_2_Presenter" }; - - var itemsAddedThroughMixin = new List(); - var itemsNotAddedThroughMixin = new List(); - var target = new TestControl { Template = new FuncControlTemplate(_ => new Panel @@ -62,24 +58,22 @@ namespace Avalonia.Controls.UnitTests.Mixins p1.Content = tc = new Control(); p1.UpdateChild(); - itemsAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); + Assert.Contains(tc, target.GetLogicalChildren()); p2.Content = tc = new Control(); p2.UpdateChild(); - itemsAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); + Assert.Contains(tc, target.GetLogicalChildren()); target.Template = null; p1.Content = tc = new Control(); p1.UpdateChild(); - itemsNotAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); + Assert.DoesNotContain(tc, target.GetLogicalChildren()); p2.Content = tc = new Control(); p2.UpdateChild(); - itemsNotAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); + Assert.DoesNotContain(tc, target.GetLogicalChildren()); - Assert.Equal(new[] { true, true }, itemsAddedThroughMixin); - Assert.Equal(new[] { false, false }, itemsNotAddedThroughMixin); } private class TestControl : TemplatedControl From 158d2d31b3d6d193e22ff7031eeb0910bf242116 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 2 Sep 2018 23:54:46 +0200 Subject: [PATCH 5/5] Add RoutedEventRegistry. Fixes #1846. --- src/Avalonia.Interactivity/RoutedEvent.cs | 8 +- .../RoutedEventRegistry.cs | 90 +++++++++++++++++++ .../RoutedEventRegistryTests.cs | 49 ++++++++++ 3 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Interactivity/RoutedEventRegistry.cs create mode 100644 tests/Avalonia.Interactivity.UnitTests/RoutedEventRegistryTests.cs diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index 61bb567d54..2d752133c1 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -72,7 +72,9 @@ namespace Avalonia.Interactivity { Contract.Requires(name != null); - return new RoutedEvent(name, routingStrategy, typeof(TOwner)); + var routedEvent = new RoutedEvent(name, routingStrategy, typeof(TOwner)); + RoutedEventRegistry.Instance.Register(typeof(TOwner), routedEvent); + return routedEvent; } public static RoutedEvent Register( @@ -83,7 +85,9 @@ namespace Avalonia.Interactivity { Contract.Requires(name != null); - return new RoutedEvent(name, routingStrategy, ownerType); + var routedEvent = new RoutedEvent(name, routingStrategy, ownerType); + RoutedEventRegistry.Instance.Register(ownerType, routedEvent); + return routedEvent; } public IDisposable AddClassHandler( diff --git a/src/Avalonia.Interactivity/RoutedEventRegistry.cs b/src/Avalonia.Interactivity/RoutedEventRegistry.cs new file mode 100644 index 0000000000..34c970a806 --- /dev/null +++ b/src/Avalonia.Interactivity/RoutedEventRegistry.cs @@ -0,0 +1,90 @@ +// 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.Collections.Generic; + +namespace Avalonia.Interactivity +{ + /// + /// Tracks registered s. + /// + public class RoutedEventRegistry + { + private readonly Dictionary> _registeredRoutedEvents = + new Dictionary>(); + + /// + /// Gets the instance. + /// + public static RoutedEventRegistry Instance { get; } + = new RoutedEventRegistry(); + + /// + /// Registers a on a type. + /// + /// The type. + /// The event. + /// + /// You won't usually want to call this method directly, instead use the + /// + /// method. + /// + public void Register(Type type, RoutedEvent @event) + { + Contract.Requires(type != null); + Contract.Requires(@event != null); + + if (!_registeredRoutedEvents.TryGetValue(type, out var list)) + { + list = new List(); + _registeredRoutedEvents.Add(type, list); + } + list.Add(@event); + } + + /// + /// Returns all routed events, that are currently registered in the event registry. + /// + /// All routed events, that are currently registered in the event registry. + public IEnumerable GetAllRegistered() + { + foreach (var events in _registeredRoutedEvents.Values) + { + foreach (var e in events) + { + yield return e; + } + } + } + + /// + /// Returns all routed events registered with the provided type. + /// If the type is not found or does not provide any routed events, an empty list is returned. + /// + /// The type. + /// All routed events registered with the provided type. + public IReadOnlyList GetRegistered(Type type) + { + Contract.Requires(type != null); + + if (_registeredRoutedEvents.TryGetValue(type, out var events)) + { + return events; + } + + return Array.Empty(); + } + + /// + /// Returns all routed events registered with the provided type. + /// If the type is not found or does not provide any routed events, an empty list is returned. + /// + /// The type. + /// All routed events registered with the provided type. + public IReadOnlyList GetRegistered() + { + return GetRegistered(typeof(TOwner)); + } + } +} diff --git a/tests/Avalonia.Interactivity.UnitTests/RoutedEventRegistryTests.cs b/tests/Avalonia.Interactivity.UnitTests/RoutedEventRegistryTests.cs new file mode 100644 index 0000000000..b9ebdea064 --- /dev/null +++ b/tests/Avalonia.Interactivity.UnitTests/RoutedEventRegistryTests.cs @@ -0,0 +1,49 @@ +// 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.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Input; +using Xunit; + +namespace Avalonia.Interactivity.UnitTests +{ + public class RoutedEventRegistryTests + { + [Fact] + public void Pointer_Events_Should_Be_Registered() + { + var expectedEvents = new List { InputElement.PointerPressedEvent, InputElement.PointerReleasedEvent }; + var registeredEvents = RoutedEventRegistry.Instance.GetRegistered(); + Assert.Contains(registeredEvents, expectedEvents.Contains); + } + + [Fact] + public void ClickEvent_Should_Be_Registered_On_Button() + { + var expectedEvents = new List { Button.ClickEvent }; + var registeredEvents = RoutedEventRegistry.Instance.GetRegistered