From e1204481a6a4c9e0257b6918960b632f9ec3f435 Mon Sep 17 00:00:00 2001 From: Paul Betts Date: Sat, 28 Aug 2010 20:49:12 -0700 Subject: [PATCH] Add property change notification tracking to ReactiveCollection --- ReactiveXaml.Tests/ReactiveCollectionTest.cs | 140 +++++++++++++++ ReactiveXaml.Tests/ReactiveXaml.Tests.csproj | 2 + ReactiveXaml/Interfaces.cs | 16 +- ReactiveXaml/ReactiveCollection.cs | 172 +++++++++++++++++++ ReactiveXaml/ReactiveXaml.csproj | 1 + 5 files changed, 327 insertions(+), 4 deletions(-) create mode 100644 ReactiveXaml.Tests/ReactiveCollectionTest.cs create mode 100644 ReactiveXaml/ReactiveCollection.cs diff --git a/ReactiveXaml.Tests/ReactiveCollectionTest.cs b/ReactiveXaml.Tests/ReactiveCollectionTest.cs new file mode 100644 index 000000000..56fc27538 --- /dev/null +++ b/ReactiveXaml.Tests/ReactiveCollectionTest.cs @@ -0,0 +1,140 @@ +using Antireptilia; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Linq; +using System.Collections.Generic; +using ReactiveXaml; +using System.IO; +using System.Text; +using ReactiveXaml.Tests; + +namespace Antireptilia.Tests +{ + [TestClass()] + public class ReactiveCollectionTest : IEnableLogger + { + [TestMethod()] + [DeploymentItem("Antireptilia.exe")] + public void CollectionCountChangedTest() + { + var fixture = new ReactiveCollection(); + var output = new List(); + fixture.CollectionCountChanged.Subscribe(output.Add); + + fixture.Add(10); + fixture.Add(20); + fixture.Add(30); + fixture.RemoveAt(1); + fixture.Clear(); + + var results = new[]{1,2,3,2,0}; + Assert.AreEqual(results.Length, output.Count); + results.Zip(output, (expected, actual) => new {expected,actual}) + .Run(x => Assert.AreEqual(x.expected, x.actual)); + } + + [TestMethod()] + [DeploymentItem("Antireptilia.exe")] + public void ItemsAddedAndRemovedTest() + { + var fixture = new ReactiveCollection(); + var added = new List(); + var removed = new List(); + fixture.ItemsAdded.Subscribe(added.Add); + fixture.ItemsRemoved.Subscribe(removed.Add); + + fixture.Add(10); + fixture.Add(20); + fixture.Add(30); + fixture.RemoveAt(1); + fixture.Clear(); + + var added_results = new[]{10,20,30}; + Assert.AreEqual(added_results.Length, added.Count); + added_results.Zip(added, (expected, actual) => new {expected,actual}) + .Run(x => Assert.AreEqual(x.expected, x.actual)); + + var removed_results = new[]{20}; + Assert.AreEqual(removed_results.Length, removed.Count); + removed_results.Zip(removed, (expected, actual) => new {expected,actual}) + .Run(x => Assert.AreEqual(x.expected, x.actual)); + } + + [TestMethod()] + [DeploymentItem("Antireptilia.exe")] + public void ReactiveCollectionIsRoundTrippable() + { + var output = new[] {"Foo", "Bar", "Baz", "Bamf"}; + var fixture = new ReactiveCollection(output); + + string json = JSONHelper.Serialize(fixture); + var results = JSONHelper.Deserialize>(json); + this.Log().Debug(json); + + output.Zip(results, (expected, actual) => new { expected, actual }) + .Run(x => Assert.AreEqual(x.expected, x.actual)); + + bool should_die = true; + results.ItemsAdded.Subscribe(_ => should_die = false); + results.Add("Foobar"); + Assert.IsFalse(should_die); + } + + [TestMethod()] + [DeploymentItem("Antireptilia.exe")] + public void ChangeTrackingShouldFireNotifications() + { + var fixture = new ReactiveCollection() { ChangeTrackingEnabled = true }; + var output = new List>(); + var item1 = new TestFixture() { IsOnlyOneWord = "Foo" }; + var item2 = new TestFixture() { IsOnlyOneWord = "Bar" }; + + fixture.ItemPropertyChanged.Subscribe(x => { + output.Add(new Tuple((TestFixture)x.Sender, x.PropertyName)); + }); + + fixture.Add(item1); + fixture.Add(item2); + + item1.IsOnlyOneWord = "Baz"; + Assert.AreEqual(1, output.Count); + item2.IsNotNullString = "FooBar"; + Assert.AreEqual(2, output.Count); + + fixture.Remove(item2); + item2.IsNotNullString = "FooBarBaz"; + Assert.AreEqual(2, output.Count); + + fixture.ChangeTrackingEnabled = false; + item1.IsNotNullString = "Bamf"; + Assert.AreEqual(2, output.Count); + + new[]{item1, item2}.Zip(output.Select(x => x.Item1), (expected, actual) => new { expected, actual }) + .Run(x => Assert.AreEqual(x.expected, x.actual)); + new[]{"IsOnlyOneWord", "IsNotNullString"}.Zip(output.Select(x => x.Item2), (expected, actual) => new { expected, actual }) + .Run(x => Assert.AreEqual(x.expected, x.actual)); + } + } + + public class JSONHelper + { + public static string Serialize(T obj) + { + var serializer = new System.Runtime.Serialization.Json.DataContractJsonSerializer(obj.GetType()); + var ms = new MemoryStream(); + serializer.WriteObject(ms, obj); + string retVal = Encoding.Default.GetString(ms.ToArray()); + return retVal; + } + + public static T Deserialize(string json) + { + var obj = Activator.CreateInstance(); + var ms = new MemoryStream(Encoding.Unicode.GetBytes(json)); + var serializer = new System.Runtime.Serialization.Json.DataContractJsonSerializer(obj.GetType()); + obj = (T)serializer.ReadObject(ms); + ms.Close(); + return obj; + } + } +} \ No newline at end of file diff --git a/ReactiveXaml.Tests/ReactiveXaml.Tests.csproj b/ReactiveXaml.Tests/ReactiveXaml.Tests.csproj index bf79604a1..26ba2b6fe 100644 --- a/ReactiveXaml.Tests/ReactiveXaml.Tests.csproj +++ b/ReactiveXaml.Tests/ReactiveXaml.Tests.csproj @@ -60,6 +60,7 @@ ..\ext\System.Reactive.dll + @@ -73,6 +74,7 @@ + diff --git a/ReactiveXaml/Interfaces.cs b/ReactiveXaml/Interfaces.cs index 5454dd127..cf311bee2 100644 --- a/ReactiveXaml/Interfaces.cs +++ b/ReactiveXaml/Interfaces.cs @@ -12,7 +12,7 @@ namespace ReactiveXaml public class ObservedChange { public TSender Sender { get; set; } - public TValue Value { get; set; } + public string PropertyName { get; set; } } public interface IReactiveNotifyPropertyChanged : INotifyPropertyChanged, IObservable { } @@ -22,13 +22,21 @@ namespace ReactiveXaml public static IObservable> ObservableForProperty(this TSender This, string propertyName) where TSender : IReactiveNotifyPropertyChanged { - var prop = This.GetType().GetProperty(propertyName); - return This.Where(x => x.PropertyName == propertyName) - .Select(x => new ObservedChange { Sender = This, Value = (TValue)prop.GetValue(This, null) }); + .Select(x => new ObservedChange { Sender = This, PropertyName = x.PropertyName }); } } + public interface IReactiveCollection : IEnumerable, IList, INotifyCollectionChanged + { + IObservable ItemsAdded { get; } + IObservable ItemsRemoved { get; } + IObservable CollectionCountChanged { get; } + + bool ChangeTrackingEnabled { get; set; } + IObservable> ItemPropertyChanged { get; } + } + public interface IReactiveCommand : ICommand, IObservable { IObservable CanExecuteObservable {get;} diff --git a/ReactiveXaml/ReactiveCollection.cs b/ReactiveXaml/ReactiveCollection.cs new file mode 100644 index 000000000..533788f54 --- /dev/null +++ b/ReactiveXaml/ReactiveCollection.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Collections.Specialized; +using System.Linq; +using System.Runtime.Serialization; + +namespace ReactiveXaml +{ + [Serializable] + public class ReactiveCollection : ObservableCollection, IReactiveCollection, INotifyPropertyChanged, IDisposable + { + public ReactiveCollection() { setupRx(); } + public ReactiveCollection(IEnumerable List) : base(List) { setupRx(); } + + [OnDeserialized] + void setupRx(StreamingContext _) { setupRx(); } + + void setupRx() + { + var coll_changed = Observable.FromEvent(this, "CollectionChanged"); + + coll_changed.Subscribe(x => { + int a = 1; + + Console.WriteLine(x.EventArgs.Action); + Console.WriteLine(x.EventArgs.OldItems); + }); + + ItemsAdded = coll_changed + .Where(x => x.EventArgs.Action == NotifyCollectionChangedAction.Add || x.EventArgs.Action == NotifyCollectionChangedAction.Replace) + .SelectMany(x => (x.EventArgs.NewItems != null ? x.EventArgs.NewItems.OfType() : Enumerable.Empty()).ToObservable()); + + ItemsRemoved = coll_changed + .Where(x => x.EventArgs.Action == NotifyCollectionChangedAction.Remove || x.EventArgs.Action == NotifyCollectionChangedAction.Replace || x.EventArgs.Action == NotifyCollectionChangedAction.Reset) + .SelectMany(x => (x.EventArgs.OldItems != null ? x.EventArgs.OldItems.OfType() : Enumerable.Empty()).ToObservable()); + + CollectionCountChanged = coll_changed + .Select(x => this.Count) + .DistinctUntilChanged(); + + _ItemPropertyChanged = new Subject>(); + + ItemsAdded.Subscribe(x => { + if (propertyChangeWatchers == null) + return; + var item = x as IReactiveNotifyPropertyChanged; + if (item == null) + return; + propertyChangeWatchers.Add(x, item.Subscribe(change => + _ItemPropertyChanged.OnNext(new ObservedChange() { Sender = x, PropertyName = change.PropertyName }))); + }); + + ItemsRemoved.Subscribe(x => { + if (propertyChangeWatchers == null) + return; + if (propertyChangeWatchers.ContainsKey(x)) { + propertyChangeWatchers[x].Dispose(); + propertyChangeWatchers.Remove(x); + } + }); + } + + [NonSerialized] + IObservable _ItemsAdded; + public IObservable ItemsAdded { + get { return _ItemsAdded; } + protected set { _ItemsAdded = value; } + } + + [NonSerialized] + IObservable _ItemsRemoved; + public IObservable ItemsRemoved { + get { return _ItemsRemoved; } + set { _ItemsRemoved = value; } + } + + [NonSerialized] + IObservable _CollectionCountChanged; + public IObservable CollectionCountChanged { + get { return _CollectionCountChanged; } + set { _CollectionCountChanged = value; } + } + + [NonSerialized] + Subject> _ItemPropertyChanged; + public IObservable> ItemPropertyChanged { + get { return _ItemPropertyChanged; } + } + + public bool ChangeTrackingEnabled { + get { return (propertyChangeWatchers != null); } + set { + if ((propertyChangeWatchers != null) == value) + return; + if (propertyChangeWatchers == null) { + propertyChangeWatchers = new Dictionary(); + } else { + releasePropChangeWatchers(); + propertyChangeWatchers = null; + } + } + } + + [NonSerialized] + Dictionary _propertyChangeWatchers; + Dictionary propertyChangeWatchers { + get { return _propertyChangeWatchers; } + set { _propertyChangeWatchers = value; } + } + + protected void releasePropChangeWatchers() + { + if (propertyChangeWatchers == null) { + return; + } + propertyChangeWatchers.Values.Run(x => x.Dispose()); + propertyChangeWatchers.Clear(); + } + + protected override void ClearItems() + { + // N.B: Reset doesn't give us the items that were cleared out, + // we have to release the watchers or else we leak them. + releasePropChangeWatchers(); + base.ClearItems(); + } + + public void Dispose() + { + ChangeTrackingEnabled = false; + } + + [field: NonSerialized] + public override event NotifyCollectionChangedEventHandler CollectionChanged; + + [field: NonSerialized] + private PropertyChangedEventHandler _propertyChangedEventHandler; + + event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged { + add { + _propertyChangedEventHandler = Delegate.Combine(_propertyChangedEventHandler, value) as PropertyChangedEventHandler; + } + remove { + _propertyChangedEventHandler = Delegate.Remove(_propertyChangedEventHandler, value) as PropertyChangedEventHandler; + } + } + + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + NotifyCollectionChangedEventHandler handler = CollectionChanged; + + if (handler != null) { + handler(this, e); + } + } + + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + PropertyChangedEventHandler handler = _propertyChangedEventHandler; + + if (handler != null) { + handler(this, e); + } + } + + } +} + +// vim: tw=120 ts=4 sw=4 et enc=utf8 : \ No newline at end of file diff --git a/ReactiveXaml/ReactiveXaml.csproj b/ReactiveXaml/ReactiveXaml.csproj index 84a3b2a9f..292806070 100644 --- a/ReactiveXaml/ReactiveXaml.csproj +++ b/ReactiveXaml/ReactiveXaml.csproj @@ -70,6 +70,7 @@ +