diff --git a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs index 32b1abfb..aee11772 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs @@ -1,16 +1,27 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Collections.ObjectModel; using Crestron.SimplSharp; namespace PepperDash.Essentials.Core; /// -/// Basically a List , with an indexer to find feedbacks by key name +/// Behaves like a List, with an indexer to find feedbacks by key name. Enforces unique feedback keys +/// (case-insensitive) - attempting to add a feedback whose key already exists in the collection throws an +/// . Feedbacks with no explicit key ( == "") are exempt +/// from the uniqueness check, since many feedbacks are intentionally constructed without a key and are not meant +/// to be looked up by the string indexer. /// -public class FeedbackCollection : List where T : Feedback +/// +/// Derives from rather than so key-uniqueness can be enforced +/// via the virtual Insert/Set/Remove/Clear hooks (List{T}'s members are not virtual). Public surface area +/// (int indexer, Add/Insert/Remove/Clear, collection-initializer support, enumeration) is preserved; an +/// method is provided for source compatibility with existing List{T}.AddRange call sites. +/// +public class FeedbackCollection : Collection where T : Feedback { + private readonly Dictionary _feedbacksByKey = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// /// Case-insensitive port lookup linked to feedbacks' keys /// @@ -18,7 +29,87 @@ public class FeedbackCollection : List where T : Feedback { get { - return this.FirstOrDefault(i => i.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + return key != null && _feedbacksByKey.TryGetValue(key, out var feedback) ? feedback : null; + } + } + + /// + /// Adds a range of feedbacks to the collection. Provided for source compatibility with List{T}.AddRange. + /// + /// The feedbacks to add. + public void AddRange(IEnumerable items) + { + if (items == null) + throw new ArgumentNullException(nameof(items)); + + foreach (var item in items) + { + Add(item); + } + } + + /// + protected override void InsertItem(int index, T item) + { + ValidateNewItem(item); + base.InsertItem(index, item); + if (!string.IsNullOrEmpty(item.Key)) + { + _feedbacksByKey[item.Key] = item; + } + } + + /// + protected override void SetItem(int index, T item) + { + ValidateNewItem(item, indexBeingReplaced: index); + var replaced = Items[index]; + base.SetItem(index, item); + if (replaced != null && !string.IsNullOrEmpty(replaced.Key) && + !string.Equals(replaced.Key, item.Key, StringComparison.OrdinalIgnoreCase)) + { + _feedbacksByKey.Remove(replaced.Key); + } + if (!string.IsNullOrEmpty(item.Key)) + { + _feedbacksByKey[item.Key] = item; + } + } + + /// + protected override void RemoveItem(int index) + { + var removed = Items[index]; + base.RemoveItem(index); + if (removed != null && !string.IsNullOrEmpty(removed.Key)) + { + _feedbacksByKey.Remove(removed.Key); + } + } + + /// + protected override void ClearItems() + { + base.ClearItems(); + _feedbacksByKey.Clear(); + } + + private void ValidateNewItem(T item, int? indexBeingReplaced = null) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + + if (string.IsNullOrEmpty(item.Key)) + return; + + if (_feedbacksByKey.TryGetValue(item.Key, out var existing)) + { + var existingIndex = Items.IndexOf(existing); + if (existingIndex != indexBeingReplaced) + { + throw new ArgumentException( + $"A Feedback with key '{item.Key}' already exists in this FeedbackCollection", nameof(item)); + } } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs b/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs index 736d507f..d78021b4 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs @@ -1,22 +1,104 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Collections.ObjectModel; namespace PepperDash.Essentials.Core; /// - /// Represents a RoutingPortCollection, which is essentially a List with an indexer for case-insensitive lookup of ports by their key names. + /// Represents a RoutingPortCollection, which behaves like a List with an indexer for case-insensitive lookup of + /// ports by their key names. Enforces unique port keys (case-insensitive) - attempting to add a port whose key + /// already exists in the collection throws an . /// - public class RoutingPortCollection : List where T: RoutingPort + /// + /// Derives from rather than so key-uniqueness can be enforced + /// via the virtual Insert/Set/Remove/Clear hooks (List{T}'s members are not virtual). Public surface area + /// (int indexer, Add/Insert/Remove/Clear, collection-initializer support, enumeration) is preserved; an + /// method is provided for source compatibility with existing List{T}.AddRange call sites. + /// + public class RoutingPortCollection : Collection where T: RoutingPort { + private readonly Dictionary _portsByKey = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// /// Case-insensitive port lookup linked to ports' keys /// - public T this[string key] + public T this[string key] { get { - return this.FirstOrDefault(i => i.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + return key != null && _portsByKey.TryGetValue(key, out var port) ? port : null; + } + } + + /// + /// Adds a range of ports to the collection. Provided for source compatibility with List{T}.AddRange. + /// + /// The ports to add. + public void AddRange(IEnumerable items) + { + if (items == null) + throw new ArgumentNullException(nameof(items)); + + foreach (var item in items) + { + Add(item); + } + } + + /// + protected override void InsertItem(int index, T item) + { + ValidateNewItem(item); + base.InsertItem(index, item); + _portsByKey[item.Key] = item; + } + + /// + protected override void SetItem(int index, T item) + { + ValidateNewItem(item, indexBeingReplaced: index); + var replaced = Items[index]; + base.SetItem(index, item); + if (replaced != null && !string.Equals(replaced.Key, item.Key, StringComparison.OrdinalIgnoreCase)) + { + _portsByKey.Remove(replaced.Key); + } + _portsByKey[item.Key] = item; + } + + /// + protected override void RemoveItem(int index) + { + var removed = Items[index]; + base.RemoveItem(index); + if (removed != null) + { + _portsByKey.Remove(removed.Key); + } + } + + /// + protected override void ClearItems() + { + base.ClearItems(); + _portsByKey.Clear(); + } + + private void ValidateNewItem(T item, int? indexBeingReplaced = null) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + if (string.IsNullOrEmpty(item.Key)) + throw new ArgumentException("RoutingPort must have a non-empty Key", nameof(item)); + + if (_portsByKey.TryGetValue(item.Key, out var existing)) + { + var existingIndex = Items.IndexOf(existing); + if (existingIndex != indexBeingReplaced) + { + throw new ArgumentException( + $"A RoutingPort with key '{item.Key}' already exists in this RoutingPortCollection", nameof(item)); + } } } }