diff --git a/src/Perspex.Base/Collections/PerspexList.cs b/src/Perspex.Base/Collections/PerspexList.cs index 25424461db..e2d829afe5 100644 --- a/src/Perspex.Base/Collections/PerspexList.cs +++ b/src/Perspex.Base/Collections/PerspexList.cs @@ -171,7 +171,7 @@ namespace Perspex.Collections /// Adds an item to the collection. /// /// The item. - public void Add(T item) + public virtual void Add(T item) { Validate?.Invoke(item); int index = _inner.Count; @@ -183,7 +183,7 @@ namespace Perspex.Collections /// Adds multiple items to the collection. /// /// The items. - public void AddRange(IEnumerable items) + public virtual void AddRange(IEnumerable items) { Contract.Requires(items != null); @@ -264,7 +264,7 @@ namespace Perspex.Collections /// /// The index. /// The item. - public void Insert(int index, T item) + public virtual void Insert(int index, T item) { Validate?.Invoke(item); _inner.Insert(index, item); @@ -276,7 +276,7 @@ namespace Perspex.Collections /// /// The index. /// The items. - public void InsertRange(int index, IEnumerable items) + public virtual void InsertRange(int index, IEnumerable items) { Contract.Requires(items != null); @@ -302,7 +302,7 @@ namespace Perspex.Collections /// /// The item. /// True if the item was found and removed, otherwise false. - public bool Remove(T item) + public virtual bool Remove(T item) { int index = _inner.IndexOf(item); @@ -320,7 +320,7 @@ namespace Perspex.Collections /// Removes multiple items from the collection. /// /// The items. - public void RemoveAll(IEnumerable items) + public virtual void RemoveAll(IEnumerable items) { Contract.Requires(items != null); @@ -337,7 +337,7 @@ namespace Perspex.Collections /// Removes the item at the specified index. /// /// The index. - public void RemoveAt(int index) + public virtual void RemoveAt(int index) { T item = _inner[index]; _inner.RemoveAt(index); @@ -349,7 +349,7 @@ namespace Perspex.Collections /// /// The first index to remove. /// The number of items to remove. - public void RemoveRange(int index, int count) + public virtual void RemoveRange(int index, int count) { if (count > 0) { diff --git a/src/Perspex.Controls/Classes.cs b/src/Perspex.Controls/Classes.cs index 9e12871dc9..7a0d79de2b 100644 --- a/src/Perspex.Controls/Classes.cs +++ b/src/Perspex.Controls/Classes.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Collections.Generic; +using System.Linq; using Perspex.Collections; namespace Perspex.Controls @@ -21,5 +22,18 @@ namespace Perspex.Controls : base(items) { } + + public override void Add(string item) + { + if (!Contains(item)) + { + base.Add(item); + } + } + + public override void AddRange(IEnumerable items) + { + base.AddRange(items.Where(x => !Contains(x))); + } } } diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 142da517a8..df9f5b08d2 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -682,7 +682,10 @@ namespace Perspex.Controls.Primitives case NotifyCollectionChangedAction.Reset: foreach (var item in ItemContainerGenerator.Containers) { - MarkContainerSelected(item, false); + if (item != null) + { + MarkContainerSelected(item, false); + } } if (!_syncingSelectedItems) diff --git a/src/Perspex.Styling/Styling/Selectors.cs b/src/Perspex.Styling/Styling/Selectors.cs index 312f7e76e1..67da8fc8ca 100644 --- a/src/Perspex.Styling/Styling/Selectors.cs +++ b/src/Perspex.Styling/Styling/Selectors.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using System.Reactive; using System.Reactive.Linq; using System.Reflection; @@ -178,16 +179,16 @@ namespace Perspex.Styling private static SelectorMatch MatchClass(IStyleable control, string name) { - var changed = Observable.FromEventPattern< + var observable = Observable.FromEventPattern< NotifyCollectionChangedEventHandler, - NotifyCollectionChangedEventHandler>( + NotifyCollectionChangedEventArgs>( x => control.Classes.CollectionChanged += x, - x => control.Classes.CollectionChanged -= x); + x => control.Classes.CollectionChanged -= x) + .Select(_ => Unit.Default) + .StartWith(Unit.Default) + .Select(_ => control.Classes.Contains(name)); - return new SelectorMatch( - Observable - .Return(control.Classes.Contains(name)) - .Concat(changed.Select(e => control.Classes.Contains(name)))); + return new SelectorMatch(observable); } private static SelectorMatch MatchDescendent(IStyleable control, Selector previous) diff --git a/src/Perspex.Styling/Styling/StyleActivator.cs b/src/Perspex.Styling/Styling/StyleActivator.cs index c6b92a4e39..aa71884530 100644 --- a/src/Perspex.Styling/Styling/StyleActivator.cs +++ b/src/Perspex.Styling/Styling/StyleActivator.cs @@ -4,7 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; +using System.Reactive; +using System.Reactive.Linq; namespace Perspex.Styling { @@ -14,134 +15,30 @@ namespace Perspex.Styling Or, } - public class StyleActivator : IObservable, IDisposable + public class StyleActivator : ObservableBase { + private readonly IObservable[] _inputs; private readonly ActivatorMode _mode; - private readonly bool[] _values; - - private readonly List _subscriptions = new List(); - - private readonly List> _observers = new List>(); - public StyleActivator( IList> inputs, ActivatorMode mode = ActivatorMode.And) { - int i = 0; - + _inputs = inputs.ToArray(); _mode = mode; - _values = new bool[inputs.Count]; - - foreach (IObservable input in inputs) - { - int capturedIndex = i; - - IDisposable subscription = input.Subscribe( - x => Update(capturedIndex, x), - x => Finish(capturedIndex), - () => Finish(capturedIndex)); - _subscriptions.Add(subscription); - ++i; - } } - public bool CurrentValue + protected override IDisposable SubscribeCore(IObserver observer) { - get; - private set; - } - - public bool HasCompleted - { - get; - private set; - } - - public void Dispose() - { - foreach (IObserver observer in _observers) - { - observer.OnCompleted(); - } - - foreach (IDisposable subscription in _subscriptions) - { - subscription.Dispose(); - } - } - - public IDisposable Subscribe(IObserver observer) - { - Contract.Requires(observer != null); - - observer.OnNext(CurrentValue); - - if (HasCompleted) - { - observer.OnCompleted(); - return Disposable.Empty; - } - else - { - _observers.Add(observer); - return Disposable.Create(() => _observers.Remove(observer)); - } - } - - private void Update(int index, bool value) - { - _values[index] = value; - - bool current; - - switch (_mode) - { - case ActivatorMode.And: - current = _values.All(x => x); - break; - case ActivatorMode.Or: - current = _values.Any(x => x); - break; - default: - throw new InvalidOperationException("Invalid Activator mode."); - } - - if (current != CurrentValue) - { - Push(current); - CurrentValue = current; - } - } - - private void Finish(int i) - { - // We can unsubscribe from everything if the completed observable: - // - Is the only subscription. - // - Has finished on 'false' and we're in And mode - // - Has finished on 'true' and we're in Or mode - var value = _values[i]; - var unsubscribe = - (_values.Length == 1) || - (_mode == ActivatorMode.And ? !value : value); - - if (unsubscribe) - { - foreach (IDisposable subscription in _subscriptions) - { - subscription.Dispose(); - } - - HasCompleted = true; - } + return _inputs.CombineLatest() + .Select(Calculate) + .DistinctUntilChanged() + .Subscribe(observer); } - private void Push(bool value) + private bool Calculate(IList values) { - foreach (IObserver observer in _observers) - { - observer.OnNext(value); - } + return _mode == ActivatorMode.And ? values.All(x => x) : values.Any(x => x); } } } diff --git a/tests/Perspex.LeakTests/StyleTests.cs b/tests/Perspex.LeakTests/StyleTests.cs index bbaade7250..ae74048ae9 100644 --- a/tests/Perspex.LeakTests/StyleTests.cs +++ b/tests/Perspex.LeakTests/StyleTests.cs @@ -2,14 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; using System.Linq; using JetBrains.dotMemoryUnit; using Perspex.Controls; -using Perspex.Controls.Primitives; -using Perspex.Controls.Templates; using Perspex.Styling; -using Perspex.VisualTree; using Xunit; using Xunit.Abstractions; @@ -123,7 +119,6 @@ namespace Perspex.LeakTests dotMemory.Check(memory => Assert.Equal(1, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); - Assert.False(true); } } } diff --git a/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj b/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj index 95cb831480..cc22e7f257 100644 --- a/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj +++ b/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj @@ -40,6 +40,11 @@ 4 + + ..\..\packages\Rx-Testing.2.2.5\lib\net45\Microsoft.Reactive.Testing.dll + True + + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll diff --git a/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs b/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs index 2172bc39f7..3b444ec77f 100644 --- a/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs +++ b/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs @@ -2,13 +2,44 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; +using System.Reactive; using System.Reactive.Linq; +using Microsoft.Reactive.Testing; using Xunit; namespace Perspex.Styling.UnitTests { - public class StyleActivatorTests + public class StyleActivatorTests : ReactiveTest { + [Fact] + public void Activator_Should_Subscribe_To_Inputs_On_First_Subscription() + { + var scheduler = new TestScheduler(); + var source = scheduler.CreateColdObservable(); + var target = new StyleActivator(new[] { source }, ActivatorMode.And); + + Assert.Equal(0, source.Subscriptions.Count); + target.Subscribe(_ => { }); + Assert.Equal(1, source.Subscriptions.Count); + } + + [Fact] + public void Activator_Should_Unsubscribe_From_Inputs_After_Last_Subscriber_Completes() + { + var scheduler = new TestScheduler(); + var source = scheduler.CreateColdObservable(); + var target = new StyleActivator(new[] { source }, ActivatorMode.And); + + var dispose = target.Subscribe(_ => { }); + Assert.Equal(1, source.Subscriptions.Count); + Assert.Equal(Subscription.Infinite, source.Subscriptions[0].Unsubscribe); + + dispose.Dispose(); + Assert.Equal(1, source.Subscriptions.Count); + Assert.Equal(0, source.Subscriptions[0].Unsubscribe); + } + [Fact] public void Activator_And_Should_Follow_Single_Input() { @@ -53,32 +84,6 @@ namespace Perspex.Styling.UnitTests Assert.Equal(1, inputs[2].SubscriberCount); } - [Fact] - public void Activator_And_Should_Unsubscribe_All_When_Input_Completes_On_False() - { - var inputs = new[] - { - new TestSubject(false), - new TestSubject(false), - new TestSubject(true), - }; - var target = new StyleActivator(inputs, ActivatorMode.And); - var result = new TestObserver(); - - target.Subscribe(result); - Assert.False(result.GetValue()); - inputs[0].OnNext(true); - inputs[1].OnNext(true); - Assert.True(result.GetValue()); - inputs[0].OnNext(false); - Assert.False(result.GetValue()); - inputs[0].OnCompleted(); - - Assert.Equal(0, inputs[0].SubscriberCount); - Assert.Equal(0, inputs[1].SubscriberCount); - Assert.Equal(0, inputs[2].SubscriberCount); - } - [Fact] public void Activator_And_Should_Not_Unsubscribe_All_When_Input_Completes_On_True() { @@ -96,7 +101,7 @@ namespace Perspex.Styling.UnitTests inputs[0].OnNext(true); inputs[0].OnCompleted(); - Assert.Equal(1, inputs[0].SubscriberCount); + Assert.Equal(0, inputs[0].SubscriberCount); Assert.Equal(1, inputs[1].SubscriberCount); Assert.Equal(1, inputs[2].SubscriberCount); } @@ -144,31 +149,6 @@ namespace Perspex.Styling.UnitTests Assert.Equal(1, inputs[2].SubscriberCount); } - [Fact] - public void Activator_Or_Should_Unsubscribe_All_When_Input_Completes_On_True() - { - var inputs = new[] - { - new TestSubject(false), - new TestSubject(false), - new TestSubject(true), - }; - var target = new StyleActivator(inputs, ActivatorMode.Or); - var result = new TestObserver(); - - target.Subscribe(result); - Assert.True(result.GetValue()); - inputs[2].OnNext(false); - Assert.False(result.GetValue()); - inputs[0].OnNext(true); - Assert.True(result.GetValue()); - inputs[0].OnCompleted(); - - Assert.Equal(0, inputs[0].SubscriberCount); - Assert.Equal(0, inputs[1].SubscriberCount); - Assert.Equal(0, inputs[2].SubscriberCount); - } - [Fact] public void Activator_Or_Should_Not_Unsubscribe_All_When_Input_Completes_On_False() { @@ -189,7 +169,7 @@ namespace Perspex.Styling.UnitTests Assert.Equal(1, inputs[0].SubscriberCount); Assert.Equal(1, inputs[1].SubscriberCount); - Assert.Equal(1, inputs[2].SubscriberCount); + Assert.Equal(0, inputs[2].SubscriberCount); } [Fact] @@ -207,5 +187,18 @@ namespace Perspex.Styling.UnitTests Assert.True(completed); } + + private Recorded>[] OnNextValues(params bool[] values) + { + var result = new List>>(); + var time = 1; + + foreach (var value in values) + { + result.Add(new Recorded>(time, Notification.CreateOnNext(value))); + } + + return result.ToArray(); + } } } diff --git a/tests/Perspex.Styling.UnitTests/packages.config b/tests/Perspex.Styling.UnitTests/packages.config index 3cf2b984d5..bacf5a37aa 100644 --- a/tests/Perspex.Styling.UnitTests/packages.config +++ b/tests/Perspex.Styling.UnitTests/packages.config @@ -6,6 +6,7 @@ +