diff --git a/Source/ReactiveProperty.NETStandard/ReactivePropertySlim.cs b/Source/ReactiveProperty.NETStandard/ReactivePropertySlim.cs new file mode 100644 index 00000000..b3f74d76 --- /dev/null +++ b/Source/ReactiveProperty.NETStandard/ReactivePropertySlim.cs @@ -0,0 +1,401 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reactive.Disposables; +using System.Threading; + +namespace Reactive.Bindings +{ + // This file includes ReactivePropertySlim and ReadOnlyReactivePropertySlim. + + internal interface IObserverLinkedList + { + void UnsubscribeNode(ObserverNode node); + } + + internal sealed class ObserverNode : IObserver, IDisposable + { + readonly IObserver observer; + IObserverLinkedList list; + + public ObserverNode Previous { get; internal set; } + public ObserverNode Next { get; internal set; } + + public ObserverNode(IObserverLinkedList list, IObserver observer) + { + this.list = list; + this.observer = observer; + } + + public void OnNext(T value) + { + observer.OnNext(value); + } + + public void OnError(Exception error) + { + observer.OnError(error); + } + + public void OnCompleted() + { + observer.OnCompleted(); + } + + public void Dispose() + { + var sourceList = Interlocked.Exchange(ref list, null); + if (sourceList != null) + { + sourceList.UnsubscribeNode(this); + sourceList = null; + } + } + } + + public class ReactivePropertySlim : IReactiveProperty, IReadOnlyReactiveProperty, IObserverLinkedList + { + const int IsDisposedFlagNumber = 1 << 9; // (reserve 0 ~ 8) + + // minimize field count + T latestValue; + ReactivePropertyMode mode; // None = 0, DistinctUntilChanged = 1, RaiseLatestValueOnSubscribe = 2, Disposed = (1 << 9) + readonly IEqualityComparer equalityComparer; + ObserverNode root; + ObserverNode last; + + public event PropertyChangedEventHandler PropertyChanged; + + public T Value + { + get + { + return latestValue; + } + set + { + if (IsDistinctUntilChanged && equalityComparer.Equals(latestValue, value)) + { + return; + } + + // Note:can set null and can set after disposed. + this.latestValue = value; + if (!IsDisposed) + { + OnNextAndRaiseValueChanged(ref value); + } + } + } + + public bool IsDisposed => (int)mode == IsDisposedFlagNumber; + + object IReactiveProperty.Value + { + get + { + return (object)Value; + } + set + { + Value = (T)value; + } + } + + object IReadOnlyReactiveProperty.Value + { + get + { + return (object)Value; + } + } + + bool IsDistinctUntilChanged => (mode & ReactivePropertyMode.DistinctUntilChanged) == ReactivePropertyMode.DistinctUntilChanged; + bool IsRaiseLatestValueOnSubscribe => (mode & ReactivePropertyMode.RaiseLatestValueOnSubscribe) == ReactivePropertyMode.RaiseLatestValueOnSubscribe; + + public ReactivePropertySlim(T initialValue = default(T), ReactivePropertyMode mode = ReactivePropertyMode.DistinctUntilChanged | ReactivePropertyMode.RaiseLatestValueOnSubscribe, IEqualityComparer equalityComparer = null) + { + this.latestValue = initialValue; + this.mode = mode; + this.equalityComparer = equalityComparer ?? EqualityComparer.Default; + } + + void OnNextAndRaiseValueChanged(ref T value) + { + // call source.OnNext + var node = root; + while (node != null) + { + node.OnNext(value); + node = node.Next; + } + + this.PropertyChanged?.Invoke(this, SingletonPropertyChangedEventArgs.Value); + } + + public void ForceNotify() + { + OnNextAndRaiseValueChanged(ref latestValue); + } + + public IDisposable Subscribe(IObserver observer) + { + if (IsDisposed) + { + observer.OnCompleted(); + return Disposable.Empty; + } + + if (IsRaiseLatestValueOnSubscribe) + { + observer.OnNext(this.latestValue); + } + + // subscribe node, node as subscription. + var next = new ObserverNode(this, observer); + if (root == null) + { + root = last = next; + } + else + { + last.Next = next; + next.Previous = last; + last = next; + } + return next; + } + + void IObserverLinkedList.UnsubscribeNode(ObserverNode node) + { + if (node == root) + { + root = node.Next; + } + if (node == last) + { + last = node.Previous; + } + + if (node.Previous != null) + { + node.Previous.Next = node.Next; + } + if (node.Next != null) + { + node.Next.Previous = node.Previous; + } + } + + public void Dispose() + { + if (IsDisposed) return; + + var node = root; + root = last = null; + mode = (ReactivePropertyMode)IsDisposedFlagNumber; + + while (node != null) + { + node.OnCompleted(); + node = node.Next; + } + } + + public override string ToString() + { + return (latestValue == null) + ? "null" + : latestValue.ToString(); + } + + // NotSupported validation. + + bool INotifyDataErrorInfo.HasErrors => throw new NotSupportedException(); + + IObservable IHasErrors.ObserveErrorChanged => throw new NotSupportedException(); + + IObservable IHasErrors.ObserveHasErrors => throw new NotSupportedException(); + + event EventHandler INotifyDataErrorInfo.ErrorsChanged + { + add + { + throw new NotSupportedException(); + } + + remove + { + throw new NotSupportedException(); + } + } + + IEnumerable INotifyDataErrorInfo.GetErrors(string propertyName) + { + throw new NotSupportedException(); + } + } + + public class ReadOnlyReactivePropertySlim : IReadOnlyReactiveProperty, IObserverLinkedList, IObserver + { + const int IsDisposedFlagNumber = 1 << 9; // (reserve 0 ~ 8) + + // minimize field count + T latestValue; + IDisposable sourceSubscription; + ReactivePropertyMode mode; // None = 0, DistinctUntilChanged = 1, RaiseLatestValueOnSubscribe = 2, Disposed = (1 << 9) + readonly IEqualityComparer equalityComparer; + + ObserverNode root; + ObserverNode last; + + public event PropertyChangedEventHandler PropertyChanged; + + public T Value + { + get + { + return latestValue; + } + } + + public bool IsDisposed => (int)mode == IsDisposedFlagNumber; + + object IReadOnlyReactiveProperty.Value + { + get + { + return (object)Value; + } + } + + bool IsDistinctUntilChanged => (mode & ReactivePropertyMode.DistinctUntilChanged) == ReactivePropertyMode.DistinctUntilChanged; + bool IsRaiseLatestValueOnSubscribe => (mode & ReactivePropertyMode.RaiseLatestValueOnSubscribe) == ReactivePropertyMode.RaiseLatestValueOnSubscribe; + + public ReadOnlyReactivePropertySlim(IObservable source, T initialValue = default(T), ReactivePropertyMode mode = ReactivePropertyMode.DistinctUntilChanged | ReactivePropertyMode.RaiseLatestValueOnSubscribe, IEqualityComparer equalityComparer = null) + { + this.latestValue = initialValue; + this.mode = mode; + this.equalityComparer = equalityComparer ?? EqualityComparer.Default; + this.sourceSubscription = source.Subscribe(this); + } + + public IDisposable Subscribe(IObserver observer) + { + if (IsDisposed) + { + observer.OnCompleted(); + return Disposable.Empty; + } + + if (IsRaiseLatestValueOnSubscribe) + { + observer.OnNext(latestValue); + } + + // subscribe node, node as subscription. + var next = new ObserverNode(this, observer); + if (root == null) + { + root = last = next; + } + else + { + last.Next = next; + next.Previous = last; + last = next; + } + + return next; + } + + void IObserverLinkedList.UnsubscribeNode(ObserverNode node) + { + if (node == root) + { + root = node.Next; + } + if (node == last) + { + last = node.Previous; + } + + if (node.Previous != null) + { + node.Previous.Next = node.Next; + } + if (node.Next != null) + { + node.Next.Previous = node.Previous; + } + } + + public void Dispose() + { + if (IsDisposed) return; + + var node = root; + root = last = null; + mode = (ReactivePropertyMode)IsDisposedFlagNumber; + + while (node != null) + { + node.OnCompleted(); + node = node.Next; + } + sourceSubscription.Dispose(); + sourceSubscription = null; + } + + void IObserver.OnNext(T value) + { + if (IsDisposed) return; + + if (IsDistinctUntilChanged && equalityComparer.Equals(latestValue, value)) + { + return; + } + + // SetValue + this.latestValue = value; + + // call source.OnNext + var node = root; + while (node != null) + { + node.OnNext(value); + node = node.Next; + } + + // Notify changed. + this.PropertyChanged?.Invoke(this, SingletonPropertyChangedEventArgs.Value); + } + + void IObserver.OnError(Exception error) + { + // do nothing. + } + + void IObserver.OnCompleted() + { + // oncompleted same as dispose. + Dispose(); + } + + public override string ToString() + { + return (latestValue == null) + ? "null" + : latestValue.ToString(); + } + } + + public static class ReadOnlyReactivePropertySlim + { + public static ReadOnlyReactivePropertySlim ToReadOnlyReactivePropertySlim(this IObservable source, T initialValue = default(T), ReactivePropertyMode mode = ReactivePropertyMode.DistinctUntilChanged | ReactivePropertyMode.RaiseLatestValueOnSubscribe, IEqualityComparer equalityComparer = null) + { + return new ReadOnlyReactivePropertySlim(source, initialValue, mode, equalityComparer); + } + } +} \ No newline at end of file diff --git a/Test/ReactiveProperty.Tests/ReactiveProperty.Tests.csproj b/Test/ReactiveProperty.Tests/ReactiveProperty.Tests.csproj index 78ff7e2e..85dff187 100644 --- a/Test/ReactiveProperty.Tests/ReactiveProperty.Tests.csproj +++ b/Test/ReactiveProperty.Tests/ReactiveProperty.Tests.csproj @@ -12,7 +12,7 @@ Properties ReactiveProperty.Tests ReactiveProperty.Tests - v4.6.2 + v4.7 512 {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} false @@ -209,11 +209,13 @@ + + diff --git a/Test/ReactiveProperty.Tests/ReactivePropertySlimTest.cs b/Test/ReactiveProperty.Tests/ReactivePropertySlimTest.cs new file mode 100644 index 00000000..11453041 --- /dev/null +++ b/Test/ReactiveProperty.Tests/ReactivePropertySlimTest.cs @@ -0,0 +1,120 @@ +using Reactive.Bindings; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; +using System.Reactive.Subjects; + +namespace ReactiveProperty.Tests +{ + [TestClass] + public class ReactivePropertySlimTest + { + [TestMethod] + public void NormalCase() + { + var rp = new ReactivePropertySlim(); + rp.Value.IsNull(); + rp.Subscribe(x => x.IsNull()); + } + + [TestMethod] + public void InitialValue() + { + var rp = new ReactivePropertySlim("Hello world"); + rp.Value.Is("Hello world"); + rp.Subscribe(x => x.Is("Hello world")); + } + + [TestMethod] + public void NoRaiseLatestValueOnSubscribe() + { + var rp = new ReactivePropertySlim(mode: ReactivePropertyMode.DistinctUntilChanged); + var called = false; + rp.Subscribe(_ => called = true); + called.Is(false); + } + + [TestMethod] + public void NoDistinctUntilChanged() + { + var rp = new ReactivePropertySlim(mode: ReactivePropertyMode.RaiseLatestValueOnSubscribe); + var list = new List(); + rp.Subscribe(list.Add); + rp.Value = "Hello world"; + rp.Value = "Hello world"; + rp.Value = "Hello japan"; + list.Is(null, "Hello world", "Hello world", "Hello japan"); + } + + [TestMethod] + public void EnumCase() + { + var rp = new ReactivePropertySlim(); + var results = new List(); + rp.Subscribe(results.Add); + results.Is(TestEnum.None); + + rp.Value = TestEnum.Enum1; + results.Is(TestEnum.None, TestEnum.Enum1); + + rp.Value = TestEnum.Enum2; + results.Is(TestEnum.None, TestEnum.Enum1, TestEnum.Enum2); + } + + [TestMethod] + public void ForceNotify() + { + var rp = new ReactivePropertySlim(0); + var collecter = new List(); + rp.Subscribe(collecter.Add); + + collecter.Is(0); + rp.ForceNotify(); + collecter.Is(0, 0); + } + + [TestMethod] + public void UnsubscribeTest() + { + var rp = new ReactivePropertySlim(mode: ReactivePropertyMode.None); + var collecter = new List<(string, int)>(); + var a = rp.Select(x => ("a", x)).Subscribe(collecter.Add); + var b = rp.Select(x => ("b", x)).Subscribe(collecter.Add); + var c = rp.Select(x => ("c", x)).Subscribe(collecter.Add); + + rp.Value = 99; + collecter.Is(("a", 99), ("b", 99), ("c", 99)); + + collecter.Clear(); + a.Dispose(); + + rp.Value = 40; + collecter.Is(("b", 40), ("c", 40)); + + collecter.Clear(); + c.Dispose(); + + rp.Value = 50; + collecter.Is(("b", 50)); + + collecter.Clear(); + b.Dispose(); + + rp.Value = 9999; + collecter.Count.Is(0); + + var d = rp.Select(x => ("d", x)).Subscribe(collecter.Add); + + rp.Value = 9; + collecter.Is(("d", 9)); + + rp.Dispose(); + } + } +} diff --git a/Test/ReactiveProperty.Tests/ReadOnlyReactivePropertySlimTest.cs b/Test/ReactiveProperty.Tests/ReadOnlyReactivePropertySlimTest.cs new file mode 100644 index 00000000..e5464b35 --- /dev/null +++ b/Test/ReactiveProperty.Tests/ReadOnlyReactivePropertySlimTest.cs @@ -0,0 +1,200 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Reactive.Bindings; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Concurrency; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Reactive.Disposables; +using System.Text; +using System.Threading.Tasks; + +namespace ReactiveProperty.Tests +{ + [TestClass] + public class ReadOnlyReactivePropertySlimTest + { + [TestMethod] + public void NormalPattern() + { + var s = new Subject(); + + var rp = s.ToReadOnlyReactivePropertySlim(); + var buffer = new List(); + rp.Subscribe(buffer.Add); + + rp.Value.IsNull(); + buffer.Count.Is(1); + buffer[0].IsNull(); + + s.OnNext("Hello"); + rp.Value.Is("Hello"); + buffer.Count.Is(2); + buffer.Is(default(string), "Hello"); + + s.OnNext("Hello"); + rp.Value.Is("Hello"); + buffer.Count.Is(2); // distinct until changed. + } + + [TestMethod] + public void MultiSubscribeTest() + { + var s = new Subject(); + + var rp = s.ToReadOnlyReactivePropertySlim(); + var buffer1 = new List(); + rp.Subscribe(buffer1.Add); + + + buffer1.Count.Is(1); + s.OnNext("Hello world"); + buffer1.Count.Is(2); + buffer1.Is(default(string), "Hello world"); + + var buffer2 = new List(); + rp.Subscribe(buffer2.Add); + buffer1.Is(default(string), "Hello world"); + buffer2.Is("Hello world"); + + s.OnNext("ReactiveProperty"); + buffer1.Is(default(string), "Hello world", "ReactiveProperty"); + buffer2.Is("Hello world", "ReactiveProperty"); + } + + [TestMethod] + public void NormalPatternNoDistinctUntilChanged() + { + var s = new Subject(); + + var rp = s.ToReadOnlyReactivePropertySlim( + mode: ReactivePropertyMode.RaiseLatestValueOnSubscribe); + var buffer = new List(); + rp.Subscribe(buffer.Add); + + rp.Value.IsNull(); + buffer.Count.Is(1); + buffer[0].IsNull(); + + s.OnNext("Hello"); + rp.Value.Is("Hello"); + buffer.Count.Is(2); + buffer.Is(default(string), "Hello"); + + s.OnNext("Hello"); + rp.Value.Is("Hello"); + buffer.Count.Is(3); // not distinct until changed. + } + + [TestMethod] + public void PropertyChangedTest() + { + var s = new Subject(); + var rp = s.ToReadOnlyReactivePropertySlim(); + var buffer = new List(); + rp.PropertyChanged += (_, args) => + { + buffer.Add(args.PropertyName); + }; + + buffer.Count.Is(0); + + s.OnNext("Hello"); + buffer.Count.Is(1); + + s.OnNext("Hello"); + buffer.Count.Is(1); + + s.OnNext("World"); + buffer.Count.Is(2); + } + + [TestMethod] + public void PropertyChangedNoDistinctUntilChangedTest() + { + var s = new Subject(); + var rp = s.ToReadOnlyReactivePropertySlim( + mode: ReactivePropertyMode.RaiseLatestValueOnSubscribe); + var buffer = new List(); + rp.PropertyChanged += (_, args) => + { + buffer.Add(args.PropertyName); + }; + + buffer.Count.Is(0); + + s.OnNext("Hello"); + buffer.Count.Is(1); + + s.OnNext("Hello"); + buffer.Count.Is(2); + + s.OnNext("World"); + buffer.Count.Is(3); + } + + [TestMethod] + public void BehaviorSubjectTest() + { + var s = new BehaviorSubject("initial value"); + var rp = s.ToReadOnlyReactivePropertySlim(); + rp.Value.Is("initial value"); + } + + [TestMethod] + public void ObservableCreateTest() + { + var i = 0; + var s = Observable.Create(ox => + { + i++; + return Disposable.Empty; + }); + + i.Is(0); + var rp = s.ToReadOnlyReactivePropertySlim(); + i.Is(1); + } + + [TestMethod] + public void UnsubscribeTest() + { + var source = new ReactivePropertySlim(mode: ReactivePropertyMode.None); + var rp = source.ToReadOnlyReactivePropertySlim(mode: ReactivePropertyMode.None); + + var collecter = new List<(string, int)>(); + var a = rp.Select(x => ("a", x)).Subscribe(collecter.Add); + var b = rp.Select(x => ("b", x)).Subscribe(collecter.Add); + var c = rp.Select(x => ("c", x)).Subscribe(collecter.Add); + + source.Value = 99; + collecter.Is(("a", 99), ("b", 99), ("c", 99)); + + collecter.Clear(); + a.Dispose(); + + source.Value = 40; + collecter.Is(("b", 40), ("c", 40)); + + collecter.Clear(); + c.Dispose(); + + source.Value = 50; + collecter.Is(("b", 50)); + + collecter.Clear(); + b.Dispose(); + + source.Value = 9999; + collecter.Count.Is(0); + + var d = rp.Select(x => ("d", x)).Subscribe(collecter.Add); + + source.Value = 9; + collecter.Is(("d", 9)); + + rp.Dispose(); + } + } +} diff --git a/Test/ReactiveProperty.Tests/app.config b/Test/ReactiveProperty.Tests/app.config index 4524b5c7..381a62d5 100644 --- a/Test/ReactiveProperty.Tests/app.config +++ b/Test/ReactiveProperty.Tests/app.config @@ -1,35 +1,35 @@ - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + diff --git a/Test/ReactiveProperty.Tests/packages.config b/Test/ReactiveProperty.Tests/packages.config index 0cc67101..e366ffff 100644 --- a/Test/ReactiveProperty.Tests/packages.config +++ b/Test/ReactiveProperty.Tests/packages.config @@ -21,8 +21,8 @@ - - + + @@ -44,13 +44,13 @@ - + - +