Browse Source

Added PerspexDictionary.

Steven Kirk 10 years ago
parent
commit
b036be75ee

+ 237 - 0
src/Perspex.Base/Collections/PerspexDictionary.cs

@@ -0,0 +1,237 @@
+// -----------------------------------------------------------------------
+// <copyright file="PerspexDictionary.cs" company="Steven Kirk">
+// Copyright 2015 MIT Licence. See licence.md for more information.
+// </copyright>
+// -----------------------------------------------------------------------
+
+namespace Perspex.Collections
+{
+    using System;
+    using System.Collections;
+    using System.Collections.Generic;
+    using System.Collections.Specialized;
+    using System.ComponentModel;
+    using System.Linq;
+
+    /// <summary>
+    /// A notifying dictionary.
+    /// </summary>
+    /// <typeparam name="TKey">The type of the dictionary key.</typeparam>
+    /// <typeparam name="TValue">The type of the dictionary value.</typeparam>
+    public class PerspexDictionary<TKey, TValue> : IDictionary<TKey, TValue>,
+        INotifyCollectionChanged,
+        INotifyPropertyChanged
+    {
+        private Dictionary<TKey, TValue> inner;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PerspexDictionary{TKey, TValue}"/> class.
+        /// </summary>
+        public PerspexDictionary()
+        {
+            this.inner = new Dictionary<TKey, TValue>();
+        }
+
+        /// <summary>
+        /// Occurs when the collection changes.
+        /// </summary>
+        public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+        /// <summary>
+        /// Raised when a property on the collection changes.
+        /// </summary>
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        /// <inheritdoc/>
+        public int Count
+        {
+            get { return this.inner.Count; }
+        }
+
+        /// <inheritdoc/>
+        public bool IsReadOnly
+        {
+            get { return false; }
+        }
+
+        /// <inheritdoc/>
+        public ICollection<TKey> Keys
+        {
+            get { return this.inner.Keys; }
+        }
+
+        /// <inheritdoc/>
+        public ICollection<TValue> Values
+        {
+            get { return this.inner.Values; }
+        }
+
+        /// <summary>
+        /// Gets or sets the named resource.
+        /// </summary>
+        /// <param name="key">The resource key.</param>
+        /// <returns>The resource, or null if not found.</returns>
+        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<TKey, TValue>(key, value),
+                            new KeyValuePair<TKey, TValue>(key, old));
+                        this.CollectionChanged(this, e);
+                    }
+                }
+                else
+                {
+                    this.NotifyAdd(key, value);
+                }
+            }
+        }
+
+        /// <inheritdoc/>
+        public void Add(TKey key, TValue value)
+        {
+            this.inner.Add(key, value);
+            this.NotifyAdd(key, value);
+        }
+
+        /// <inheritdoc/>
+        public void Clear()
+        {
+            var old = this.inner;
+
+            this.inner = new Dictionary<TKey, TValue>();
+
+            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);
+            }
+        }
+
+        /// <inheritdoc/>
+        public bool ContainsKey(TKey key)
+        {
+            return this.inner.ContainsKey(key);
+        }
+
+        /// <inheritdoc/>
+        public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
+        {
+            ((IDictionary<TKey, TValue>)this.inner).CopyTo(array, arrayIndex);
+        }
+
+        /// <inheritdoc/>
+        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
+        {
+            return this.inner.GetEnumerator();
+        }
+
+        /// <inheritdoc/>
+        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<TKey, TValue>(key, value) },
+                        -1);
+                    this.CollectionChanged(this, e);
+                }
+
+                return true;
+            }
+            else
+            {
+                return false;
+            }
+        }
+
+        /// <inheritdoc/>
+        public bool TryGetValue(TKey key, out TValue value)
+        {
+            return this.inner.TryGetValue(key, out value);
+        }
+
+        /// <inheritdoc/>
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return this.inner.GetEnumerator();
+        }
+
+        /// <inheritdoc/>
+        void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
+        {
+            this.Add(item.Key, item.Value);
+        }
+
+        /// <inheritdoc/>
+        bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
+        {
+            return this.inner.Contains(item);
+        }
+
+        /// <inheritdoc/>
+        bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> 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<TKey, TValue>(key, value);
+                var e = new NotifyCollectionChangedEventArgs(
+                    NotifyCollectionChangedAction.Add,
+                    new[] { new KeyValuePair<TKey, TValue>(key, value) },
+                    -1);
+                this.CollectionChanged(this, e);
+            }
+        }
+    }
+}

+ 1 - 0
src/Perspex.Base/Perspex.Base.csproj

@@ -42,6 +42,7 @@
       <Link>Properties\SharedAssemblyInfo.cs</Link>
     </Compile>
     <Compile Include="BindingDescriptor.cs" />
+    <Compile Include="Collections\PerspexDictionary.cs" />
     <Compile Include="Diagnostics\PerspexObjectExtensions.cs" />
     <Compile Include="GlobalSuppressions.cs" />
     <Compile Include="IObservablePropertyBag.cs" />

+ 36 - 0
tests/Perspex.Base.UnitTests/Collections/CollectionChangedTracker.cs

@@ -0,0 +1,36 @@
+// -----------------------------------------------------------------------
+// <copyright file="CollectionChangedTracker.cs" company="Steven Kirk">
+// Copyright 2015 MIT Licence. See licence.md for more information.
+// </copyright>
+// -----------------------------------------------------------------------
+
+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;
+        }
+    }
+}

+ 154 - 0
tests/Perspex.Base.UnitTests/Collections/PerspexDictionaryTests.cs

@@ -0,0 +1,154 @@
+// -----------------------------------------------------------------------
+// <copyright file="PerspexDictionaryTests.cs" company="Steven Kirk">
+// Copyright 2015 MIT Licence. See licence.md for more information.
+// </copyright>
+// -----------------------------------------------------------------------
+
+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<string, string>();
+            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<string, string>("foo", "bar"), tracker.Args.NewItems[0]);
+        }
+
+        [Fact]
+        public void Adding_Item_Should_Raise_PropertyChanged()
+        {
+            var target = new PerspexDictionary<string, string>();
+            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<string, string>();
+            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<string, string>("foo", "bar"), tracker.Args.NewItems[0]);
+        }
+
+        [Fact]
+        public void Assigning_Item_Should_Raise_CollectionChanged_Replace()
+        {
+            var target = new PerspexDictionary<string, string>();
+
+            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<string, string>("foo", "bar"), tracker.Args.NewItems[0]);
+        }
+
+        [Fact]
+        public void Assigning_Item_Should_Raise_PropertyChanged_Add()
+        {
+            var target = new PerspexDictionary<string, string>();
+            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<string, string>();
+
+            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<string, string>();
+
+            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<string, string>("foo", "bar"), tracker.Args.OldItems[0]);
+        }
+
+        [Fact]
+        public void Removing_Item_Should_Raise_PropertyChanged()
+        {
+            var target = new PerspexDictionary<string, string>();
+
+            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<string, string>();
+
+            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<string, string>("foo", "bar"), tracker.Args.OldItems[0]);
+        }
+
+        [Fact]
+        public void Clearing_Collection_Should_Raise_PropertyChanged()
+        {
+            var target = new PerspexDictionary<string, string>();
+
+            target["foo"] = "bar";
+            target["baz"] = "qux";
+            var tracker = new PropertyChangedTracker(target);
+            target.Clear();
+
+            Assert.Equal(new[] { "Count", "Item[]" }, tracker.Names);
+        }
+    }
+}

+ 1 - 1
tests/Perspex.Base.UnitTests/Collections/PerspexListTests.cs

@@ -4,7 +4,7 @@
 // </copyright>
 // -----------------------------------------------------------------------
 
-namespace Perspex.Base.UnitTests
+namespace Perspex.Base.UnitTests.Collections
 {
     using System;
     using System.Collections.Generic;

+ 33 - 0
tests/Perspex.Base.UnitTests/Collections/PropertyChangedTracker.cs

@@ -0,0 +1,33 @@
+// -----------------------------------------------------------------------
+// <copyright file="PropertyChangedTracker.cs" company="Steven Kirk">
+// Copyright 2015 MIT Licence. See licence.md for more information.
+// </copyright>
+// -----------------------------------------------------------------------
+
+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<string>();
+            obj.PropertyChanged += this.PropertyChanged;
+        }
+
+        public List<string> Names { get; private set; }
+
+        public void Reset()
+        {
+            this.Names.Clear();
+        }
+
+        private void PropertyChanged(object sender, PropertyChangedEventArgs e)
+        {
+            this.Names.Add(e.PropertyName);
+        }
+    }
+}

+ 2 - 0
tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj

@@ -76,6 +76,8 @@
     <Otherwise />
   </Choose>
   <ItemGroup>
+    <Compile Include="Collections\CollectionChangedTracker.cs" />
+    <Compile Include="Collections\PerspexDictionaryTests.cs" />
     <Compile Include="Collections\PerspexListTests.cs" />
     <Compile Include="GlobalSuppressions.cs" />
     <Compile Include="PerspexObjectTests_GetObservable.cs" />