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 @@
+