diff --git a/.build/dependencies.props b/.build/dependencies.props index f97e58774e..cac54299e2 100644 --- a/.build/dependencies.props +++ b/.build/dependencies.props @@ -69,7 +69,6 @@ 3.17.0 3.13.1 1.9.1.1 - 7.2.0.1422 2.7.8 1.1.0 0.4.1.1 diff --git a/.rat-excludes b/.rat-excludes index 257b685363..cddb9ba01b 100644 --- a/.rat-excludes +++ b/.rat-excludes @@ -27,6 +27,7 @@ psake/* KStemData\d+\.cs Snowball/* Egothor.Stemmer/* +Events/* Sax/* JaspellTernarySearchTrie\.cs RectangularArrays\.cs diff --git a/src/Lucene.Net.Facet/Taxonomy/CachedOrdinalsReader.cs b/src/Lucene.Net.Facet/Taxonomy/CachedOrdinalsReader.cs index 8acea00363..19f85a64d6 100644 --- a/src/Lucene.Net.Facet/Taxonomy/CachedOrdinalsReader.cs +++ b/src/Lucene.Net.Facet/Taxonomy/CachedOrdinalsReader.cs @@ -3,7 +3,7 @@ using Lucene.Net.Support.Threading; using Lucene.Net.Util; #if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR -using Prism.Events; +using Lucene.Net.Util.Events; #endif using System; using System.Collections.Generic; diff --git a/src/Lucene.Net.Tests/Support/Util/Events/MockDelegateReference.cs b/src/Lucene.Net.Tests/Support/Util/Events/MockDelegateReference.cs new file mode 100644 index 0000000000..7565e2f544 --- /dev/null +++ b/src/Lucene.Net.Tests/Support/Util/Events/MockDelegateReference.cs @@ -0,0 +1,43 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using System; + +namespace Lucene.Net.Util.Events +{ + class MockDelegateReference : IDelegateReference + { + public Delegate Target { get; set; } + + public MockDelegateReference() + { + + } + + public MockDelegateReference(Delegate target) + { + Target = target; + } + } +} + +#endif \ No newline at end of file diff --git a/src/Lucene.Net.Tests/Support/Util/Events/TestBackgroundEventSubscription.cs b/src/Lucene.Net.Tests/Support/Util/Events/TestBackgroundEventSubscription.cs new file mode 100644 index 0000000000..d959c297ad --- /dev/null +++ b/src/Lucene.Net.Tests/Support/Util/Events/TestBackgroundEventSubscription.cs @@ -0,0 +1,91 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using NUnit.Framework; +using System; +using System.Threading; +using Assert = Lucene.Net.TestFramework.Assert; + +namespace Lucene.Net.Util.Events +{ + [TestFixture] + public class TestBackgroundEventSubscription + { + [Test] + public void ShouldReceiveDelegateOnDifferentThread() + { + ManualResetEvent completeEvent = new ManualResetEvent(false); + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); + SynchronizationContext calledSyncContext = null; + Action action = delegate + { + calledSyncContext = SynchronizationContext.Current; + completeEvent.Set(); + }; + + IDelegateReference actionDelegateReference = new MockDelegateReference() { Target = action }; + IDelegateReference filterDelegateReference = new MockDelegateReference() { Target = (Predicate)delegate { return true; } }; + + var eventSubscription = new BackgroundEventSubscription(actionDelegateReference, filterDelegateReference); + + + var publishAction = eventSubscription.GetExecutionStrategy(); + + Assert.NotNull(publishAction); + + publishAction.Invoke(null); + + completeEvent.WaitOne(5000); + + Assert.AreNotEqual(SynchronizationContext.Current, calledSyncContext); + } + + [Test] + public void ShouldReceiveDelegateOnDifferentThreadNonGeneric() + { + var completeEvent = new ManualResetEvent(false); + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); + SynchronizationContext calledSyncContext = null; + Action action = delegate + { + calledSyncContext = SynchronizationContext.Current; + completeEvent.Set(); + }; + + IDelegateReference actionDelegateReference = new MockDelegateReference() { Target = action }; + + var eventSubscription = new BackgroundEventSubscription(actionDelegateReference); + + var publishAction = eventSubscription.GetExecutionStrategy(); + + Assert.NotNull(publishAction); + + publishAction.Invoke(null); + + completeEvent.WaitOne(5000); + + Assert.AreNotEqual(SynchronizationContext.Current, calledSyncContext); + } + } +} + +#endif \ No newline at end of file diff --git a/src/Lucene.Net.Tests/Support/Util/Events/TestDelegateReference.cs b/src/Lucene.Net.Tests/Support/Util/Events/TestDelegateReference.cs new file mode 100644 index 0000000000..2d6ed5bfe5 --- /dev/null +++ b/src/Lucene.Net.Tests/Support/Util/Events/TestDelegateReference.cs @@ -0,0 +1,214 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using NUnit.Framework; +using System; +using System.Threading.Tasks; +using Assert = Lucene.Net.TestFramework.Assert; + +namespace Lucene.Net.Util.Events +{ + [TestFixture] + public class TestDelegateReference + { + [Test] + public void KeepAlivePreventsDelegateFromBeingCollected() + { + var delegates = new SomeClassHandler(); + var delegateReference = new DelegateReference((Action)delegates.DoEvent, true); + + delegates = null; + GC.Collect(); + + Assert.NotNull(delegateReference.Target); + } + + [Test] + public async Task NotKeepAliveAllowsDelegateToBeCollected() + { + var delegates = new SomeClassHandler(); + var delegateReference = new DelegateReference((Action)delegates.DoEvent, false); + + delegates = null; + await Task.Delay(100); + GC.Collect(); + + Assert.Null(delegateReference.Target); + } + + [Test] + public async Task NotKeepAliveKeepsDelegateIfStillAlive() + { + var delegates = new SomeClassHandler(); + var delegateReference = new DelegateReference((Action)delegates.DoEvent, false); + + GC.Collect(); + + Assert.NotNull(delegateReference.Target); + + GC.KeepAlive(delegates); //Makes delegates ineligible for garbage collection until this point (to prevent oompiler optimizations that may release the referenced object prematurely). + delegates = null; + await Task.Delay(100); + GC.Collect(); + + Assert.Null(delegateReference.Target); + } + + [Test] + public void TargetShouldReturnAction() + { + var classHandler = new SomeClassHandler(); + Action myAction = new Action(classHandler.MyAction); + + var weakAction = new DelegateReference(myAction, false); + + ((Action)weakAction.Target)("payload"); + Assert.AreEqual("payload", classHandler.MyActionArg); + } + + [Test] + public async Task ShouldAllowCollectionOfOriginalDelegate() + { + var classHandler = new SomeClassHandler(); + Action myAction = new Action(classHandler.MyAction); + + var weakAction = new DelegateReference(myAction, false); + + var originalAction = new WeakReference(myAction); + myAction = null; + await Task.Delay(100); + GC.Collect(); + Assert.False(originalAction.IsAlive); + + ((Action)weakAction.Target)("payload"); + Assert.AreEqual("payload", classHandler.MyActionArg); + } + + [Test] + public async Task ShouldReturnNullIfTargetNotAlive() + { + SomeClassHandler handler = new SomeClassHandler(); + var weakHandlerRef = new WeakReference(handler); + + var action = new DelegateReference((Action)handler.DoEvent, false); + + handler = null; + await Task.Delay(100); + GC.Collect(); + Assert.False(weakHandlerRef.IsAlive); + + Assert.Null(action.Target); + } + + [Test] + public void WeakDelegateWorksWithStaticMethodDelegates() + { + var action = new DelegateReference((Action)SomeClassHandler.StaticMethod, false); + + Assert.NotNull(action.Target); + } + + [Test] + public void TargetEqualsActionShouldReturnTrue() + { + var classHandler = new SomeClassHandler(); + Action myAction = new Action(classHandler.MyAction); + + var weakAction = new DelegateReference(myAction, false); + + Assert.True(weakAction.TargetEquals(new Action(classHandler.MyAction))); + } + + [Test] + public async Task TargetEqualsNullShouldReturnTrueIfTargetNotAlive() + { + SomeClassHandler handler = new SomeClassHandler(); + var weakHandlerRef = new WeakReference(handler); + + var action = new DelegateReference((Action)handler.DoEvent, false); + + handler = null; + + // Intentional delay to encourage Garbage Collection to actually occur + await Task.Delay(100); + GC.Collect(); + Assert.False(weakHandlerRef.IsAlive); + + Assert.True(action.TargetEquals(null)); + } + + [Test] + public void TargetEqualsNullShouldReturnFalseIfTargetAlive() + { + SomeClassHandler handler = new SomeClassHandler(); + var weakHandlerRef = new WeakReference(handler); + + var action = new DelegateReference((Action)handler.DoEvent, false); + + Assert.False(action.TargetEquals(null)); + Assert.True(weakHandlerRef.IsAlive); + GC.KeepAlive(handler); + } + + [Test] + public void TargetEqualsWorksWithStaticMethodDelegates() + { + var action = new DelegateReference((Action)SomeClassHandler.StaticMethod, false); + + Assert.True(action.TargetEquals((Action)SomeClassHandler.StaticMethod)); + } + + //todo: fix + //[Test] + //public void NullDelegateThrows() + //{ + // Assert.ThrowsException(() => + // { + // var action = new DelegateReference(null, true); + // }); + //} + + public class SomeClassHandler + { + public string MyActionArg; + + public void DoEvent(string value) + { + string myValue = value; + } + + public static void StaticMethod() + { +#pragma warning disable 0219 + int i = 0; +#pragma warning restore 0219 + } + + public void MyAction(string arg) + { + MyActionArg = arg; + } + } + } +} + +#endif \ No newline at end of file diff --git a/src/Lucene.Net.Tests/Support/Util/Events/TestDispatcherEventSubscription.cs b/src/Lucene.Net.Tests/Support/Util/Events/TestDispatcherEventSubscription.cs new file mode 100644 index 0000000000..51eed98a84 --- /dev/null +++ b/src/Lucene.Net.Tests/Support/Util/Events/TestDispatcherEventSubscription.cs @@ -0,0 +1,123 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using NUnit.Framework; +using System; +using System.Threading; +using Assert = Lucene.Net.TestFramework.Assert; + +namespace Lucene.Net.Util.Events +{ + [TestFixture] + public class TestDispatcherEventSubscription + { + [Test] + public void ShouldCallInvokeOnDispatcher() + { + DispatcherEventSubscription eventSubscription = null; + + IDelegateReference actionDelegateReference = new MockDelegateReference() + { + Target = (Action)(arg => + { + return; + }) + }; + + IDelegateReference filterDelegateReference = new MockDelegateReference + { + Target = (Predicate)(arg => true) + }; + var mockSyncContext = new MockSynchronizationContext(); + + eventSubscription = new DispatcherEventSubscription(actionDelegateReference, filterDelegateReference, mockSyncContext); + + eventSubscription.GetExecutionStrategy().Invoke(new object[0]); + + Assert.True(mockSyncContext.InvokeCalled); + } + + [Test] + public void ShouldCallInvokeOnDispatcherNonGeneric() + { + DispatcherEventSubscription eventSubscription = null; + + IDelegateReference actionDelegateReference = new MockDelegateReference() + { + Target = (Action)(() => + { }) + }; + + var mockSyncContext = new MockSynchronizationContext(); + + eventSubscription = new DispatcherEventSubscription(actionDelegateReference, mockSyncContext); + + eventSubscription.GetExecutionStrategy().Invoke(new object[0]); + + Assert.True(mockSyncContext.InvokeCalled); + } + + [Test] + public void ShouldPassParametersCorrectly() + { + IDelegateReference actionDelegateReference = new MockDelegateReference() + { + Target = + (Action)(arg1 => + { + return; + }) + }; + IDelegateReference filterDelegateReference = new MockDelegateReference + { + Target = (Predicate)(arg => true) + }; + + var mockSyncContext = new MockSynchronizationContext(); + + DispatcherEventSubscription eventSubscription = new DispatcherEventSubscription(actionDelegateReference, filterDelegateReference, mockSyncContext); + + var executionStrategy = eventSubscription.GetExecutionStrategy(); + Assert.NotNull(executionStrategy); + + object argument1 = new object(); + + executionStrategy.Invoke(new[] { argument1 }); + + Assert.AreSame(argument1, mockSyncContext.InvokeArg); + } + } + + internal class MockSynchronizationContext : SynchronizationContext + { + public bool InvokeCalled; + public object InvokeArg; + + public override void Post(SendOrPostCallback d, object state) + { + InvokeCalled = true; + InvokeArg = state; + } + } +} + +#endif \ No newline at end of file diff --git a/src/Lucene.Net.Tests/Support/Util/Events/TestEventAggregator.cs b/src/Lucene.Net.Tests/Support/Util/Events/TestEventAggregator.cs new file mode 100644 index 0000000000..5f945013f0 --- /dev/null +++ b/src/Lucene.Net.Tests/Support/Util/Events/TestEventAggregator.cs @@ -0,0 +1,45 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using NUnit.Framework; +using Assert = Lucene.Net.TestFramework.Assert; + +namespace Lucene.Net.Util.Events +{ + [TestFixture] + public class TestEventAggregator + { + [Test] + public void GetReturnsSingleInstancesOfSameEventType() + { + var eventAggregator = new EventAggregator(); + var instance1 = eventAggregator.GetEvent(); + var instance2 = eventAggregator.GetEvent(); + + Assert.AreSame(instance2, instance1); + } + + internal class MockEventBase : EventBase { } + } +} + +#endif \ No newline at end of file diff --git a/src/Lucene.Net.Tests/Support/Util/Events/TestEventBase.cs b/src/Lucene.Net.Tests/Support/Util/Events/TestEventBase.cs new file mode 100644 index 0000000000..c71ddcb3ac --- /dev/null +++ b/src/Lucene.Net.Tests/Support/Util/Events/TestEventBase.cs @@ -0,0 +1,138 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using NUnit.Framework; +using System; +using Assert = Lucene.Net.TestFramework.Assert; + +namespace Lucene.Net.Util.Events +{ + [TestFixture] + public class TestEventBase + { + [Test] + public void CanPublishSimpleEvents() + { + var eventBase = new TestableEventBase(); + var eventSubscription = new MockEventSubscription(); + bool eventPublished = false; + eventSubscription.GetPublishActionReturnValue = delegate + { + eventPublished = true; + }; + eventBase.Subscribe(eventSubscription); + + eventBase.Publish(); + + Assert.True(eventSubscription.GetPublishActionCalled); + Assert.True(eventPublished); + } + + [Test] + public void CanHaveMultipleSubscribersAndRaiseCustomEvent() + { + var customEvent = new TestableEventBase(); + Payload payload = new Payload(); + object[] received1 = null; + object[] received2 = null; + var eventSubscription1 = new MockEventSubscription(); + eventSubscription1.GetPublishActionReturnValue = delegate (object[] args) { received1 = args; }; + var eventSubscription2 = new MockEventSubscription(); + eventSubscription2.GetPublishActionReturnValue = delegate (object[] args) { received2 = args; }; + + customEvent.Subscribe(eventSubscription1); + customEvent.Subscribe(eventSubscription2); + + customEvent.Publish(payload); + + Assert.AreEqual(1, received1.Length); + Assert.AreSame(received1[0], payload); + + Assert.AreEqual(1, received2.Length); + Assert.AreSame(received2[0], payload); + } + + [Test] + public void ShouldSubscribeAndUnsubscribe() + { + var eventBase = new TestableEventBase(); + + var eventSubscription = new MockEventSubscription(); + eventBase.Subscribe(eventSubscription); + + Assert.NotNull(eventSubscription.SubscriptionToken); + Assert.True(eventBase.Contains(eventSubscription.SubscriptionToken)); + + eventBase.Unsubscribe(eventSubscription.SubscriptionToken); + + Assert.False(eventBase.Contains(eventSubscription.SubscriptionToken)); + } + + [Test] + public void WhenEventSubscriptionActionIsNullPruneItFromList() + { + var eventBase = new TestableEventBase(); + + var eventSubscription = new MockEventSubscription(); + eventSubscription.GetPublishActionReturnValue = null; + + var token = eventBase.Subscribe(eventSubscription); + + eventBase.Publish(); + + Assert.False(eventBase.Contains(token)); + } + + + class TestableEventBase : EventBase + { + public SubscriptionToken Subscribe(IEventSubscription subscription) + { + return base.InternalSubscribe(subscription); + } + + public void Publish(params object[] arguments) + { + base.InternalPublish(arguments); + } + } + + class MockEventSubscription : IEventSubscription + { + public Action GetPublishActionReturnValue; + public bool GetPublishActionCalled; + + public Action GetExecutionStrategy() + { + GetPublishActionCalled = true; + return GetPublishActionReturnValue; + } + + public SubscriptionToken SubscriptionToken { get; set; } + } + + class Payload { } + + } +} + +#endif \ No newline at end of file diff --git a/src/Lucene.Net.Tests/Support/Util/Events/TestEventSubscription.cs b/src/Lucene.Net.Tests/Support/Util/Events/TestEventSubscription.cs new file mode 100644 index 0000000000..dc78cdfa0a --- /dev/null +++ b/src/Lucene.Net.Tests/Support/Util/Events/TestEventSubscription.cs @@ -0,0 +1,347 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using NUnit.Framework; +using System; +using System.Collections.Generic; +using Assert = Lucene.Net.TestFramework.Assert; + +namespace Lucene.Net.Util.Events +{ + [TestFixture] + public class TestEventSubscription + { + [Test] + public void NullTargetInActionThrows() + { + Assert.Throws(() => + { + var actionDelegateReference = new MockDelegateReference() + { + Target = null + }; + var filterDelegateReference = new MockDelegateReference() + { + Target = (Predicate)(arg => + { + return true; + }) + }; + var eventSubscription = new EventSubscription(actionDelegateReference, + filterDelegateReference); + }); + + } + + [Test] + public void NullTargetInActionThrowsNonGeneric() + { + Assert.Throws(() => + { + var actionDelegateReference = new MockDelegateReference() + { + Target = null + }; + var eventSubscription = new EventSubscription(actionDelegateReference); + }); + } + + [Test] + public void DifferentTargetTypeInActionThrows() + { + Assert.Throws(() => + { + var actionDelegateReference = new MockDelegateReference() + { + Target = (Action)delegate { } + }; + var filterDelegateReference = new MockDelegateReference() + { + Target = (Predicate)(arg => + { + return true; + }) + }; + var eventSubscription = new EventSubscription(actionDelegateReference, + filterDelegateReference); + }); + } + + [Test] + public void DifferentTargetTypeInActionThrowsNonGeneric() + { + Assert.Throws(() => + { + var actionDelegateReference = new MockDelegateReference() + { + Target = (Action)delegate { } + }; + + var eventSubscription = new EventSubscription(actionDelegateReference); + }); + } + + [Test] + public void NullActionThrows() + { + Assert.Throws(() => + { + var filterDelegateReference = new MockDelegateReference() + { + Target = (Predicate)(arg => + { + return true; + }) + }; + var eventSubscription = new EventSubscription(null, filterDelegateReference); + }); + } + + [Test] + public void NullActionThrowsNonGeneric() + { + Assert.Throws(() => + { + var eventSubscription = new EventSubscription(null); + }); + } + + [Test] + public void NullTargetInFilterThrows() + { + Assert.Throws(() => + { + var actionDelegateReference = new MockDelegateReference() + { + Target = (Action)delegate { } + }; + + var filterDelegateReference = new MockDelegateReference() + { + Target = null + }; + var eventSubscription = new EventSubscription(actionDelegateReference, + filterDelegateReference); + }); + } + + + [Test] + public void DifferentTargetTypeInFilterThrows() + { + Assert.Throws(() => + { + var actionDelegateReference = new MockDelegateReference() + { + Target = (Action)delegate { } + }; + + var filterDelegateReference = new MockDelegateReference() + { + Target = (Predicate)(arg => + { + return true; + }) + }; + + var eventSubscription = new EventSubscription(actionDelegateReference, + filterDelegateReference); + }); + } + + [Test] + public void NullFilterThrows() + { + Assert.Throws(() => + { + var actionDelegateReference = new MockDelegateReference() + { + Target = (Action)delegate { } + }; + + var eventSubscription = new EventSubscription(actionDelegateReference, + null); + }); + } + + [Test] + public void CanInitEventSubscription() + { + var actionDelegateReference = new MockDelegateReference((Action)delegate { }); + var filterDelegateReference = new MockDelegateReference((Predicate)delegate { return true; }); + var eventSubscription = new EventSubscription(actionDelegateReference, filterDelegateReference); + + var subscriptionToken = new SubscriptionToken(t => { }); + + eventSubscription.SubscriptionToken = subscriptionToken; + + Assert.AreSame(actionDelegateReference.Target, eventSubscription.Action); + Assert.AreSame(filterDelegateReference.Target, eventSubscription.Filter); + Assert.AreSame(subscriptionToken, eventSubscription.SubscriptionToken); + } + + [Test] + public void CanInitEventSubscriptionNonGeneric() + { + var actionDelegateReference = new MockDelegateReference((Action)delegate { }); + var eventSubscription = new EventSubscription(actionDelegateReference); + + var subscriptionToken = new SubscriptionToken(t => { }); + + eventSubscription.SubscriptionToken = subscriptionToken; + + Assert.AreSame(actionDelegateReference.Target, eventSubscription.Action); + Assert.AreSame(subscriptionToken, eventSubscription.SubscriptionToken); + } + + [Test] + public void GetPublishActionReturnsDelegateThatExecutesTheFilterAndThenTheAction() + { + var executedDelegates = new List(); + var actionDelegateReference = + new MockDelegateReference((Action)delegate { executedDelegates.Add("Action"); }); + + var filterDelegateReference = new MockDelegateReference((Predicate)delegate + { + executedDelegates.Add( + "Filter"); + return true; + }); + + var eventSubscription = new EventSubscription(actionDelegateReference, filterDelegateReference); + + + var publishAction = eventSubscription.GetExecutionStrategy(); + + Assert.NotNull(publishAction); + + publishAction.Invoke(null); + + Assert.AreEqual(2, executedDelegates.Count); + Assert.AreEqual("Filter", executedDelegates[0]); + Assert.AreEqual("Action", executedDelegates[1]); + } + + [Test] + public void GetPublishActionReturnsNullIfActionIsNull() + { + var actionDelegateReference = new MockDelegateReference((Action)delegate { }); + var filterDelegateReference = new MockDelegateReference((Predicate)delegate { return true; }); + + var eventSubscription = new EventSubscription(actionDelegateReference, filterDelegateReference); + + var publishAction = eventSubscription.GetExecutionStrategy(); + + Assert.NotNull(publishAction); + + actionDelegateReference.Target = null; + + publishAction = eventSubscription.GetExecutionStrategy(); + + Assert.Null(publishAction); + } + + [Test] + public void GetPublishActionReturnsNullIfActionIsNullNonGeneric() + { + var actionDelegateReference = new MockDelegateReference((Action)delegate { }); + + var eventSubscription = new EventSubscription(actionDelegateReference); + + var publishAction = eventSubscription.GetExecutionStrategy(); + + Assert.NotNull(publishAction); + + actionDelegateReference.Target = null; + + publishAction = eventSubscription.GetExecutionStrategy(); + + Assert.Null(publishAction); + } + + [Test] + public void GetPublishActionReturnsNullIfFilterIsNull() + { + var actionDelegateReference = new MockDelegateReference((Action)delegate { }); + var filterDelegateReference = new MockDelegateReference((Predicate)delegate { return true; }); + + var eventSubscription = new EventSubscription(actionDelegateReference, filterDelegateReference); + + var publishAction = eventSubscription.GetExecutionStrategy(); + + Assert.NotNull(publishAction); + + filterDelegateReference.Target = null; + + publishAction = eventSubscription.GetExecutionStrategy(); + + Assert.Null(publishAction); + } + + [Test] + public void GetPublishActionDoesNotExecuteActionIfFilterReturnsFalse() + { + bool actionExecuted = false; + var actionDelegateReference = new MockDelegateReference() + { + Target = (Action)delegate { actionExecuted = true; } + }; + var filterDelegateReference = new MockDelegateReference((Predicate)delegate + { + return false; + }); + + var eventSubscription = new EventSubscription(actionDelegateReference, filterDelegateReference); + + + var publishAction = eventSubscription.GetExecutionStrategy(); + + publishAction.Invoke(new object[] { null }); + + Assert.False(actionExecuted); + } + + [Test] + public void StrategyPassesArgumentToDelegates() + { + string passedArgumentToAction = null; + string passedArgumentToFilter = null; + + var actionDelegateReference = new MockDelegateReference((Action)(obj => passedArgumentToAction = obj)); + var filterDelegateReference = new MockDelegateReference((Predicate)(obj => + { + passedArgumentToFilter = obj; + return true; + })); + + var eventSubscription = new EventSubscription(actionDelegateReference, filterDelegateReference); + var publishAction = eventSubscription.GetExecutionStrategy(); + + publishAction.Invoke(new[] { "TestString" }); + + Assert.AreEqual("TestString", passedArgumentToAction); + Assert.AreEqual("TestString", passedArgumentToFilter); + } + } +} + +#endif \ No newline at end of file diff --git a/src/Lucene.Net.Tests/Support/Util/Events/TestPubSubEvent.cs b/src/Lucene.Net.Tests/Support/Util/Events/TestPubSubEvent.cs new file mode 100644 index 0000000000..710545ecc9 --- /dev/null +++ b/src/Lucene.Net.Tests/Support/Util/Events/TestPubSubEvent.cs @@ -0,0 +1,712 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Assert = Lucene.Net.TestFramework.Assert; + +namespace Lucene.Net.Util.Events +{ + [TestFixture] + public class TestPubSubEvent + { + [Test] + public void EnsureSubscriptionListIsEmptyAfterPublishingAMessage() + { + var pubSubEvent = new TestablePubSubEvent(); + SubscribeExternalActionWithoutReference(pubSubEvent); + GC.Collect(); + pubSubEvent.Publish("testPayload"); + Assert.True(pubSubEvent.BaseSubscriptions.Count == 0, "Subscriptionlist is not empty"); + } + + [Test] + public void EnsureSubscriptionListIsNotEmptyWithoutPublishOrSubscribe() + { + var pubSubEvent = new TestablePubSubEvent(); + SubscribeExternalActionWithoutReference(pubSubEvent); + GC.Collect(); + Assert.True(pubSubEvent.BaseSubscriptions.Count == 1, "Subscriptionlist is empty"); + } + + [Test] + public void EnsureSubscriptionListIsEmptyAfterSubscribeAgainAMessage() + { + var pubSubEvent = new TestablePubSubEvent(); + SubscribeExternalActionWithoutReference(pubSubEvent); + GC.Collect(); + SubscribeExternalActionWithoutReference(pubSubEvent); + pubSubEvent.Prune(); + Assert.True(pubSubEvent.BaseSubscriptions.Count == 1, "Subscriptionlist is empty"); + } + + private static void SubscribeExternalActionWithoutReference(TestablePubSubEvent pubSubEvent) + { + pubSubEvent.Subscribe(new ExternalAction().ExecuteAction); + } + + + [Test] + public void CanSubscribeAndRaiseEvent() + { + TestablePubSubEvent pubSubEvent = new TestablePubSubEvent(); + bool published = false; + pubSubEvent.Subscribe(delegate { published = true; }, ThreadOption.PublisherThread, true, delegate { return true; }); + pubSubEvent.Publish(null); + + Assert.True(published); + } + + [Test] + public void CanSubscribeAndRaiseEventNonGeneric() + { + var pubSubEvent = new TestablePubSubEvent(); + bool published = false; + pubSubEvent.Subscribe(delegate { published = true; }, ThreadOption.PublisherThread, true); + pubSubEvent.Publish(); + + Assert.True(published); + } + + [Test] + public void CanSubscribeAndRaiseCustomEvent() + { + var customEvent = new TestablePubSubEvent(); + Payload payload = new Payload(); + var action = new ActionHelper(); + customEvent.Subscribe(action.Action); + + customEvent.Publish(payload); + + Assert.AreSame(action.ActionArg(), payload); + } + + [Test] + public void CanHaveMultipleSubscribersAndRaiseCustomEvent() + { + var customEvent = new TestablePubSubEvent(); + Payload payload = new Payload(); + var action1 = new ActionHelper(); + var action2 = new ActionHelper(); + customEvent.Subscribe(action1.Action); + customEvent.Subscribe(action2.Action); + + customEvent.Publish(payload); + + Assert.AreSame(action1.ActionArg(), payload); + Assert.AreSame(action2.ActionArg(), payload); + } + + [Test] + public void CanHaveMultipleSubscribersAndRaiseEvent() + { + var customEvent = new TestablePubSubEvent(); + var action1 = new ActionHelper(); + var action2 = new ActionHelper(); + customEvent.Subscribe(action1.Action); + customEvent.Subscribe(action2.Action); + + customEvent.Publish(); + + Assert.True(action1.ActionCalled); + Assert.True(action2.ActionCalled); + } + + [Test] + public void SubscribeTakesExecuteDelegateThreadOptionAndFilter() + { + TestablePubSubEvent pubSubEvent = new TestablePubSubEvent(); + var action = new ActionHelper(); + pubSubEvent.Subscribe(action.Action); + + pubSubEvent.Publish("test"); + + Assert.AreEqual("test", action.ActionArg()); + + } + + [Test] + public void FilterEnablesActionTarget() + { + TestablePubSubEvent pubSubEvent = new TestablePubSubEvent(); + var goodFilter = new MockFilter { FilterReturnValue = true }; + var actionGoodFilter = new ActionHelper(); + var badFilter = new MockFilter { FilterReturnValue = false }; + var actionBadFilter = new ActionHelper(); + pubSubEvent.Subscribe(actionGoodFilter.Action, ThreadOption.PublisherThread, true, goodFilter.FilterString); + pubSubEvent.Subscribe(actionBadFilter.Action, ThreadOption.PublisherThread, true, badFilter.FilterString); + + pubSubEvent.Publish("test"); + + Assert.True(actionGoodFilter.ActionCalled); + Assert.False(actionBadFilter.ActionCalled); + + } + + [Test] + public void FilterEnablesActionTarget_Weak() + { + TestablePubSubEvent pubSubEvent = new TestablePubSubEvent(); + var goodFilter = new MockFilter { FilterReturnValue = true }; + var actionGoodFilter = new ActionHelper(); + var badFilter = new MockFilter { FilterReturnValue = false }; + var actionBadFilter = new ActionHelper(); + pubSubEvent.Subscribe(actionGoodFilter.Action, goodFilter.FilterString); + pubSubEvent.Subscribe(actionBadFilter.Action, badFilter.FilterString); + + pubSubEvent.Publish("test"); + + Assert.True(actionGoodFilter.ActionCalled); + Assert.False(actionBadFilter.ActionCalled); + + } + + [Test] + public void SubscribeDefaultsThreadOptionAndNoFilter() + { + TestablePubSubEvent pubSubEvent = new TestablePubSubEvent(); + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); + SynchronizationContext calledSyncContext = null; + var myAction = new ActionHelper() + { + ActionToExecute = + () => calledSyncContext = SynchronizationContext.Current + }; + pubSubEvent.Subscribe(myAction.Action); + + pubSubEvent.Publish("test"); + + Assert.AreEqual(SynchronizationContext.Current, calledSyncContext); + } + + [Test] + public void SubscribeDefaultsThreadOptionAndNoFilterNonGeneric() + { + var pubSubEvent = new TestablePubSubEvent(); + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); + SynchronizationContext calledSyncContext = null; + var myAction = new ActionHelper() + { + ActionToExecute = + () => calledSyncContext = SynchronizationContext.Current + }; + pubSubEvent.Subscribe(myAction.Action); + + pubSubEvent.Publish(); + + Assert.AreEqual(SynchronizationContext.Current, calledSyncContext); + } + + [Test] + public void ShouldUnsubscribeFromPublisherThread() + { + var PubSubEvent = new TestablePubSubEvent(); + + var actionEvent = new ActionHelper(); + PubSubEvent.Subscribe( + actionEvent.Action, + ThreadOption.PublisherThread); + + Assert.True(PubSubEvent.Contains(actionEvent.Action)); + PubSubEvent.Unsubscribe(actionEvent.Action); + Assert.False(PubSubEvent.Contains(actionEvent.Action)); + } + + [Test] + public void ShouldUnsubscribeFromPublisherThreadNonGeneric() + { + var pubSubEvent = new TestablePubSubEvent(); + + var actionEvent = new ActionHelper(); + pubSubEvent.Subscribe( + actionEvent.Action, + ThreadOption.PublisherThread); + + Assert.True(pubSubEvent.Contains(actionEvent.Action)); + pubSubEvent.Unsubscribe(actionEvent.Action); + Assert.False(pubSubEvent.Contains(actionEvent.Action)); + } + + [Test] + public void UnsubscribeShouldNotFailWithNonSubscriber() + { + TestablePubSubEvent pubSubEvent = new TestablePubSubEvent(); + + Action subscriber = delegate { }; + pubSubEvent.Unsubscribe(subscriber); + } + + [Test] + public void UnsubscribeShouldNotFailWithNonSubscriberNonGeneric() + { + var pubSubEvent = new TestablePubSubEvent(); + + Action subscriber = delegate { }; + pubSubEvent.Unsubscribe(subscriber); + } + + [Test] + public void ShouldUnsubscribeFromBackgroundThread() + { + var PubSubEvent = new TestablePubSubEvent(); + + var actionEvent = new ActionHelper(); + PubSubEvent.Subscribe( + actionEvent.Action, + ThreadOption.BackgroundThread); + + Assert.True(PubSubEvent.Contains(actionEvent.Action)); + PubSubEvent.Unsubscribe(actionEvent.Action); + Assert.False(PubSubEvent.Contains(actionEvent.Action)); + } + + [Test] + public void ShouldUnsubscribeFromBackgroundThreadNonGeneric() + { + var pubSubEvent = new TestablePubSubEvent(); + + var actionEvent = new ActionHelper(); + pubSubEvent.Subscribe( + actionEvent.Action, + ThreadOption.BackgroundThread); + + Assert.True(pubSubEvent.Contains(actionEvent.Action)); + pubSubEvent.Unsubscribe(actionEvent.Action); + Assert.False(pubSubEvent.Contains(actionEvent.Action)); + } + + [Test] + public void ShouldUnsubscribeFromUIThread() + { + var PubSubEvent = new TestablePubSubEvent(); + PubSubEvent.SynchronizationContext = new SynchronizationContext(); + + var actionEvent = new ActionHelper(); + PubSubEvent.Subscribe( + actionEvent.Action, + ThreadOption.UIThread); + + Assert.True(PubSubEvent.Contains(actionEvent.Action)); + PubSubEvent.Unsubscribe(actionEvent.Action); + Assert.False(PubSubEvent.Contains(actionEvent.Action)); + } + + [Test] + public void ShouldUnsubscribeFromUIThreadNonGeneric() + { + var pubSubEvent = new TestablePubSubEvent(); + pubSubEvent.SynchronizationContext = new SynchronizationContext(); + + var actionEvent = new ActionHelper(); + pubSubEvent.Subscribe( + actionEvent.Action, + ThreadOption.UIThread); + + Assert.True(pubSubEvent.Contains(actionEvent.Action)); + pubSubEvent.Unsubscribe(actionEvent.Action); + Assert.False(pubSubEvent.Contains(actionEvent.Action)); + } + + [Test] + public void ShouldUnsubscribeASingleDelegate() + { + var PubSubEvent = new TestablePubSubEvent(); + + int callCount = 0; + + var actionEvent = new ActionHelper() { ActionToExecute = () => callCount++ }; + PubSubEvent.Subscribe(actionEvent.Action); + PubSubEvent.Subscribe(actionEvent.Action); + + PubSubEvent.Publish(null); + Assert.AreEqual(2, callCount); + + callCount = 0; + PubSubEvent.Unsubscribe(actionEvent.Action); + PubSubEvent.Publish(null); + Assert.AreEqual(1, callCount); + } + + [Test] + public void ShouldUnsubscribeASingleDelegateNonGeneric() + { + var pubSubEvent = new TestablePubSubEvent(); + + int callCount = 0; + + var actionEvent = new ActionHelper() { ActionToExecute = () => callCount++ }; + pubSubEvent.Subscribe(actionEvent.Action); + pubSubEvent.Subscribe(actionEvent.Action); + + pubSubEvent.Publish(); + Assert.AreEqual(2, callCount); + + callCount = 0; + pubSubEvent.Unsubscribe(actionEvent.Action); + pubSubEvent.Publish(); + Assert.AreEqual(1, callCount); + } + + [Test] + public async Task ShouldNotExecuteOnGarbageCollectedDelegateReferenceWhenNotKeepAlive() + { + var PubSubEvent = new TestablePubSubEvent(); + + ExternalAction externalAction = new ExternalAction(); + PubSubEvent.Subscribe(externalAction.ExecuteAction); + + PubSubEvent.Publish("testPayload"); + Assert.AreEqual("testPayload", externalAction.PassedValue); + + WeakReference actionEventReference = new WeakReference(externalAction); + externalAction = null; + await Task.Delay(100); + GC.Collect(); + Assert.False(actionEventReference.IsAlive); + + PubSubEvent.Publish("testPayload"); + } + + [Test] + public async Task ShouldNotExecuteOnGarbageCollectedDelegateReferenceWhenNotKeepAliveNonGeneric() + { + var pubSubEvent = new TestablePubSubEvent(); + + var externalAction = new ExternalAction(); + pubSubEvent.Subscribe(externalAction.ExecuteAction); + + pubSubEvent.Publish(); + Assert.True(externalAction.Executed); + + var actionEventReference = new WeakReference(externalAction); + externalAction = null; + await Task.Delay(100); + GC.Collect(); + Assert.False(actionEventReference.IsAlive); + + pubSubEvent.Publish(); + } + + [Test] + public async Task ShouldNotExecuteOnGarbageCollectedFilterReferenceWhenNotKeepAlive() + { + var PubSubEvent = new TestablePubSubEvent(); + + bool wasCalled = false; + var actionEvent = new ActionHelper() { ActionToExecute = () => wasCalled = true }; + + ExternalFilter filter = new ExternalFilter(); + PubSubEvent.Subscribe(actionEvent.Action, ThreadOption.PublisherThread, false, filter.AlwaysTrueFilter); + + PubSubEvent.Publish("testPayload"); + Assert.True(wasCalled); + + wasCalled = false; + WeakReference filterReference = new WeakReference(filter); + filter = null; + await Task.Delay(100); + GC.Collect(); + Assert.False(filterReference.IsAlive); + + PubSubEvent.Publish("testPayload"); + Assert.False(wasCalled); + } + + [Test] + public void CanAddSubscriptionWhileEventIsFiring() + { + var PubSubEvent = new TestablePubSubEvent(); + + var emptyAction = new ActionHelper(); + var subscriptionAction = new ActionHelper + { + ActionToExecute = (() => + PubSubEvent.Subscribe( + emptyAction.Action)) + }; + + PubSubEvent.Subscribe(subscriptionAction.Action); + + Assert.False(PubSubEvent.Contains(emptyAction.Action)); + + PubSubEvent.Publish(null); + + Assert.True((PubSubEvent.Contains(emptyAction.Action))); + } + + [Test] + public void CanAddSubscriptionWhileEventIsFiringNonGeneric() + { + var pubSubEvent = new TestablePubSubEvent(); + + var emptyAction = new ActionHelper(); + var subscriptionAction = new ActionHelper + { + ActionToExecute = (() => + pubSubEvent.Subscribe( + emptyAction.Action)) + }; + + pubSubEvent.Subscribe(subscriptionAction.Action); + + Assert.False(pubSubEvent.Contains(emptyAction.Action)); + + pubSubEvent.Publish(); + + Assert.True((pubSubEvent.Contains(emptyAction.Action))); + } + + [Test] + public void InlineDelegateDeclarationsDoesNotGetCollectedIncorrectlyWithWeakReferences() + { + var PubSubEvent = new TestablePubSubEvent(); + bool published = false; + PubSubEvent.Subscribe(delegate { published = true; }, ThreadOption.PublisherThread, false, delegate { return true; }); + GC.Collect(); + PubSubEvent.Publish(null); + + Assert.True(published); + } + + [Test] + public void InlineDelegateDeclarationsDoesNotGetCollectedIncorrectlyWithWeakReferencesNonGeneric() + { + var pubSubEvent = new TestablePubSubEvent(); + bool published = false; + pubSubEvent.Subscribe(delegate { published = true; }, ThreadOption.PublisherThread, false); + GC.Collect(); + pubSubEvent.Publish(); + + Assert.True(published); + } + + [Test] + public void ShouldNotGarbageCollectDelegateReferenceWhenUsingKeepAlive() + { + var PubSubEvent = new TestablePubSubEvent(); + + var externalAction = new ExternalAction(); + PubSubEvent.Subscribe(externalAction.ExecuteAction, ThreadOption.PublisherThread, true); + + WeakReference actionEventReference = new WeakReference(externalAction); + externalAction = null; + GC.Collect(); + GC.Collect(); + Assert.True(actionEventReference.IsAlive); + + PubSubEvent.Publish("testPayload"); + + Assert.AreEqual("testPayload", ((ExternalAction)actionEventReference.Target).PassedValue); + } + + [Test] + public void ShouldNotGarbageCollectDelegateReferenceWhenUsingKeepAliveNonGeneric() + { + var pubSubEvent = new TestablePubSubEvent(); + + var externalAction = new ExternalAction(); + pubSubEvent.Subscribe(externalAction.ExecuteAction, ThreadOption.PublisherThread, true); + + WeakReference actionEventReference = new WeakReference(externalAction); + externalAction = null; + GC.Collect(); + GC.Collect(); + Assert.True(actionEventReference.IsAlive); + + pubSubEvent.Publish(); + + Assert.True(((ExternalAction)actionEventReference.Target).Executed); + } + + [Test] + public void RegisterReturnsTokenThatCanBeUsedToUnsubscribe() + { + var PubSubEvent = new TestablePubSubEvent(); + var emptyAction = new ActionHelper(); + + var token = PubSubEvent.Subscribe(emptyAction.Action); + PubSubEvent.Unsubscribe(token); + + Assert.False(PubSubEvent.Contains(emptyAction.Action)); + } + + [Test] + public void RegisterReturnsTokenThatCanBeUsedToUnsubscribeNonGeneric() + { + var pubSubEvent = new TestablePubSubEvent(); + var emptyAction = new ActionHelper(); + + var token = pubSubEvent.Subscribe(emptyAction.Action); + pubSubEvent.Unsubscribe(token); + + Assert.False(pubSubEvent.Contains(emptyAction.Action)); + } + + [Test] + public void ContainsShouldSearchByToken() + { + var PubSubEvent = new TestablePubSubEvent(); + var emptyAction = new ActionHelper(); + var token = PubSubEvent.Subscribe(emptyAction.Action); + + Assert.True(PubSubEvent.Contains(token)); + + PubSubEvent.Unsubscribe(emptyAction.Action); + Assert.False(PubSubEvent.Contains(token)); + } + + [Test] + public void ContainsShouldSearchByTokenNonGeneric() + { + var pubSubEvent = new TestablePubSubEvent(); + var emptyAction = new ActionHelper(); + var token = pubSubEvent.Subscribe(emptyAction.Action); + + Assert.True(pubSubEvent.Contains(token)); + + pubSubEvent.Unsubscribe(emptyAction.Action); + Assert.False(pubSubEvent.Contains(token)); + } + + [Test] + public void SubscribeDefaultsToPublisherThread() + { + var PubSubEvent = new TestablePubSubEvent(); + Action action = delegate { }; + var token = PubSubEvent.Subscribe(action, true); + + Assert.AreEqual(1, PubSubEvent.BaseSubscriptions.Count); + Assert.AreEqual(typeof(EventSubscription), PubSubEvent.BaseSubscriptions.ElementAt(0).GetType()); + } + + [Test] + public void SubscribeDefaultsToPublisherThreadNonGeneric() + { + var pubSubEvent = new TestablePubSubEvent(); + Action action = delegate { }; + var token = pubSubEvent.Subscribe(action, true); + + Assert.AreEqual(1, pubSubEvent.BaseSubscriptions.Count); + Assert.AreEqual(typeof(EventSubscription), pubSubEvent.BaseSubscriptions.ElementAt(0).GetType()); + } + + public class ExternalFilter + { + public bool AlwaysTrueFilter(string value) + { + return true; + } + } + + public class ExternalAction + { + public string PassedValue; + public bool Executed = false; + + public void ExecuteAction(string value) + { + PassedValue = value; + Executed = true; + } + + public void ExecuteAction() + { + Executed = true; + } + } + + class TestablePubSubEvent : PubSubEvent + { + public ICollection BaseSubscriptions + { + get { return base.Subscriptions; } + } + } + + class TestablePubSubEvent : PubSubEvent + { + public ICollection BaseSubscriptions + { + get { return base.Subscriptions; } + } + } + + public class Payload { } + } + + public class ActionHelper + { + public bool ActionCalled; + public Action ActionToExecute = null; + private object actionArg; + + public T ActionArg() + { + return (T)actionArg; + } + + public void Action(TestPubSubEvent.Payload arg) + { + Action((object)arg); + } + + public void Action(string arg) + { + Action((object)arg); + } + + public void Action(object arg) + { + actionArg = arg; + ActionCalled = true; + if (ActionToExecute != null) + { + ActionToExecute.Invoke(); + } + } + + public void Action() + { + ActionCalled = true; + if (ActionToExecute != null) + { + ActionToExecute.Invoke(); + } + } + } + + public class MockFilter + { + public bool FilterReturnValue; + + public bool FilterString(string arg) + { + return FilterReturnValue; + } + } +} + +#endif \ No newline at end of file diff --git a/src/Lucene.Net/Index/IndexReader.cs b/src/Lucene.Net/Index/IndexReader.cs index c80ed743e6..f8fa305276 100644 --- a/src/Lucene.Net/Index/IndexReader.cs +++ b/src/Lucene.Net/Index/IndexReader.cs @@ -4,7 +4,7 @@ using Lucene.Net.Support.Threading; using Lucene.Net.Util; #if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR -using Prism.Events; +using Lucene.Net.Util.Events; #endif using System; using System.Collections; diff --git a/src/Lucene.Net/Lucene.Net.csproj b/src/Lucene.Net/Lucene.Net.csproj index 3175904396..a1889b96bd 100644 --- a/src/Lucene.Net/Lucene.Net.csproj +++ b/src/Lucene.Net/Lucene.Net.csproj @@ -59,12 +59,10 @@ - - diff --git a/src/Lucene.Net/Search/CachingWrapperFilter.cs b/src/Lucene.Net/Search/CachingWrapperFilter.cs index ab0a8900db..ece5aab600 100644 --- a/src/Lucene.Net/Search/CachingWrapperFilter.cs +++ b/src/Lucene.Net/Search/CachingWrapperFilter.cs @@ -3,7 +3,7 @@ using Lucene.Net.Support.Threading; using Lucene.Net.Util; #if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR -using Prism.Events; +using Lucene.Net.Util.Events; #endif using System.Collections.Generic; using System.Runtime.CompilerServices; diff --git a/src/Lucene.Net/Search/FieldCacheImpl.cs b/src/Lucene.Net/Search/FieldCacheImpl.cs index ad1703db59..5f40e42034 100644 --- a/src/Lucene.Net/Search/FieldCacheImpl.cs +++ b/src/Lucene.Net/Search/FieldCacheImpl.cs @@ -7,7 +7,7 @@ using Lucene.Net.Support.Threading; using Lucene.Net.Util; #if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR -using Prism.Events; +using Lucene.Net.Util.Events; #endif using System; using System.Collections.Generic; diff --git a/src/Lucene.Net/Support/Util/Events/BackgroundEventSubscription.cs b/src/Lucene.Net/Support/Util/Events/BackgroundEventSubscription.cs new file mode 100644 index 0000000000..ced968e082 --- /dev/null +++ b/src/Lucene.Net/Support/Util/Events/BackgroundEventSubscription.cs @@ -0,0 +1,84 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using System; +using System.Threading.Tasks; + +namespace Lucene.Net.Util.Events +{ + /// + /// Extends to invoke the delegate in a background thread. + /// + internal class BackgroundEventSubscription : EventSubscription + { + /// + /// Creates a new instance of . + /// + /// A reference to a delegate of type . + /// When or are . + /// When the target of is not of type . + public BackgroundEventSubscription(IDelegateReference actionReference) + : base(actionReference) + { + } + + /// + /// Invokes the specified in an asynchronous thread by using a . + /// + /// The action to execute. + public override void InvokeAction(Action action) + { + Task.Run(action); + } + } + /// + /// Extends to invoke the delegate in a background thread. + /// + /// The type to use for the generic and types. + internal class BackgroundEventSubscription : EventSubscription + { + /// + /// Creates a new instance of . + /// + /// A reference to a delegate of type . + /// A reference to a delegate of type . + /// When or are . + /// When the target of is not of type , + /// or the target of is not of type . + public BackgroundEventSubscription(IDelegateReference actionReference, IDelegateReference filterReference) + : base(actionReference, filterReference) + { + } + + /// + /// Invokes the specified in an asynchronous thread by using a . + /// + /// The action to execute. + /// The payload to pass while invoking it. + public override void InvokeAction(Action action, TPayload argument) + { + //ThreadPool.QueueUserWorkItem( (o) => action(argument) ); + Task.Run(() => action(argument)); + } + } +} +#endif diff --git a/src/Lucene.Net/Support/Util/Events/DelegateReference.cs b/src/Lucene.Net/Support/Util/Events/DelegateReference.cs new file mode 100644 index 0000000000..5ab8bfd416 --- /dev/null +++ b/src/Lucene.Net/Support/Util/Events/DelegateReference.cs @@ -0,0 +1,117 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using System; +using System.Reflection; + +namespace Lucene.Net.Util.Events +{ + /// + /// Represents a reference to a that may contain a + /// to the target. This class is used + /// internally. + /// + internal class DelegateReference : IDelegateReference + { + private readonly Delegate _delegate; + private readonly WeakReference _weakReference; + private readonly MethodInfo _method; + private readonly Type _delegateType; + + /// + /// Initializes a new instance of . + /// + /// The original to create a reference for. + /// If the class will create a weak reference to the delegate, allowing it to be garbage collected. Otherwise it will keep a strong reference to the target. + /// If the passed is not assignable to . + public DelegateReference(Delegate @delegate, bool keepReferenceAlive) + { + if (@delegate == null) + throw new ArgumentNullException("delegate"); + + if (keepReferenceAlive) + { + this._delegate = @delegate; + } + else + { + _weakReference = new WeakReference(@delegate.Target); + _method = @delegate.GetMethodInfo(); + _delegateType = @delegate.GetType(); + } + } + + /// + /// Gets the (the target) referenced by the current object. + /// + /// if the object referenced by the current object has been garbage collected; otherwise, a reference to the referenced by the current object. + public Delegate Target + { + get + { + if (_delegate != null) + { + return _delegate; + } + else + { + return TryGetDelegate(); + } + } + } + + /// + /// Checks if the (the target) referenced by the current object are equal to another . + /// This is equivalent with comparing with , only more efficient. + /// + /// The other delegate to compare with. + /// True if the target referenced by the current object are equal to . + public bool TargetEquals(Delegate @delegate) + { + if (_delegate != null) + { + return _delegate == @delegate; + } + if (@delegate == null) + { + return !_method.IsStatic && !_weakReference.IsAlive; + } + return _weakReference.Target == @delegate.Target && Equals(_method, @delegate.GetMethodInfo()); + } + + private Delegate TryGetDelegate() + { + if (_method.IsStatic) + { + return _method.CreateDelegate(_delegateType, null); + } + object target = _weakReference.Target; + if (target != null) + { + return _method.CreateDelegate(_delegateType, target); + } + return null; + } + } +} + +#endif \ No newline at end of file diff --git a/src/Lucene.Net/Support/Util/Events/DispatcherEventSubscription.cs b/src/Lucene.Net/Support/Util/Events/DispatcherEventSubscription.cs new file mode 100644 index 0000000000..b56ee88470 --- /dev/null +++ b/src/Lucene.Net/Support/Util/Events/DispatcherEventSubscription.cs @@ -0,0 +1,95 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using System; +using System.Threading; + +namespace Lucene.Net.Util.Events +{ + /// + /// Extends to invoke the delegate + /// in a specific . + /// + internal class DispatcherEventSubscription : EventSubscription + { + private readonly SynchronizationContext syncContext; + + /// + /// Creates a new instance of . + /// + ///A reference to a delegate of type . + ///The synchronization context to use for UI thread dispatching. + ///When or are . + ///When the target of is not of type . + public DispatcherEventSubscription(IDelegateReference actionReference, SynchronizationContext context) + : base(actionReference) + { + syncContext = context; + } + + /// + /// Invokes the specified asynchronously in the specified . + /// + /// The action to execute. + public override void InvokeAction(Action action) + { + syncContext.Post((o) => action(), null); + } + } + + /// + /// Extends to invoke the delegate + /// in a specific . + /// + /// The type to use for the generic and types. + internal class DispatcherEventSubscription : EventSubscription + { + private readonly SynchronizationContext syncContext; + + /// + /// Creates a new instance of . + /// + ///A reference to a delegate of type . + ///A reference to a delegate of type . + ///The synchronization context to use for UI thread dispatching. + ///When or are . + ///When the target of is not of type , + ///or the target of is not of type . + public DispatcherEventSubscription(IDelegateReference actionReference, IDelegateReference filterReference, SynchronizationContext context) + : base(actionReference, filterReference) + { + syncContext = context; + } + + /// + /// Invokes the specified asynchronously in the specified . + /// + /// The action to execute. + /// The payload to pass while invoking it. + public override void InvokeAction(Action action, TPayload argument) + { + syncContext.Post((o) => action((TPayload)o), argument); + } + } +} + +#endif diff --git a/src/Lucene.Net/Support/Util/Events/EventAggregator.cs b/src/Lucene.Net/Support/Util/Events/EventAggregator.cs new file mode 100644 index 0000000000..0c753b16c5 --- /dev/null +++ b/src/Lucene.Net/Support/Util/Events/EventAggregator.cs @@ -0,0 +1,68 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Lucene.Net.Util.Events +{ + /// + /// Implements . + /// + internal class EventAggregator : IEventAggregator + { + private readonly Dictionary events = new Dictionary(); + // Captures the sync context for the UI thread when constructed on the UI thread + // in a platform agnostic way so it can be used for UI thread dispatching + private readonly SynchronizationContext syncContext = SynchronizationContext.Current; + + /// + /// Gets the single instance of the event managed by this EventAggregator. Multiple calls to this method with the same returns the same event instance. + /// + /// The type of event to get. This must inherit from . + /// A singleton instance of an event object of type . + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")] + public TEventType GetEvent() where TEventType : EventBase, new() + { + lock (events) + { + EventBase existingEvent = null; + + if (!events.TryGetValue(typeof(TEventType), out existingEvent)) + { + TEventType newEvent = new TEventType(); + newEvent.SynchronizationContext = syncContext; + events[typeof(TEventType)] = newEvent; + + return newEvent; + } + else + { + return (TEventType)existingEvent; + } + } + } + } +} + +#endif diff --git a/src/Lucene.Net/Support/Util/Events/EventBase.cs b/src/Lucene.Net/Support/Util/Events/EventBase.cs new file mode 100644 index 0000000000..7d087d5051 --- /dev/null +++ b/src/Lucene.Net/Support/Util/Events/EventBase.cs @@ -0,0 +1,163 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Lucene.Net.Util.Events +{ + /// + /// Defines a base class to publish and subscribe to events. + /// + internal abstract class EventBase + { + private readonly List _subscriptions = new List(); + + /// + /// Allows the SynchronizationContext to be set by the EventAggregator for UI Thread Dispatching + /// + public SynchronizationContext SynchronizationContext { get; set; } + + /// + /// Gets the list of current subscriptions. + /// + /// The current subscribers. + protected ICollection Subscriptions + { + get { return _subscriptions; } + } + + /// + /// Adds the specified to the subscribers' collection. + /// + /// The subscriber. + /// The that uniquely identifies every subscriber. + /// + /// Adds the subscription to the internal list and assigns it a new . + /// + protected virtual SubscriptionToken InternalSubscribe(IEventSubscription eventSubscription) + { + if (eventSubscription == null) throw new ArgumentNullException(nameof(eventSubscription)); + + eventSubscription.SubscriptionToken = new SubscriptionToken(Unsubscribe); + + lock (Subscriptions) + { + Subscriptions.Add(eventSubscription); + } + return eventSubscription.SubscriptionToken; + } + + /// + /// Calls all the execution strategies exposed by the list of . + /// + /// The arguments that will be passed to the listeners. + /// Before executing the strategies, this class will prune all the subscribers from the + /// list that return a when calling the + /// method. + protected virtual void InternalPublish(params object[] arguments) + { + List> executionStrategies = PruneAndReturnStrategies(); + foreach (var executionStrategy in executionStrategies) + { + executionStrategy(arguments); + } + } + + /// + /// Removes the subscriber matching the . + /// + /// The returned by while subscribing to the event. + public virtual void Unsubscribe(SubscriptionToken token) + { + lock (Subscriptions) + { + IEventSubscription subscription = Subscriptions.FirstOrDefault(evt => evt.SubscriptionToken == token); + if (subscription != null) + { + Subscriptions.Remove(subscription); + } + } + } + + /// + /// Returns if there is a subscriber matching . + /// + /// The returned by while subscribing to the event. + /// if there is a that matches; otherwise . + public virtual bool Contains(SubscriptionToken token) + { + lock (Subscriptions) + { + IEventSubscription subscription = Subscriptions.FirstOrDefault(evt => evt.SubscriptionToken == token); + return subscription != null; + } + } + + private List> PruneAndReturnStrategies() + { + List> returnList = new List>(); + + lock (Subscriptions) + { + for (var i = Subscriptions.Count - 1; i >= 0; i--) + { + Action listItem = + _subscriptions[i].GetExecutionStrategy(); + + if (listItem == null) + { + // Prune from main list. Log? + _subscriptions.RemoveAt(i); + } + else + { + returnList.Add(listItem); + } + } + } + + return returnList; + } + + /// + /// Forces the PubSubEvent to remove any subscriptions that no longer have an execution strategy. + /// + public void Prune() + { + lock (Subscriptions) + { + for (var i = Subscriptions.Count - 1; i >= 0; i--) + { + if (_subscriptions[i].GetExecutionStrategy() == null) + { + _subscriptions.RemoveAt(i); + } + } + } + } + } +} + +#endif diff --git a/src/Lucene.Net/Support/Util/Events/EventSubscription.cs b/src/Lucene.Net/Support/Util/Events/EventSubscription.cs new file mode 100644 index 0000000000..d5ee36290e --- /dev/null +++ b/src/Lucene.Net/Support/Util/Events/EventSubscription.cs @@ -0,0 +1,214 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using System; +using System.Globalization; + +namespace Lucene.Net.Util.Events +{ + /// + /// Provides a way to retrieve a to execute an action depending + /// on the value of a second filter predicate that returns true if the action should execute. + /// + internal class EventSubscription : IEventSubscription + { + private readonly IDelegateReference _actionReference; + + /// + /// Creates a new instance of . + /// + ///A reference to a delegate of type . + ///When or are . + ///When the target of is not of type . + public EventSubscription(IDelegateReference actionReference) + { + if (actionReference == null) + throw new ArgumentNullException(nameof(actionReference)); + if (!(actionReference.Target is Action)) + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.InvalidDelegateRerefenceTypeException, typeof(Action).FullName), nameof(actionReference)); + + _actionReference = actionReference; + } + + /// + /// Gets the target that is referenced by the . + /// + /// An or if the referenced target is not alive. + public Action Action + { + get { return (Action)_actionReference.Target; } + } + + /// + /// Gets or sets a that identifies this . + /// + /// A token that identifies this . + public SubscriptionToken SubscriptionToken { get; set; } + + /// + /// Gets the execution strategy to publish this event. + /// + /// An with the execution strategy, or if the is no longer valid. + /// + /// If is no longer valid because it was + /// garbage collected, this method will return . + /// Otherwise it will return a delegate that evaluates the and if it + /// returns will then call . The returned + /// delegate holds a hard reference to the target + /// delegates. As long as the returned delegate is not garbage collected, + /// the references delegates won't get collected either. + /// + public virtual Action GetExecutionStrategy() + { + Action action = this.Action; + if (action != null) + { + return arguments => + { + InvokeAction(action); + }; + } + return null; + } + + /// + /// Invokes the specified synchronously when not overridden. + /// + /// The action to execute. + /// An is thrown if is null. + public virtual void InvokeAction(Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + action(); + } + } + + /// + /// Provides a way to retrieve a to execute an action depending + /// on the value of a second filter predicate that returns true if the action should execute. + /// + /// The type to use for the generic and types. + internal class EventSubscription : IEventSubscription + { + private readonly IDelegateReference _actionReference; + private readonly IDelegateReference _filterReference; + + /// + /// Creates a new instance of . + /// + ///A reference to a delegate of type . + ///A reference to a delegate of type . + ///When or are . + ///When the target of is not of type , + ///or the target of is not of type . + public EventSubscription(IDelegateReference actionReference, IDelegateReference filterReference) + { + if (actionReference == null) + throw new ArgumentNullException(nameof(actionReference)); + if (!(actionReference.Target is Action)) + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.InvalidDelegateRerefenceTypeException, typeof(Action).FullName), nameof(actionReference)); + + if (filterReference == null) + throw new ArgumentNullException(nameof(filterReference)); + if (!(filterReference.Target is Predicate)) + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.InvalidDelegateRerefenceTypeException, typeof(Predicate).FullName), nameof(filterReference)); + + _actionReference = actionReference; + _filterReference = filterReference; + } + + /// + /// Gets the target that is referenced by the . + /// + /// An or if the referenced target is not alive. + public Action Action + { + get { return (Action)_actionReference.Target; } + } + + /// + /// Gets the target that is referenced by the . + /// + /// An or if the referenced target is not alive. + public Predicate Filter + { + get { return (Predicate)_filterReference.Target; } + } + + /// + /// Gets or sets a that identifies this . + /// + /// A token that identifies this . + public SubscriptionToken SubscriptionToken { get; set; } + + /// + /// Gets the execution strategy to publish this event. + /// + /// An with the execution strategy, or if the is no longer valid. + /// + /// If or are no longer valid because they were + /// garbage collected, this method will return . + /// Otherwise it will return a delegate that evaluates the and if it + /// returns will then call . The returned + /// delegate holds hard references to the and target + /// delegates. As long as the returned delegate is not garbage collected, + /// the and references delegates won't get collected either. + /// + public virtual Action GetExecutionStrategy() + { + Action action = this.Action; + Predicate filter = this.Filter; + if (action != null && filter != null) + { + return arguments => + { + TPayload argument = default(TPayload); + if (arguments != null && arguments.Length > 0 && arguments[0] != null) + { + argument = (TPayload)arguments[0]; + } + if (filter(argument)) + { + InvokeAction(action, argument); + } + }; + } + return null; + } + + /// + /// Invokes the specified synchronously when not overridden. + /// + /// The action to execute. + /// The payload to pass while invoking it. + /// An is thrown if is null. + public virtual void InvokeAction(Action action, TPayload argument) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + action(argument); + } + } +} + +#endif diff --git a/src/Lucene.Net/Support/Util/Events/IDelegateReference.cs b/src/Lucene.Net/Support/Util/Events/IDelegateReference.cs new file mode 100644 index 0000000000..003cf52d30 --- /dev/null +++ b/src/Lucene.Net/Support/Util/Events/IDelegateReference.cs @@ -0,0 +1,40 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using System; + +namespace Lucene.Net.Util.Events +{ + /// + /// Represents a reference to a . + /// + internal interface IDelegateReference + { + /// + /// Gets the referenced object. + /// + /// A instance if the target is valid; otherwise . + Delegate Target { get; } + } +} + +#endif \ No newline at end of file diff --git a/src/Lucene.Net/Support/Util/Events/IEventAggregator.cs b/src/Lucene.Net/Support/Util/Events/IEventAggregator.cs new file mode 100644 index 0000000000..585e69b153 --- /dev/null +++ b/src/Lucene.Net/Support/Util/Events/IEventAggregator.cs @@ -0,0 +1,40 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +namespace Lucene.Net.Util.Events +{ + /// + /// Defines an interface to get instances of an event type. + /// + internal interface IEventAggregator + { + /// + /// Gets an instance of an event type. + /// + /// The type of event to get. + /// An instance of an event object of type . + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")] + TEventType GetEvent() where TEventType : EventBase, new(); + } +} + +#endif \ No newline at end of file diff --git a/src/Lucene.Net/Support/Util/Events/IEventSubscription.cs b/src/Lucene.Net/Support/Util/Events/IEventSubscription.cs new file mode 100644 index 0000000000..c34fed1384 --- /dev/null +++ b/src/Lucene.Net/Support/Util/Events/IEventSubscription.cs @@ -0,0 +1,47 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using System; + +namespace Lucene.Net.Util.Events +{ + /// + /// Defines a contract for an event subscription to be used by . + /// + internal interface IEventSubscription + { + /// + /// Gets or sets a that identifies this . + /// + /// A token that identifies this . + SubscriptionToken SubscriptionToken { get; set; } + + /// + /// Gets the execution strategy to publish this event. + /// + /// An with the execution strategy, or if the is no longer valid. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + Action GetExecutionStrategy(); + } +} + +#endif diff --git a/src/Lucene.Net/Support/Util/Events/PubSubEvent.cs b/src/Lucene.Net/Support/Util/Events/PubSubEvent.cs new file mode 100644 index 0000000000..d90f41234b --- /dev/null +++ b/src/Lucene.Net/Support/Util/Events/PubSubEvent.cs @@ -0,0 +1,329 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Lucene.Net.Util.Events +{ + /// + /// Defines a class that manages publication and subscription to events. + /// + internal class PubSubEvent : EventBase + { + /// + /// Subscribes a delegate to an event that will be published on the . + /// will maintain a to the target of the supplied delegate. + /// + /// The delegate that gets executed when the event is published. + /// A that uniquely identifies the added subscription. + /// + /// The PubSubEvent collection is thread-safe. + /// + public SubscriptionToken Subscribe(Action action) + { + return Subscribe(action, ThreadOption.PublisherThread); + } + + /// + /// Subscribes a delegate to an event. + /// PubSubEvent will maintain a to the Target of the supplied delegate. + /// + /// The delegate that gets executed when the event is raised. + /// Specifies on which thread to receive the delegate callback. + /// A that uniquely identifies the added subscription. + /// + /// The PubSubEvent collection is thread-safe. + /// + public SubscriptionToken Subscribe(Action action, ThreadOption threadOption) + { + return Subscribe(action, threadOption, false); + } + + /// + /// Subscribes a delegate to an event that will be published on the . + /// + /// The delegate that gets executed when the event is published. + /// When , the keeps a reference to the subscriber so it does not get garbage collected. + /// A that uniquely identifies the added subscription. + /// + /// If is set to , will maintain a to the Target of the supplied delegate. + /// If not using a WeakReference ( is ), the user must explicitly call Unsubscribe for the event when disposing the subscriber in order to avoid memory leaks or unexpected behavior. + /// + /// The PubSubEvent collection is thread-safe. + /// + public SubscriptionToken Subscribe(Action action, bool keepSubscriberReferenceAlive) + { + return Subscribe(action, ThreadOption.PublisherThread, keepSubscriberReferenceAlive); + } + + /// + /// Subscribes a delegate to an event. + /// + /// The delegate that gets executed when the event is published. + /// Specifies on which thread to receive the delegate callback. + /// When , the keeps a reference to the subscriber so it does not get garbage collected. + /// A that uniquely identifies the added subscription. + /// + /// If is set to , will maintain a to the Target of the supplied delegate. + /// If not using a WeakReference ( is ), the user must explicitly call Unsubscribe for the event when disposing the subscriber in order to avoid memory leaks or unexpected behavior. + /// + /// The PubSubEvent collection is thread-safe. + /// + public virtual SubscriptionToken Subscribe(Action action, ThreadOption threadOption, bool keepSubscriberReferenceAlive) + { + IDelegateReference actionReference = new DelegateReference(action, keepSubscriberReferenceAlive); + + EventSubscription subscription; + switch (threadOption) + { + case ThreadOption.PublisherThread: + subscription = new EventSubscription(actionReference); + break; + case ThreadOption.BackgroundThread: + subscription = new BackgroundEventSubscription(actionReference); + break; + case ThreadOption.UIThread: + if (SynchronizationContext == null) throw new InvalidOperationException(Resources.EventAggregatorNotConstructedOnUIThread); + subscription = new DispatcherEventSubscription(actionReference, SynchronizationContext); + break; + default: + subscription = new EventSubscription(actionReference); + break; + } + + return InternalSubscribe(subscription); + } + + /// + /// Publishes the . + /// + public virtual void Publish() + { + InternalPublish(); + } + + /// + /// Removes the first subscriber matching from the subscribers' list. + /// + /// The used when subscribing to the event. + public virtual void Unsubscribe(Action subscriber) + { + lock (Subscriptions) + { + IEventSubscription eventSubscription = Subscriptions.Cast().FirstOrDefault(evt => evt.Action == subscriber); + if (eventSubscription != null) + { + Subscriptions.Remove(eventSubscription); + } + } + } + + /// + /// Returns if there is a subscriber matching . + /// + /// The used when subscribing to the event. + /// if there is an that matches; otherwise . + public virtual bool Contains(Action subscriber) + { + IEventSubscription eventSubscription; + lock (Subscriptions) + { + eventSubscription = Subscriptions.Cast().FirstOrDefault(evt => evt.Action == subscriber); + } + return eventSubscription != null; + } + } + + /// + /// Defines a class that manages publication and subscription to events. + /// + /// The type of message that will be passed to the subscribers. + internal class PubSubEvent : EventBase + { + /// + /// Subscribes a delegate to an event that will be published on the . + /// will maintain a to the target of the supplied delegate. + /// + /// The delegate that gets executed when the event is published. + /// A that uniquely identifies the added subscription. + /// + /// The PubSubEvent collection is thread-safe. + /// + public SubscriptionToken Subscribe(Action action) + { + return Subscribe(action, ThreadOption.PublisherThread); + } + + /// + /// Subscribes a delegate to an event that will be published on the + /// + /// The delegate that gets executed when the event is raised. + /// Filter to evaluate if the subscriber should receive the event. + /// A that uniquely identifies the added subscription. + public virtual SubscriptionToken Subscribe(Action action, Predicate filter) + { + return Subscribe(action, ThreadOption.PublisherThread, false, filter); + } + + /// + /// Subscribes a delegate to an event. + /// PubSubEvent will maintain a to the Target of the supplied delegate. + /// + /// The delegate that gets executed when the event is raised. + /// Specifies on which thread to receive the delegate callback. + /// A that uniquely identifies the added subscription. + /// + /// The PubSubEvent collection is thread-safe. + /// + public SubscriptionToken Subscribe(Action action, ThreadOption threadOption) + { + return Subscribe(action, threadOption, false); + } + + /// + /// Subscribes a delegate to an event that will be published on the . + /// + /// The delegate that gets executed when the event is published. + /// When , the keeps a reference to the subscriber so it does not get garbage collected. + /// A that uniquely identifies the added subscription. + /// + /// If is set to , will maintain a to the Target of the supplied delegate. + /// If not using a WeakReference ( is ), the user must explicitly call Unsubscribe for the event when disposing the subscriber in order to avoid memory leaks or unexpected behavior. + /// + /// The PubSubEvent collection is thread-safe. + /// + public SubscriptionToken Subscribe(Action action, bool keepSubscriberReferenceAlive) + { + return Subscribe(action, ThreadOption.PublisherThread, keepSubscriberReferenceAlive); + } + + /// + /// Subscribes a delegate to an event. + /// + /// The delegate that gets executed when the event is published. + /// Specifies on which thread to receive the delegate callback. + /// When , the keeps a reference to the subscriber so it does not get garbage collected. + /// A that uniquely identifies the added subscription. + /// + /// If is set to , will maintain a to the Target of the supplied delegate. + /// If not using a WeakReference ( is ), the user must explicitly call Unsubscribe for the event when disposing the subscriber in order to avoid memory leaks or unexpected behavior. + /// + /// The PubSubEvent collection is thread-safe. + /// + public SubscriptionToken Subscribe(Action action, ThreadOption threadOption, bool keepSubscriberReferenceAlive) + { + return Subscribe(action, threadOption, keepSubscriberReferenceAlive, null); + } + + /// + /// Subscribes a delegate to an event. + /// + /// The delegate that gets executed when the event is published. + /// Specifies on which thread to receive the delegate callback. + /// When , the keeps a reference to the subscriber so it does not get garbage collected. + /// Filter to evaluate if the subscriber should receive the event. + /// A that uniquely identifies the added subscription. + /// + /// If is set to , will maintain a to the Target of the supplied delegate. + /// If not using a WeakReference ( is ), the user must explicitly call Unsubscribe for the event when disposing the subscriber in order to avoid memory leaks or unexpected behavior. + /// + /// The PubSubEvent collection is thread-safe. + /// + public virtual SubscriptionToken Subscribe(Action action, ThreadOption threadOption, bool keepSubscriberReferenceAlive, Predicate filter) + { + IDelegateReference actionReference = new DelegateReference(action, keepSubscriberReferenceAlive); + IDelegateReference filterReference; + if (filter != null) + { + filterReference = new DelegateReference(filter, keepSubscriberReferenceAlive); + } + else + { + filterReference = new DelegateReference(new Predicate(delegate { return true; }), true); + } + EventSubscription subscription; + switch (threadOption) + { + case ThreadOption.PublisherThread: + subscription = new EventSubscription(actionReference, filterReference); + break; + case ThreadOption.BackgroundThread: + subscription = new BackgroundEventSubscription(actionReference, filterReference); + break; + case ThreadOption.UIThread: + if (SynchronizationContext == null) throw new InvalidOperationException(Resources.EventAggregatorNotConstructedOnUIThread); + subscription = new DispatcherEventSubscription(actionReference, filterReference, SynchronizationContext); + break; + default: + subscription = new EventSubscription(actionReference, filterReference); + break; + } + + return InternalSubscribe(subscription); + } + + /// + /// Publishes the . + /// + /// Message to pass to the subscribers. + public virtual void Publish(TPayload payload) + { + InternalPublish(payload); + } + + /// + /// Removes the first subscriber matching from the subscribers' list. + /// + /// The used when subscribing to the event. + public virtual void Unsubscribe(Action subscriber) + { + lock (Subscriptions) + { + IEventSubscription eventSubscription = Subscriptions.Cast>().FirstOrDefault(evt => evt.Action == subscriber); + if (eventSubscription != null) + { + Subscriptions.Remove(eventSubscription); + } + } + } + + /// + /// Returns if there is a subscriber matching . + /// + /// The used when subscribing to the event. + /// if there is an that matches; otherwise . + public virtual bool Contains(Action subscriber) + { + IEventSubscription eventSubscription; + lock (Subscriptions) + { + eventSubscription = Subscriptions.Cast>().FirstOrDefault(evt => evt.Action == subscriber); + } + return eventSubscription != null; + } + } +} + +#endif \ No newline at end of file diff --git a/src/Lucene.Net/Support/Util/Events/Resources.cs b/src/Lucene.Net/Support/Util/Events/Resources.cs new file mode 100644 index 0000000000..04e9d28ec9 --- /dev/null +++ b/src/Lucene.Net/Support/Util/Events/Resources.cs @@ -0,0 +1,32 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +namespace Lucene.Net.Util.Events +{ + internal static class Resources + { + public static string EventAggregatorNotConstructedOnUIThread = "To use the UIThread option for subscribing, the EventAggregator must be constructed on the UI thread."; + public static string InvalidDelegateRerefenceTypeException = "Invalid Delegate Reference Type Exception"; + } +} + +#endif \ No newline at end of file diff --git a/src/Lucene.Net/Support/Util/Events/SubscriptionToken.cs b/src/Lucene.Net/Support/Util/Events/SubscriptionToken.cs new file mode 100644 index 0000000000..e422eb3bb9 --- /dev/null +++ b/src/Lucene.Net/Support/Util/Events/SubscriptionToken.cs @@ -0,0 +1,106 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using System; + +namespace Lucene.Net.Util.Events +{ + /// + /// Subscription token returned from on subscribe. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Should never have a need for a finalizer, hence no need for Dispose(bool)")] + internal class SubscriptionToken + { + private readonly Guid _token; + private Action _unsubscribeAction; + + /// + /// Initializes a new instance of . + /// + public SubscriptionToken(Action unsubscribeAction) + { + _unsubscribeAction = unsubscribeAction; + _token = Guid.NewGuid(); + } + + /// + ///Indicates whether the current object is equal to another object of the same type. + /// + /// + /// if the current object is equal to the parameter; otherwise, . + /// + ///An object to compare with this object. + public bool Equals(SubscriptionToken other) + { + if (other == null) return false; + return Equals(_token, other._token); + } + + /// + ///Determines whether the specified is equal to the current . + /// + /// + ///true if the specified is equal to the current ; otherwise, false. + /// + ///The to compare with the current . + ///The parameter is null.2 + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) return true; + return Equals(obj as SubscriptionToken); + } + + /// + /// Serves as a hash function for a particular type. + /// + /// + /// A hash code for the current . + /// + /// 2 + public override int GetHashCode() + { + return _token.GetHashCode(); + } + + /// + /// Disposes the SubscriptionToken, removing the subscription from the corresponding . + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Should never have need for a finalizer, hence no need for Dispose(bool).")] + public virtual void Dispose() + { + // While the SubscriptionToken class implements IDisposable, in the case of weak subscriptions + // (i.e. keepSubscriberReferenceAlive set to false in the Subscribe method) it's not necessary to unsubscribe, + // as no resources should be kept alive by the event subscription. + // In such cases, if a warning is issued, it could be suppressed. + + if (this._unsubscribeAction != null) + { + this._unsubscribeAction(this); + this._unsubscribeAction = null; + } + + GC.SuppressFinalize(this); + } + } +} + +#endif diff --git a/src/Lucene.Net/Support/Util/Events/ThreadOption.cs b/src/Lucene.Net/Support/Util/Events/ThreadOption.cs new file mode 100644 index 0000000000..3043c2110b --- /dev/null +++ b/src/Lucene.Net/Support/Util/Events/ThreadOption.cs @@ -0,0 +1,47 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +namespace Lucene.Net.Util.Events +{ + /// + /// Specifies on which thread a subscriber will be called. + /// + internal enum ThreadOption + { + /// + /// The call is done on the same thread on which the was published. + /// + PublisherThread, + + /// + /// The call is done on the UI thread. + /// + UIThread, + + /// + /// The call is done asynchronously on a background thread. + /// + BackgroundThread + } +} + +#endif diff --git a/src/Lucene.Net/Support/Util/Events/WeakDelegatesManager.cs b/src/Lucene.Net/Support/Util/Events/WeakDelegatesManager.cs new file mode 100644 index 0000000000..5f171fe483 --- /dev/null +++ b/src/Lucene.Net/Support/Util/Events/WeakDelegatesManager.cs @@ -0,0 +1,71 @@ +// Source: https://github.com/PrismLibrary/Prism/blob/7f0b1680bbe754da790274f80851265f808d9bbf + +#region Copyright .NET Foundation, Licensed under the MIT License (MIT) +// The MIT License (MIT) +// +// Copyright(c).NET Foundation +// +// All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Lucene.Net.Util.Events +{ + /// + /// Manage delegates using weak references to prevent keeping target instances longer than expected. + /// + internal class WeakDelegatesManager + { + private readonly List _listeners = new List(); + + /// + /// Adds a weak reference to the specified listener. + /// + /// The original to add. + public void AddListener(Delegate listener) + { + _listeners.Add(new DelegateReference(listener, false)); + } + + /// + /// Removes the weak reference to the specified listener. + /// + /// The original to remove. + public void RemoveListener(Delegate listener) + { + //Remove the listener, and prune collected listeners + _listeners.RemoveAll(reference => reference.TargetEquals(null) || reference.TargetEquals(listener)); + } + + /// + /// Invoke the delegates for all targets still being alive. + /// + /// An array of objects that are the arguments to pass to the delegates. -or- null, if the method represented by the delegate does not require arguments. + public void Raise(params object[] args) + { + _listeners.RemoveAll(listener => listener.TargetEquals(null)); + + foreach (Delegate handler in _listeners.Select(listener => listener.Target).Where(listener => listener != null).ToList()) + { + handler.DynamicInvoke(args); + } + } + } +} + +#endif diff --git a/src/Lucene.Net/Support/Util/WeakEvents.cs b/src/Lucene.Net/Support/Util/WeakEvents.cs index 793847056e..365c847263 100644 --- a/src/Lucene.Net/Support/Util/WeakEvents.cs +++ b/src/Lucene.Net/Support/Util/WeakEvents.cs @@ -1,6 +1,6 @@ #if !FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR using Lucene.Net.Index; -using Prism.Events; +using Lucene.Net.Util.Events; using System.Collections.Generic; using System.Runtime.CompilerServices;