diff --git a/src/Perspex.Base/Collections/PerspexDictionary.cs b/src/Perspex.Base/Collections/PerspexDictionary.cs new file mode 100644 index 0000000000..3c981d0ac2 --- /dev/null +++ b/src/Perspex.Base/Collections/PerspexDictionary.cs @@ -0,0 +1,237 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Collections +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.ComponentModel; + using System.Linq; + + /// + /// A notifying dictionary. + /// + /// The type of the dictionary key. + /// The type of the dictionary value. + public class PerspexDictionary : IDictionary, + INotifyCollectionChanged, + INotifyPropertyChanged + { + private Dictionary inner; + + /// + /// Initializes a new instance of the class. + /// + public PerspexDictionary() + { + this.inner = new Dictionary(); + } + + /// + /// Occurs when the collection changes. + /// + public event NotifyCollectionChangedEventHandler CollectionChanged; + + /// + /// Raised when a property on the collection changes. + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + public int Count + { + get { return this.inner.Count; } + } + + /// + public bool IsReadOnly + { + get { return false; } + } + + /// + public ICollection Keys + { + get { return this.inner.Keys; } + } + + /// + public ICollection Values + { + get { return this.inner.Values; } + } + + /// + /// Gets or sets the named resource. + /// + /// The resource key. + /// The resource, or null if not found. + public TValue this[TKey key] + { + get + { + return this.inner[key]; + } + + set + { + TValue old; + bool replace = this.inner.TryGetValue(key, out old); + this.inner[key] = value; + + if (replace) + { + if (this.PropertyChanged != null) + { + this.PropertyChanged(this, new PropertyChangedEventArgs($"Item[{key}]")); + } + + if (this.CollectionChanged != null) + { + var e = new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Replace, + new KeyValuePair(key, value), + new KeyValuePair(key, old)); + this.CollectionChanged(this, e); + } + } + else + { + this.NotifyAdd(key, value); + } + } + } + + /// + public void Add(TKey key, TValue value) + { + this.inner.Add(key, value); + this.NotifyAdd(key, value); + } + + /// + public void Clear() + { + var old = this.inner; + + this.inner = new Dictionary(); + + if (this.PropertyChanged != null) + { + this.PropertyChanged(this, new PropertyChangedEventArgs("Count")); + this.PropertyChanged(this, new PropertyChangedEventArgs($"Item[]")); + } + + if (this.CollectionChanged != null) + { + var e = new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + old.ToList(), + -1); + this.CollectionChanged(this, e); + } + } + + /// + public bool ContainsKey(TKey key) + { + return this.inner.ContainsKey(key); + } + + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((IDictionary)this.inner).CopyTo(array, arrayIndex); + } + + /// + public IEnumerator> GetEnumerator() + { + return this.inner.GetEnumerator(); + } + + /// + public bool Remove(TKey key) + { + TValue value; + + if (this.inner.TryGetValue(key, out value)) + { + if (this.PropertyChanged != null) + { + this.PropertyChanged(this, new PropertyChangedEventArgs("Count")); + this.PropertyChanged(this, new PropertyChangedEventArgs($"Item[{key}]")); + } + + if (this.CollectionChanged != null) + { + var e = new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + new[] { new KeyValuePair(key, value) }, + -1); + this.CollectionChanged(this, e); + } + + return true; + } + else + { + return false; + } + } + + /// + public bool TryGetValue(TKey key, out TValue value) + { + return this.inner.TryGetValue(key, out value); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return this.inner.GetEnumerator(); + } + + /// + void ICollection>.Add(KeyValuePair item) + { + this.Add(item.Key, item.Value); + } + + /// + bool ICollection>.Contains(KeyValuePair item) + { + return this.inner.Contains(item); + } + + /// + bool ICollection>.Remove(KeyValuePair item) + { + return this.Remove(item.Key); + } + + private void NotifyAdd(TKey key, TValue value) + { + if (this.PropertyChanged != null) + { + this.PropertyChanged(this, new PropertyChangedEventArgs("Count")); + this.PropertyChanged(this, new PropertyChangedEventArgs($"Item[{key}]")); + } + + if (this.CollectionChanged != null) + { + var val = new KeyValuePair(key, value); + var e = new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + new[] { new KeyValuePair(key, value) }, + -1); + this.CollectionChanged(this, e); + } + } + } +} \ No newline at end of file diff --git a/src/Perspex.Base/Perspex.Base.csproj b/src/Perspex.Base/Perspex.Base.csproj index b26cfaf677..654fbb3926 100644 --- a/src/Perspex.Base/Perspex.Base.csproj +++ b/src/Perspex.Base/Perspex.Base.csproj @@ -42,6 +42,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/tests/Perspex.Base.UnitTests/Collections/CollectionChangedTracker.cs b/tests/Perspex.Base.UnitTests/Collections/CollectionChangedTracker.cs new file mode 100644 index 0000000000..27ec09f3cf --- /dev/null +++ b/tests/Perspex.Base.UnitTests/Collections/CollectionChangedTracker.cs @@ -0,0 +1,36 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Base.UnitTests.Collections +{ + using System; + using System.Collections.Specialized; + + internal class CollectionChangedTracker + { + public CollectionChangedTracker(INotifyCollectionChanged collection) + { + collection.CollectionChanged += this.CollectionChanged; + } + + public NotifyCollectionChangedEventArgs Args { get; private set; } + + public void Reset() + { + this.Args = null; + } + + private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (this.Args != null) + { + throw new Exception("CollectionChanged called more than once."); + } + + this.Args = e; + } + } +} diff --git a/tests/Perspex.Base.UnitTests/Collections/PerspexDictionaryTests.cs b/tests/Perspex.Base.UnitTests/Collections/PerspexDictionaryTests.cs new file mode 100644 index 0000000000..4777811a53 --- /dev/null +++ b/tests/Perspex.Base.UnitTests/Collections/PerspexDictionaryTests.cs @@ -0,0 +1,154 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Base.UnitTests.Collections +{ + using System.Collections.Generic; + using System.Collections.Specialized; + using Perspex.Collections; + using Xunit; + + public class PerspexDictionaryTests + { + [Fact] + public void Adding_Item_Should_Raise_CollectionChanged() + { + var target = new PerspexDictionary(); + var tracker = new CollectionChangedTracker(target); + + target.Add("foo", "bar"); + + Assert.NotNull(tracker.Args); + Assert.Equal(NotifyCollectionChangedAction.Add, tracker.Args.Action); + Assert.Equal(-1, tracker.Args.NewStartingIndex); + Assert.Equal(1, tracker.Args.NewItems.Count); + Assert.Equal(new KeyValuePair("foo", "bar"), tracker.Args.NewItems[0]); + } + + [Fact] + public void Adding_Item_Should_Raise_PropertyChanged() + { + var target = new PerspexDictionary(); + var tracker = new PropertyChangedTracker(target); + + target.Add("foo", "bar"); + + Assert.Equal(new[] { "Count", "Item[foo]" }, tracker.Names); + } + + [Fact] + public void Assigning_Item_Should_Raise_CollectionChanged_Add() + { + var target = new PerspexDictionary(); + var tracker = new CollectionChangedTracker(target); + + target["foo"] = "bar"; + + Assert.NotNull(tracker.Args); + Assert.Equal(NotifyCollectionChangedAction.Add, tracker.Args.Action); + Assert.Equal(-1, tracker.Args.NewStartingIndex); + Assert.Equal(1, tracker.Args.NewItems.Count); + Assert.Equal(new KeyValuePair("foo", "bar"), tracker.Args.NewItems[0]); + } + + [Fact] + public void Assigning_Item_Should_Raise_CollectionChanged_Replace() + { + var target = new PerspexDictionary(); + + target["foo"] = "baz"; + var tracker = new CollectionChangedTracker(target); + target["foo"] = "bar"; + + Assert.NotNull(tracker.Args); + Assert.Equal(NotifyCollectionChangedAction.Replace, tracker.Args.Action); + Assert.Equal(-1, tracker.Args.NewStartingIndex); + Assert.Equal(1, tracker.Args.NewItems.Count); + Assert.Equal(new KeyValuePair("foo", "bar"), tracker.Args.NewItems[0]); + } + + [Fact] + public void Assigning_Item_Should_Raise_PropertyChanged_Add() + { + var target = new PerspexDictionary(); + var tracker = new PropertyChangedTracker(target); + + target["foo"] = "bar"; + + Assert.Equal(new[] { "Count", "Item[foo]" }, tracker.Names); + } + + [Fact] + public void Assigning_Item_Should_Raise_PropertyChanged_Replace() + { + var target = new PerspexDictionary(); + + target["foo"] = "baz"; + var tracker = new PropertyChangedTracker(target); + target["foo"] = "bar"; + + Assert.Equal(new[] { "Item[foo]" }, tracker.Names); + } + + [Fact] + public void Removing_Item_Should_Raise_CollectionChanged() + { + var target = new PerspexDictionary(); + + target["foo"] = "bar"; + var tracker = new CollectionChangedTracker(target); + target.Remove("foo"); + + Assert.NotNull(tracker.Args); + Assert.Equal(NotifyCollectionChangedAction.Remove, tracker.Args.Action); + Assert.Equal(-1, tracker.Args.OldStartingIndex); + Assert.Equal(1, tracker.Args.OldItems.Count); + Assert.Equal(new KeyValuePair("foo", "bar"), tracker.Args.OldItems[0]); + } + + [Fact] + public void Removing_Item_Should_Raise_PropertyChanged() + { + var target = new PerspexDictionary(); + + target["foo"] = "bar"; + var tracker = new PropertyChangedTracker(target); + target.Remove("foo"); + + Assert.Equal(new[] { "Count", "Item[foo]" }, tracker.Names); + } + + [Fact] + public void Clearing_Collection_Should_Raise_CollectionChanged() + { + var target = new PerspexDictionary(); + + target["foo"] = "bar"; + target["baz"] = "qux"; + var tracker = new CollectionChangedTracker(target); + target.Clear(); + + Assert.NotNull(tracker.Args); + Assert.Equal(NotifyCollectionChangedAction.Remove, tracker.Args.Action); + Assert.Equal(-1, tracker.Args.OldStartingIndex); + Assert.Equal(2, tracker.Args.OldItems.Count); + Assert.Equal(new KeyValuePair("foo", "bar"), tracker.Args.OldItems[0]); + } + + [Fact] + public void Clearing_Collection_Should_Raise_PropertyChanged() + { + var target = new PerspexDictionary(); + + target["foo"] = "bar"; + target["baz"] = "qux"; + var tracker = new PropertyChangedTracker(target); + target.Clear(); + + Assert.Equal(new[] { "Count", "Item[]" }, tracker.Names); + } + } +} diff --git a/tests/Perspex.Base.UnitTests/Collections/PerspexListTests.cs b/tests/Perspex.Base.UnitTests/Collections/PerspexListTests.cs index 4518fda7db..21ccdbc106 100644 --- a/tests/Perspex.Base.UnitTests/Collections/PerspexListTests.cs +++ b/tests/Perspex.Base.UnitTests/Collections/PerspexListTests.cs @@ -4,7 +4,7 @@ // // ----------------------------------------------------------------------- -namespace Perspex.Base.UnitTests +namespace Perspex.Base.UnitTests.Collections { using System; using System.Collections.Generic; diff --git a/tests/Perspex.Base.UnitTests/Collections/PropertyChangedTracker.cs b/tests/Perspex.Base.UnitTests/Collections/PropertyChangedTracker.cs new file mode 100644 index 0000000000..8c7260f57a --- /dev/null +++ b/tests/Perspex.Base.UnitTests/Collections/PropertyChangedTracker.cs @@ -0,0 +1,33 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Base.UnitTests.Collections +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + + internal class PropertyChangedTracker + { + public PropertyChangedTracker(INotifyPropertyChanged obj) + { + this.Names = new List(); + obj.PropertyChanged += this.PropertyChanged; + } + + public List Names { get; private set; } + + public void Reset() + { + this.Names.Clear(); + } + + private void PropertyChanged(object sender, PropertyChangedEventArgs e) + { + this.Names.Add(e.PropertyName); + } + } +} diff --git a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj index e7f901e257..de667b5823 100644 --- a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj +++ b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj @@ -76,6 +76,8 @@ + +