From a55b2deb225bf6dc7876cf3b42453cc5ded7fb50 Mon Sep 17 00:00:00 2001 From: Tom Waterbley Date: Thu, 23 Oct 2025 10:20:34 +0200 Subject: [PATCH 1/7] Add generic TreeView control --- .../Components/Generics/GenericTreeView.cs | 190 +++++++++--------- .../Components/Interfaces/ITreeView.cs | 105 ++++++++++ .../Components/Interfaces/ITreeViewBase.cs | 30 +++ .../Components/TreeView.cs | 94 ++------- .../Components/TreeViewBase.cs | 58 ++++++ ...{TreeViewItemOption.cs => TreeViewItem.cs} | 20 +- .../GenericTreeViewTests.cs | 60 +++--- 7 files changed, 343 insertions(+), 214 deletions(-) create mode 100644 InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs create mode 100644 InteractiveAutomationToolkit/Components/Interfaces/ITreeViewBase.cs create mode 100644 InteractiveAutomationToolkit/Components/TreeViewBase.cs rename InteractiveAutomationToolkit/Components/{TreeViewItemOption.cs => TreeViewItem.cs} (91%) diff --git a/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs b/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs index 17ab95b..87bcd9f 100644 --- a/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs +++ b/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs @@ -11,31 +11,31 @@ namespace Skyline.DataMiner.Utils.InteractiveAutomationScript /// A generic tree view structure that allows attaching custom metadata to each item. /// /// The type of the value associated with each tree view item. - public class TreeView : InteractiveWidget, IIsReadonlyWidget + public class TreeView : TreeViewBase, ITreeView { private Dictionary checkedItemCache; private Dictionary collapsedItemCache; - private Dictionary> lookupTable; + private Dictionary> lookupTable; private bool itemsChanged = false; - private List> changedItems = new List>(); + private List> changedItems = new List>(); private bool itemsChecked = false; - private List> checkedItems = new List>(); + private List> checkedItems = new List>(); private bool itemsUnchecked = false; - private List> uncheckedItems = new List>(); + private List> uncheckedItems = new List>(); private bool itemsExpanded = false; - private List> expandedItems = new List>(); + private List> expandedItems = new List>(); private bool itemsCollapsed = false; - private List> collapsedItems = new List>(); + private List> collapsedItems = new List>(); /// /// Initializes a new instance of the class. /// - public TreeView() : this(Enumerable.Empty>()) + public TreeView() : this(Enumerable.Empty>()) { } @@ -43,7 +43,7 @@ public TreeView() : this(Enumerable.Empty>()) /// Initializes a new instance of the class. /// /// Root nodes of the tree view. - public TreeView(IEnumerable> treeViewItems) + public TreeView(IEnumerable> treeViewItems) { Type = UIBlockType.TreeView; Items = treeViewItems; @@ -54,7 +54,7 @@ public TreeView(IEnumerable> treeViewItems) /// Triggered when a different item is selected or no longer selected. /// WantsOnChange will be set to true when this event is subscribed to. /// - public event EventHandler>> Changed + public event EventHandler>> Changed { add { @@ -76,7 +76,7 @@ public event EventHandler>> Changed /// Triggered whenever an item is selected. /// WantsOnChange will be set to true when this event is subscribed to. /// - public event EventHandler>> Checked + public event EventHandler>> Checked { add { @@ -98,7 +98,7 @@ public event EventHandler>> Checked /// Triggered whenever an item is no longer selected. /// WantsOnChange will be set to true when this event is subscribed to. /// - public event EventHandler>> Unchecked + public event EventHandler>> Unchecked { add { @@ -121,7 +121,7 @@ public event EventHandler>> Unchecked /// Can be used for lazy loading. /// Will be triggered whenever a node with SupportsLazyLoading set to true is expanded. /// - public event EventHandler>> Expanded + public event EventHandler>> Expanded { add { @@ -138,7 +138,7 @@ public event EventHandler>> Expanded /// Triggered whenever an item is collapsed. /// Will be triggered whenever a node with SupportsLazyLoading set to true is collapsed. /// - public event EventHandler>> Collapsed + public event EventHandler>> Collapsed { add { @@ -151,21 +151,21 @@ public event EventHandler>> Collapsed } } - private event EventHandler>> OnChanged; + private event EventHandler>> OnChanged; - private event EventHandler>> OnChecked; + private event EventHandler>> OnChecked; - private event EventHandler>> OnUnchecked; + private event EventHandler>> OnUnchecked; - private event EventHandler>> OnExpanded; + private event EventHandler>> OnExpanded; - private event EventHandler>> OnCollapsed; + private event EventHandler>> OnCollapsed; /// /// Gets or sets the top-level items in the tree view. /// The TreeViewItemOption.Item.ChildItems property can be used to navigate further down the tree. /// - public IEnumerable> Items + public IEnumerable> Items { get { @@ -189,7 +189,7 @@ public IEnumerable> Items /// /// Gets all items in the tree view that are selected. /// - public IEnumerable> CheckedItems + public IEnumerable> CheckedItems { get { @@ -200,7 +200,7 @@ public IEnumerable> CheckedItems /// /// Gets all leaves (= items without children) in the tree view that are selected. /// - public IEnumerable> CheckedLeaves + public IEnumerable> CheckedLeaves { get { @@ -211,7 +211,7 @@ public IEnumerable> CheckedLeaves /// /// Gets all nodes (= items with children) in the tree view that are selected. /// - public IEnumerable> CheckedNodes + public IEnumerable> CheckedNodes { get { @@ -252,46 +252,8 @@ public IEnumerable CheckedNodeValues } } - /// - /// Gets or sets the tooltip. - /// - /// When the value is null. - public string Tooltip - { - get - { - return BlockDefinition.TooltipText; - } - - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - BlockDefinition.TooltipText = value; - } - } - /// - public virtual bool IsReadOnly - { - get - { - return BlockDefinition.IsReadOnly; - } - - set - { - BlockDefinition.IsReadOnly = value; - } - } - - /// - /// Sets the IsCollapsed state for all items in the tree view to true, causing the entire tree view to be collapsed. - /// - public void Collapse() + public override void Collapse() { foreach (var item in GetAllItems()) { @@ -299,10 +261,8 @@ public void Collapse() } } - /// - /// Sets the IsCollapsed state for all items in the tree view to false, causing the entire tree view to be expanded. - /// - public void Expand() + /// + public override void Expand() { foreach (var item in GetAllItems()) { @@ -316,7 +276,7 @@ public void Expand() /// Key used to search for the item. /// Item in the tree that matches the provided key. /// True if the item was found, otherwise false. - public bool TryFindTreeViewItem(string key, out TreeViewItemOption item) + public bool TryFindTreeViewItem(string key, out TreeViewItem item) { return lookupTable.TryGetValue(key, out item); } @@ -326,16 +286,45 @@ public bool TryFindTreeViewItem(string key, out TreeViewItemOption item) /// This is done after loading the results from the UI Block, after handling the Events or when setting the Items. /// This method should only be called from outside the TreeView if you are checking or collapsing items from outside of the TreeView and need to access the CheckedItems or CollapsedItems. /// - public void UpdateItemCache() + public override void UpdateItemCache() { - UpdateItemCache(Items); + // Don't pass Items here as it would cause recursion - just update the existing cache + var newCheckedItemCache = new Dictionary(); + var newCollapsedItemCache = new Dictionary(); + + // Ensure lookupTable exists + if (lookupTable == null) + { + lookupTable = new Dictionary>(); + } + + // Update caches based on current lookup table + foreach (var item in lookupTable.Values) + { + try + { + newCheckedItemCache.Add(item.KeyValue, item.IsChecked); + if (item.SupportsLazyLoading) + { + newCollapsedItemCache.Add(item.KeyValue, item.IsCollapsed); + } + } + catch (Exception e) + { + throw new TreeViewDuplicateItemsException(item.KeyValue, e); + } + } + + // Replace caches atomically + checkedItemCache = newCheckedItemCache; + collapsedItemCache = newCollapsedItemCache; } /// /// Iterates over all items in the tree and returns them in a flat collection. /// /// A flat collection containing all items in the tree view. - public IEnumerable> GetAllItems() + public IEnumerable> GetAllItems() { return lookupTable.Values; } @@ -346,7 +335,7 @@ public IEnumerable> GetAllItems() /// /// Depth of the requested items. /// All items in the tree view that are located at the provided depth. - public IEnumerable> GetItems(int depth) + public IEnumerable> GetItems(int depth) { return GetItems(Items, depth, 0); } @@ -384,7 +373,7 @@ protected internal override void LoadResult(IUIResults uiResults) if (changedItemKeys.Any() && OnChanged != null) { itemsChanged = true; - changedItems = new List>(); + changedItems = new List>(); foreach (string changedItemKey in changedItemKeys) { @@ -396,7 +385,7 @@ protected internal override void LoadResult(IUIResults uiResults) } // Persist states - foreach (TreeViewItemOption item in lookupTable.Values) + foreach (TreeViewItem item in lookupTable.Values) { item.Item.IsChecked = checkedItemKeys.Contains(item.KeyValue); item.Item.IsCollapsed = !expandedItemKeys.Contains(item.KeyValue); @@ -455,7 +444,7 @@ protected internal override void RaiseResultEvents() /// Returns all items in the TreeView that are checked. /// /// All checked TreeViewItems in the TreeView. - private IEnumerable> GetCheckedItems() + private IEnumerable> GetCheckedItems() { return lookupTable.Values.Where(x => x.ItemType == TreeViewItem.TreeViewItemType.CheckBox && x.IsChecked); } @@ -466,9 +455,9 @@ private IEnumerable> GetCheckedItems() /// List of TreeViewItems to be visited. /// Parent TreeViewItemOption wrappers. /// Flat collection containing every item in the provided children collection and all underlying items. - private IEnumerable> GetAllItemsRecursive(IEnumerable children, IEnumerable> parentOptions) + private IEnumerable> GetAllItemsRecursive(IEnumerable children, IEnumerable> parentOptions) { - List> allItems = new List>(); + List> allItems = new List>(); foreach (var item in children) { var option = parentOptions.FirstOrDefault(x => x.Item == item); @@ -491,11 +480,11 @@ private IEnumerable> GetAllItemsRecursive(IEnumerableDepth that was requested. /// Current depth in the tree. /// All TreeViewItems in the TreeView that are located on the provided depth. - private IEnumerable> GetItems(IEnumerable> children, int requestedDepth, int currentDepth) + private IEnumerable> GetItems(IEnumerable> children, int requestedDepth, int currentDepth) { - List> requestedItems = new List>(); + List> requestedItems = new List>(); bool depthReached = requestedDepth == currentDepth; - foreach (TreeViewItemOption item in children) + foreach (TreeViewItem item in children) { if (depthReached) { @@ -518,7 +507,7 @@ private void RegisterExpandedItems(IEnumerable expandedItemKeys) if (newlyExpandedItems.Any() && OnExpanded != null) { itemsExpanded = true; - expandedItems = new List>(); + expandedItems = new List>(); foreach (string newlyExpandedItemKey in newlyExpandedItems) { @@ -536,7 +525,7 @@ private void RegisterCollapsedItems(IEnumerable expandedItemKeys) if (newlyCollapsedItems.Any() && OnCollapsed != null) { itemsCollapsed = true; - collapsedItems = new List>(); + collapsedItems = new List>(); foreach (string newlyCollapsedItemKey in newlyCollapsedItems) { @@ -554,7 +543,7 @@ private List RegisterCheckedItems(IEnumerable checkedItemKeys) if (newlyCheckedItemKeys.Any() && OnChecked != null) { itemsChecked = true; - checkedItems = new List>(); + checkedItems = new List>(); foreach (string newlyCheckedItemKey in newlyCheckedItemKeys) { @@ -574,7 +563,7 @@ private List RegisterUncheckedItems(IEnumerable checkedItemKeys) if (newlyUncheckedItemKeys.Any() && OnUnchecked != null) { itemsUnchecked = true; - uncheckedItems = new List>(); + uncheckedItems = new List>(); foreach (string newlyUncheckedItemKey in newlyUncheckedItemKeys) { @@ -588,22 +577,33 @@ private List RegisterUncheckedItems(IEnumerable checkedItemKeys) return newlyUncheckedItemKeys; } - private void UpdateItemCache(IEnumerable> items) + private void UpdateItemCache(IEnumerable> items) { - checkedItemCache = new Dictionary(); - collapsedItemCache = new Dictionary(); - lookupTable = new Dictionary>(); - - BuildLookupTable(items); + // Initialize new caches but preserve the existing lookup table if items is null or empty + var newCheckedItemCache = new Dictionary(); + var newCollapsedItemCache = new Dictionary(); + + // Only rebuild lookup table if we have new items to process + if (items != null && items.Any()) + { + lookupTable = new Dictionary>(); + BuildLookupTable(items); + } + else if (lookupTable == null) + { + // Initialize empty lookup table only if it doesn't exist + lookupTable = new Dictionary>(); + } + // Update caches based on current lookup table foreach (var item in lookupTable.Values) { try { - checkedItemCache.Add(item.KeyValue, item.IsChecked); + newCheckedItemCache.Add(item.KeyValue, item.IsChecked); if (item.SupportsLazyLoading) { - collapsedItemCache.Add(item.KeyValue, item.IsCollapsed); + newCollapsedItemCache.Add(item.KeyValue, item.IsCollapsed); } } catch (Exception e) @@ -611,9 +611,13 @@ private void UpdateItemCache(IEnumerable> items) throw new TreeViewDuplicateItemsException(item.KeyValue, e); } } + + // Replace caches atomically + checkedItemCache = newCheckedItemCache; + collapsedItemCache = newCollapsedItemCache; } - private void BuildLookupTable(IEnumerable> items) + private void BuildLookupTable(IEnumerable> items) { foreach (var item in items) { @@ -643,7 +647,7 @@ private void BuildLookupTableFromTreeViewItems(IEnumerable items) if (!lookupTable.ContainsKey(item.KeyValue)) { // Create a wrapper with default value for items without explicit metadata - var wrapper = new TreeViewItemOption(item, default(T)); + var wrapper = new TreeViewItem(item, default(T)); lookupTable[item.KeyValue] = wrapper; } diff --git a/InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs b/InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs new file mode 100644 index 0000000..8b1a2dc --- /dev/null +++ b/InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs @@ -0,0 +1,105 @@ +namespace Skyline.DataMiner.Utils.InteractiveAutomationScript +{ + using System.Collections.Generic; + + using Skyline.DataMiner.Net.AutomationUI.Objects; + + /// + /// Defines a tree view widget with basic operations. + /// + public interface ITreeView : ITreeViewBase + { + /// + /// Gets or sets the top-level items in the tree view. + /// The TreeViewItem.ChildItems property can be used to navigate further down the tree. + /// + IEnumerable Items { get; set; } + + /// + /// Gets all items in the tree view that are selected. + /// + IEnumerable CheckedItems { get; } + + /// + /// Gets all leaves (= items without children) in the tree view that are selected. + /// + IEnumerable CheckedLeaves { get; } + + /// + /// Gets all nodes (= items with children) in the tree view that are selected. + /// + IEnumerable CheckedNodes { get; } + + /// + /// Iterates over all items in the tree and returns them in a flat collection. + /// + /// A flat collection containing all items in the tree view. + IEnumerable GetAllItems(); + + /// + /// Returns all items in the tree view that are located at the provided depth. + /// Whenever the requested depth is greater than the longest branch in the tree, an empty collection will be returned. + /// + /// Depth of the requested items. + /// All items in the tree view that are located at the provided depth. + IEnumerable GetItems(int depth); + + /// + /// Can be used to retrieve an item from the tree view based on its key value. + /// + /// Key used to search for the item. + /// Item in the tree that matches the provided key. + /// True if the item was found, otherwise false. + bool TryFindTreeViewItem(string key, out TreeViewItem item); + } + + /// + /// Defines a generic tree view widget with support for typed treeview items. + /// + /// The type of the value associated with each treeview item. + public interface ITreeView : ITreeViewBase + { + /// + /// Gets or sets the top-level items in the tree view. + /// The TreeViewItem.ChildItems property can be used to navigate further down the tree. + /// + IEnumerable> Items { get; set; } + + /// + /// Gets all items in the tree view that are selected. + /// + IEnumerable> CheckedItems { get; } + + /// + /// Gets all leaves (= items without children) in the tree view that are selected. + /// + IEnumerable> CheckedLeaves { get; } + + /// + /// Gets all nodes (= items with children) in the tree view that are selected. + /// + IEnumerable> CheckedNodes { get; } + + /// + /// Iterates over all items in the tree and returns them in a flat collection. + /// + /// A flat collection containing all items in the tree view. + IEnumerable> GetAllItems(); + + /// + /// Returns all items in the tree view that are located at the provided depth. + /// Whenever the requested depth is greater than the longest branch in the tree, an empty collection will be returned. + /// + /// Depth of the requested items. + /// All items in the tree view that are located at the provided depth. + IEnumerable> GetItems(int depth); + + /// + /// Can be used to retrieve an item from the tree view based on its key value. + /// + /// Key used to search for the item. + /// Item in the tree that matches the provided key. + /// True if the item was found, otherwise false. + bool TryFindTreeViewItem(string key, out TreeViewItem item); + } +} \ No newline at end of file diff --git a/InteractiveAutomationToolkit/Components/Interfaces/ITreeViewBase.cs b/InteractiveAutomationToolkit/Components/Interfaces/ITreeViewBase.cs new file mode 100644 index 0000000..72dc57e --- /dev/null +++ b/InteractiveAutomationToolkit/Components/Interfaces/ITreeViewBase.cs @@ -0,0 +1,30 @@ +namespace Skyline.DataMiner.Utils.InteractiveAutomationScript +{ + /// + /// Defines the base functionality for a treeview widget. + /// + public interface ITreeViewBase : IIsReadonlyWidget + { + /// + /// Gets or sets the tooltip text associated with the treeview. + /// + string Tooltip { get; set; } + + /// + /// Sets the IsCollapsed state for all items in the tree view to false, causing the entire tree view to be expanded. + /// + void Expand(); + + /// + /// Sets the IsCollapsed state for all items in the tree view to true, causing the entire tree view to be collapsed. + /// + void Collapse(); + + /// + /// This method is used to update the cached TreeViewItems and lookup table. + /// This is done after loading the results from the UI Block, after handling the Events or when setting the Items. + /// This method should only be called from outside the TreeView if you are checking or collapsing items from outside of the TreeView and need to access the CheckedItems or CollapsedItems. + /// + void UpdateItemCache(); + } +} \ No newline at end of file diff --git a/InteractiveAutomationToolkit/Components/TreeView.cs b/InteractiveAutomationToolkit/Components/TreeView.cs index c9622ad..1eef587 100644 --- a/InteractiveAutomationToolkit/Components/TreeView.cs +++ b/InteractiveAutomationToolkit/Components/TreeView.cs @@ -10,7 +10,7 @@ /// /// A tree view structure. /// - public class TreeView : InteractiveWidget, IIsReadonlyWidget + public class TreeView : TreeViewBase, ITreeView { private Dictionary checkedItemCache; private Dictionary collapsedItemCache; // TODO: should only contain Items with LazyLoading set to true @@ -160,10 +160,7 @@ public event EventHandler> Collapsed private event EventHandler> OnCollapsed; - /// - /// Gets or sets the top-level items in the tree view. - /// The TreeViewItem.ChildItems property can be used to navigate further down the tree. - /// + /// public IEnumerable Items { get @@ -183,9 +180,7 @@ public IEnumerable Items } } - /// - /// Gets all items in the tree view that are selected. - /// + /// public IEnumerable CheckedItems { get @@ -194,9 +189,7 @@ public IEnumerable CheckedItems } } - /// - /// Gets all leaves (= items without children) in the tree view that are selected. - /// + /// public IEnumerable CheckedLeaves { get @@ -205,9 +198,7 @@ public IEnumerable CheckedLeaves } } - /// - /// Gets all nodes (= items with children) in the tree view that are selected. - /// + /// public IEnumerable CheckedNodes { get @@ -216,48 +207,8 @@ public IEnumerable CheckedNodes } } - /// - /// Gets or sets the tooltip. - /// - /// When the value is null. - public string Tooltip - { - get - { - return BlockDefinition.TooltipText; - } - - set - { - if (value == null) - { - throw new ArgumentNullException("value"); - } - - BlockDefinition.TooltipText = value; - } - } - - /// /// - /// - public virtual bool IsReadOnly - { - get - { - return BlockDefinition.IsReadOnly; - } - - set - { - BlockDefinition.IsReadOnly = value; - } - } - - /// - /// Sets the IsCollapsed state for all items in the tree view to true, causing the entire tree view to be collapsed. - /// - public void Collapse() + public override void Collapse() { foreach (var item in GetAllItems()) { @@ -265,10 +216,8 @@ public void Collapse() } } - /// - /// Sets the IsCollapsed state for all items in the tree view to false, causing the entire tree view to be expanded. - /// - public void Expand() + /// + public override void Expand() { foreach (var item in GetAllItems()) { @@ -276,24 +225,15 @@ public void Expand() } } - /// - /// Can be used to retrieve an item from the tree view based on its key value. - /// - /// Key used to search for the item. - /// Item in the tree that matches the provided key. - /// True if the item was found, otherwise false. + /// public bool TryFindTreeViewItem(string key, out TreeViewItem item) { item = GetAllItems().FirstOrDefault(x => x.KeyValue.Equals(key)); return item != null; } - /// - /// This method is used to update the cached TreeViewItems and lookup table. - /// This is done after loading the results from the UI Block, after handling the Events or when setting the Items. - /// This method should only be called from outside the TreeView if you are checking or collapsing items from outside of the TreeView and need to access the CheckedItems or CollapsedItems. - /// - public void UpdateItemCache() + /// + public override void UpdateItemCache() { checkedItemCache = new Dictionary(); collapsedItemCache = new Dictionary(); @@ -318,10 +258,7 @@ public void UpdateItemCache() } } - /// - /// Iterates over all items in the tree and returns them in a flat collection. - /// - /// A flat collection containing all items in the tree view. + /// public IEnumerable GetAllItems() { List allItems = new List(); @@ -334,12 +271,7 @@ public IEnumerable GetAllItems() return allItems; } - /// - /// Returns all items in the tree view that are located at the provided depth. - /// Whenever the requested depth is greater than the longest branch in the tree, an empty collection will be returned. - /// - /// Depth of the requested items. - /// All items in the tree view that are located at the provided depth. + /// public IEnumerable GetItems(int depth) { return GetItems(Items, depth, 0); diff --git a/InteractiveAutomationToolkit/Components/TreeViewBase.cs b/InteractiveAutomationToolkit/Components/TreeViewBase.cs new file mode 100644 index 0000000..09e5e51 --- /dev/null +++ b/InteractiveAutomationToolkit/Components/TreeViewBase.cs @@ -0,0 +1,58 @@ +namespace Skyline.DataMiner.Utils.InteractiveAutomationScript +{ + using System; + + using Skyline.DataMiner.Automation; + + public abstract class TreeViewBase : InteractiveWidget, ITreeViewBase + { + protected TreeViewBase() + { + Type = UIBlockType.TreeView; + IsReadOnly = false; + } + + /// + /// When the value is null. + public string Tooltip + { + get + { + return BlockDefinition.TooltipText; + } + + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + BlockDefinition.TooltipText = value; + } + } + + /// + public virtual bool IsReadOnly + { + get + { + return BlockDefinition.IsReadOnly; + } + + set + { + BlockDefinition.IsReadOnly = value; + } + } + + /// + public abstract void Collapse(); + + /// + public abstract void Expand(); + + /// + public abstract void UpdateItemCache(); + } +} diff --git a/InteractiveAutomationToolkit/Components/TreeViewItemOption.cs b/InteractiveAutomationToolkit/Components/TreeViewItem.cs similarity index 91% rename from InteractiveAutomationToolkit/Components/TreeViewItemOption.cs rename to InteractiveAutomationToolkit/Components/TreeViewItem.cs index 423b9ff..923585f 100644 --- a/InteractiveAutomationToolkit/Components/TreeViewItemOption.cs +++ b/InteractiveAutomationToolkit/Components/TreeViewItem.cs @@ -11,22 +11,22 @@ namespace Skyline.DataMiner.Utils.InteractiveAutomationScript /// This allows attaching custom metadata to tree view items. /// /// The type of the value associated with this tree view item. - public sealed class TreeViewItemOption : IEquatable> + public sealed class TreeViewItem : IEquatable> { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The underlying TreeViewItem. /// The value to associate with this tree view item. /// When item is null. - public TreeViewItemOption(TreeViewItem item, T value) + public TreeViewItem(TreeViewItem item, T value) { Item = item ?? throw new ArgumentNullException(nameof(item)); Value = value; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// Creates a TreeViewItem with the specified parameters. /// /// The unique key for the tree view item. @@ -35,12 +35,12 @@ public TreeViewItemOption(TreeViewItem item, T value) /// The type of the tree view item. /// The child items of this tree view item. /// When keyValue or displayValue is null. - public TreeViewItemOption( + public TreeViewItem( string keyValue, string displayValue, T value, TreeViewItem.TreeViewItemType itemType = TreeViewItem.TreeViewItemType.CheckBox, - IEnumerable> childItems = null) + IEnumerable> childItems = null) { if (keyValue == null) { @@ -136,7 +136,7 @@ public bool SupportsLazyLoading /// true if the specified object is equal to the current object; otherwise, false. public override bool Equals(object obj) { - return obj is TreeViewItemOption option && Equals(option); + return obj is TreeViewItem option && Equals(option); } /// @@ -144,7 +144,7 @@ public override bool Equals(object obj) /// /// The TreeViewItemOption to compare with the current TreeViewItemOption. /// true if the specified TreeViewItemOption is equal to the current TreeViewItemOption; otherwise, false. - public bool Equals(TreeViewItemOption other) + public bool Equals(TreeViewItem other) { if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(null, other)) return false; @@ -180,7 +180,7 @@ public override string ToString() /// The first object to compare. /// The second object to compare. /// true if the objects are equal; otherwise, false. - public static bool operator ==(TreeViewItemOption left, TreeViewItemOption right) + public static bool operator ==(TreeViewItem left, TreeViewItem right) { return Equals(left, right); } @@ -191,7 +191,7 @@ public override string ToString() /// The first object to compare. /// The second object to compare. /// true if the objects are not equal; otherwise, false. - public static bool operator !=(TreeViewItemOption left, TreeViewItemOption right) + public static bool operator !=(TreeViewItem left, TreeViewItem right) { return !Equals(left, right); } diff --git a/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs b/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs index 7c60a33..463e28f 100644 --- a/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs +++ b/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs @@ -12,8 +12,8 @@ public void Constructor_Test() { var rootItems = new[] { - new TreeViewItemOption("root1", "Root Item 1", 1), - new TreeViewItemOption("root2", "Root Item 2", 2) + new TreeViewItem("root1", "Root Item 1", 1), + new TreeViewItem("root2", "Root Item 2", 2) }; var treeView = new TreeView(rootItems); @@ -28,11 +28,11 @@ public void Constructor_WithChildItems_Test() { var childItems = new[] { - new TreeViewItemOption("child1", "Child Item 1", "child1data"), - new TreeViewItemOption("child2", "Child Item 2", "child2data") + new TreeViewItem("child1", "Child Item 1", "child1data"), + new TreeViewItem("child2", "Child Item 2", "child2data") }; - var rootItem = new TreeViewItemOption("root", "Root Item", "rootdata", childItems: childItems); + var rootItem = new TreeViewItem("root", "Root Item", "rootdata", childItems: childItems); var treeView = new TreeView(new[] { rootItem }); Assert.IsNotNull(treeView); @@ -45,8 +45,8 @@ public void TryFindTreeViewItem_Test() { var rootItems = new[] { - new TreeViewItemOption("root1", "Root Item 1", 100), - new TreeViewItemOption("root2", "Root Item 2", 200) + new TreeViewItem("root1", "Root Item 1", 100), + new TreeViewItem("root2", "Root Item 2", 200) }; var treeView = new TreeView(rootItems); @@ -66,7 +66,7 @@ public void TryFindTreeViewItem_NotFound_Test() { var rootItems = new[] { - new TreeViewItemOption("root1", "Root Item 1", 100) + new TreeViewItem("root1", "Root Item 1", 100) }; var treeView = new TreeView(rootItems); @@ -81,9 +81,9 @@ public void TryFindTreeViewItem_NotFound_Test() [TestMethod] public void GetAllItems_Test() { - var child1 = new TreeViewItemOption("child1", "Child 1", 10); - var child2 = new TreeViewItemOption("child2", "Child 2", 20); - var rootItem = new TreeViewItemOption("root", "Root", 1, childItems: new[] { child1, child2 }); + var child1 = new TreeViewItem("child1", "Child 1", 10); + var child2 = new TreeViewItem("child2", "Child 2", 20); + var rootItem = new TreeViewItem("root", "Root", 1, childItems: new[] { child1, child2 }); var treeView = new TreeView(new[] { rootItem }); treeView.UpdateItemCache(); @@ -99,8 +99,8 @@ public void GetAllItems_Test() [TestMethod] public void GetItems_Depth0_Test() { - var child = new TreeViewItemOption("child", "Child", 10); - var root = new TreeViewItemOption("root", "Root", 1, childItems: new[] { child }); + var child = new TreeViewItem("child", "Child", 10); + var root = new TreeViewItem("root", "Root", 1, childItems: new[] { child }); var treeView = new TreeView(new[] { root }); treeView.UpdateItemCache(); @@ -114,8 +114,8 @@ public void GetItems_Depth0_Test() [TestMethod] public void GetItems_Depth1_Test() { - var child = new TreeViewItemOption("child", "Child", 10); - var root = new TreeViewItemOption("root", "Root", 1, childItems: new[] { child }); + var child = new TreeViewItem("child", "Child", 10); + var root = new TreeViewItem("root", "Root", 1, childItems: new[] { child }); var treeView = new TreeView(new[] { root }); treeView.UpdateItemCache(); @@ -129,9 +129,9 @@ public void GetItems_Depth1_Test() [TestMethod] public void CheckedValues_Test() { - var item1 = new TreeViewItemOption("item1", "Item 1", 100); - var item2 = new TreeViewItemOption("item2", "Item 2", 200); - var item3 = new TreeViewItemOption("item3", "Item 3", 300); + var item1 = new TreeViewItem("item1", "Item 1", 100); + var item2 = new TreeViewItem("item2", "Item 2", 200); + var item3 = new TreeViewItem("item3", "Item 3", 300); var treeView = new TreeView(new[] { item1, item2, item3 }); treeView.UpdateItemCache(); @@ -152,8 +152,8 @@ public void CheckedValues_Test() [TestMethod] public void CollapseAndExpand_Test() { - var child = new TreeViewItemOption("child", "Child", 10); - var root = new TreeViewItemOption("root", "Root", 1, childItems: new[] { child }); + var child = new TreeViewItem("child", "Child", 10); + var root = new TreeViewItem("root", "Root", 1, childItems: new[] { child }); var treeView = new TreeView(new[] { root }); treeView.UpdateItemCache(); @@ -172,9 +172,9 @@ public void CollapseAndExpand_Test() [TestMethod] public void TreeViewItemOption_Equals_Test() { - var item1 = new TreeViewItemOption("key1", "Display 1", 100); - var item2 = new TreeViewItemOption("key1", "Display 1", 100); - var item3 = new TreeViewItemOption("key2", "Display 2", 200); + var item1 = new TreeViewItem("key1", "Display 1", 100); + var item2 = new TreeViewItem("key1", "Display 1", 100); + var item3 = new TreeViewItem("key2", "Display 2", 200); Assert.AreEqual(item1, item2); Assert.AreNotEqual(item1, item3); @@ -183,7 +183,7 @@ public void TreeViewItemOption_Equals_Test() [TestMethod] public void TreeViewItemOption_Properties_Test() { - var item = new TreeViewItemOption("key", "display", "value"); + var item = new TreeViewItem("key", "display", "value"); Assert.AreEqual("key", item.KeyValue); Assert.AreEqual("display", item.DisplayValue); @@ -200,9 +200,9 @@ public void TreeViewItemOption_Properties_Test() [TestMethod] public void CheckedLeaves_And_CheckedNodes_Test() { - var leaf1 = new TreeViewItemOption("leaf1", "Leaf 1", 10); - var leaf2 = new TreeViewItemOption("leaf2", "Leaf 2", 20); - var node = new TreeViewItemOption("node", "Node", 100, childItems: new[] { leaf1, leaf2 }); + var leaf1 = new TreeViewItem("leaf1", "Leaf 1", 10); + var leaf2 = new TreeViewItem("leaf2", "Leaf 2", 20); + var node = new TreeViewItem("node", "Node", 100, childItems: new[] { leaf1, leaf2 }); var treeView = new TreeView(new[] { node }); treeView.UpdateItemCache(); @@ -231,7 +231,7 @@ public void CheckedLeaves_And_CheckedNodes_Test() [TestMethod] public void EmptyConstructor_Test() { - var treeView = new TreeView(System.Linq.Enumerable.Empty>()); + var treeView = new TreeView(System.Linq.Enumerable.Empty>()); Assert.IsNotNull(treeView); Assert.AreEqual(0, treeView.Items.Count()); @@ -253,7 +253,7 @@ public void ParameterlessConstructor_Test() public void NullValue_Test() { // Test that null values are handled correctly - var item = new TreeViewItemOption("key", "display", null); + var item = new TreeViewItem("key", "display", null); Assert.IsNull(item.Value); Assert.AreEqual("key", item.KeyValue); @@ -265,7 +265,7 @@ public void ComplexType_Test() { // Test with a complex custom type var customData = new { Id = 123, Name = "Test", Active = true }; - var item = new TreeViewItemOption("key", "display", customData); + var item = new TreeViewItem("key", "display", customData); Assert.IsNotNull(item.Value); Assert.AreEqual(customData, item.Value); From 11f4572001b21fcdf0e61f2fc3854e98a75e7758 Mon Sep 17 00:00:00 2001 From: Tom Waterbley Date: Thu, 23 Oct 2025 12:45:02 +0200 Subject: [PATCH 2/7] Update --- .../Components/Generics/GenericTreeView.cs | 272 +++++------------- .../Components/Interfaces/ITreeView.cs | 15 + .../Components/TreeView.cs | 65 ++--- .../Components/TreeViewItem.cs | 61 ++-- .../GenericTreeViewTests.cs | 18 +- 5 files changed, 158 insertions(+), 273 deletions(-) diff --git a/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs b/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs index 87bcd9f..de460d8 100644 --- a/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs +++ b/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs @@ -8,11 +8,13 @@ namespace Skyline.DataMiner.Utils.InteractiveAutomationScript using Skyline.DataMiner.Net.AutomationUI.Objects; /// - /// A generic tree view structure that allows attaching custom metadata to each item. + /// A generic tree view structure that allows attaching custom metadata to each item. /// /// The type of the value associated with each tree view item. public class TreeView : TreeViewBase, ITreeView { + private readonly List> rootItems = new List>(); + private Dictionary checkedItemCache; private Dictionary collapsedItemCache; private Dictionary> lookupTable; @@ -33,21 +35,19 @@ public class TreeView : TreeViewBase, ITreeView private List> collapsedItems = new List>(); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public TreeView() : this(Enumerable.Empty>()) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Root nodes of the tree view. public TreeView(IEnumerable> treeViewItems) { - Type = UIBlockType.TreeView; Items = treeViewItems; - IsReadOnly = false; } /// @@ -73,8 +73,8 @@ public event EventHandler>> Changed } /// - /// Triggered whenever an item is selected. - /// WantsOnChange will be set to true when this event is subscribed to. + /// Triggered whenever an item is selected. + /// WantsOnChange will be set to true when this event is subscribed to. /// public event EventHandler>> Checked { @@ -95,8 +95,8 @@ public event EventHandler>> Checked } /// - /// Triggered whenever an item is no longer selected. - /// WantsOnChange will be set to true when this event is subscribed to. + /// Triggered whenever an item is no longer selected. + /// WantsOnChange will be set to true when this event is subscribed to. /// public event EventHandler>> Unchecked { @@ -117,9 +117,9 @@ public event EventHandler>> Unchecked } /// - /// Triggered whenever an item is expanded. - /// Can be used for lazy loading. - /// Will be triggered whenever a node with SupportsLazyLoading set to true is expanded. + /// Triggered whenever an item is expanded. + /// Can be used for lazy loading. + /// Will be triggered whenever a node with SupportsLazyLoading set to true is expanded. /// public event EventHandler>> Expanded { @@ -135,8 +135,8 @@ public event EventHandler>> Expanded } /// - /// Triggered whenever an item is collapsed. - /// Will be triggered whenever a node with SupportsLazyLoading set to true is collapsed. + /// Triggered whenever an item is collapsed. + /// Will be triggered whenever a node with SupportsLazyLoading set to true is collapsed. /// public event EventHandler>> Collapsed { @@ -161,17 +161,12 @@ public event EventHandler>> Collapsed private event EventHandler>> OnCollapsed; - /// - /// Gets or sets the top-level items in the tree view. - /// The TreeViewItemOption.Item.ChildItems property can be used to navigate further down the tree. - /// + /// public IEnumerable> Items { get { - // Return the TreeViewItemOption wrappers based on the underlying TreeViewItems - return BlockDefinition.TreeViewItems.Select(item => lookupTable.Values.FirstOrDefault(x => x.Item == item)) - .Where(x => x != null); + return rootItems; } set @@ -181,14 +176,16 @@ public IEnumerable> Items throw new ArgumentNullException(nameof(value)); } + rootItems.Clear(); + rootItems.AddRange(value); + BlockDefinition.TreeViewItems = new List(value.Select(x => x.Item)); - UpdateItemCache(value); + + UpdateItemCache(); } } - /// - /// Gets all items in the tree view that are selected. - /// + /// public IEnumerable> CheckedItems { get @@ -197,9 +194,7 @@ public IEnumerable> CheckedItems } } - /// - /// Gets all leaves (= items without children) in the tree view that are selected. - /// + /// public IEnumerable> CheckedLeaves { get @@ -208,9 +203,7 @@ public IEnumerable> CheckedLeaves } } - /// - /// Gets all nodes (= items with children) in the tree view that are selected. - /// + /// public IEnumerable> CheckedNodes { get @@ -219,9 +212,7 @@ public IEnumerable> CheckedNodes } } - /// - /// Gets the values of all items in the tree view that are selected. - /// + /// public IEnumerable CheckedValues { get @@ -230,9 +221,7 @@ public IEnumerable CheckedValues } } - /// - /// Gets the values of all leaves in the tree view that are selected. - /// + /// public IEnumerable CheckedLeafValues { get @@ -241,9 +230,7 @@ public IEnumerable CheckedLeafValues } } - /// - /// Gets the values of all nodes in the tree view that are selected. - /// + /// public IEnumerable CheckedNodeValues { get @@ -270,88 +257,55 @@ public override void Expand() } } - /// - /// Can be used to retrieve an item from the tree view based on its key value. - /// - /// Key used to search for the item. - /// Item in the tree that matches the provided key. - /// True if the item was found, otherwise false. + /// public bool TryFindTreeViewItem(string key, out TreeViewItem item) { return lookupTable.TryGetValue(key, out item); } - /// - /// This method is used to update the cached TreeViewItems and lookup table. - /// This is done after loading the results from the UI Block, after handling the Events or when setting the Items. - /// This method should only be called from outside the TreeView if you are checking or collapsing items from outside of the TreeView and need to access the CheckedItems or CollapsedItems. - /// + /// public override void UpdateItemCache() { - // Don't pass Items here as it would cause recursion - just update the existing cache - var newCheckedItemCache = new Dictionary(); - var newCollapsedItemCache = new Dictionary(); - - // Ensure lookupTable exists - if (lookupTable == null) - { - lookupTable = new Dictionary>(); - } + checkedItemCache = new Dictionary(); + collapsedItemCache = new Dictionary(); + lookupTable = new Dictionary>(); - // Update caches based on current lookup table - foreach (var item in lookupTable.Values) + foreach (var item in GetAllItems(rootItems)) { try { - newCheckedItemCache.Add(item.KeyValue, item.IsChecked); + checkedItemCache.Add(item.KeyValue, item.IsChecked); if (item.SupportsLazyLoading) { - newCollapsedItemCache.Add(item.KeyValue, item.IsCollapsed); + collapsedItemCache.Add(item.KeyValue, item.IsCollapsed); } + + lookupTable.Add(item.KeyValue, item); } catch (Exception e) { throw new TreeViewDuplicateItemsException(item.KeyValue, e); } } - - // Replace caches atomically - checkedItemCache = newCheckedItemCache; - collapsedItemCache = newCollapsedItemCache; } - /// - /// Iterates over all items in the tree and returns them in a flat collection. - /// - /// A flat collection containing all items in the tree view. + /// public IEnumerable> GetAllItems() { return lookupTable.Values; } - /// - /// Returns all items in the tree view that are located at the provided depth. - /// Whenever the requested depth is greater than the longest branch in the tree, an empty collection will be returned. - /// - /// Depth of the requested items. - /// All items in the tree view that are located at the provided depth. + /// public IEnumerable> GetItems(int depth) { return GetItems(Items, depth, 0); } - /// - /// Load any changes made through user interaction. - /// - /// - /// Represents the information a user has entered or selected in a dialog box of an interactive - /// Automation script. - /// - /// should be used as key to get the changes for this widget. + /// protected internal override void LoadResult(IUIResults uiResults) { - var checkedItemKeys = uiResults.GetCheckedItemKeys(this); - var expandedItemKeys = uiResults.GetExpandedItemKeys(this); + var checkedItemKeys = uiResults.GetCheckedItemKeys(this); // this includes all checked items + var expandedItemKeys = uiResults.GetExpandedItemKeys(this); // this includes all expanded items with LazyLoading set to true // Check for changes // Expanded Items @@ -377,10 +331,7 @@ protected internal override void LoadResult(IUIResults uiResults) foreach (string changedItemKey in changedItemKeys) { - if (lookupTable.TryGetValue(changedItemKey, out var item)) - { - changedItems.Add(item); - } + changedItems.Add(lookupTable[changedItemKey]); } } @@ -394,11 +345,7 @@ protected internal override void LoadResult(IUIResults uiResults) UpdateItemCache(); } - /// - /// Raises zero or more events of the widget. - /// This method is called after was called on all widgets. - /// - /// It is up to the implementer to determine if an event must be raised. + /// protected internal override void RaiseResultEvents() { // Expanded items @@ -441,7 +388,7 @@ protected internal override void RaiseResultEvents() } /// - /// Returns all items in the TreeView that are checked. + /// Returns all items in the TreeView that are checked. /// /// All checked TreeViewItems in the TreeView. private IEnumerable> GetCheckedItems() @@ -450,31 +397,31 @@ private IEnumerable> GetCheckedItems() } /// - /// This method is used to recursively go through all the items in the TreeView. + /// This method is used to recursively go through all the items in the TreeView. /// /// List of TreeViewItems to be visited. - /// Parent TreeViewItemOption wrappers. /// Flat collection containing every item in the provided children collection and all underlying items. - private IEnumerable> GetAllItemsRecursive(IEnumerable children, IEnumerable> parentOptions) + private IEnumerable> GetAllItems(IEnumerable> children) { - List> allItems = new List>(); - foreach (var item in children) + if (children == null) + yield break; + + var queue = new Queue>(children); + + while (queue.Count > 0) { - var option = parentOptions.FirstOrDefault(x => x.Item == item); - if (option != null) + var item = queue.Dequeue(); + yield return item; + + foreach (var child in item.ChildItems) { - allItems.Add(option); - // For child items, we need to search in the lookup table - var childOptions = option.ChildItems.Select(child => lookupTable.Values.FirstOrDefault(x => x.Item == child)).Where(x => x != null); - allItems.AddRange(GetAllItemsRecursive(option.ChildItems, childOptions)); + queue.Enqueue(child); } } - - return allItems; } /// - /// Returns all TreeViewItems in the TreeView that are located on the provided depth. + /// Returns all TreeViewItems in the TreeView that are located on the provided depth. /// /// Items to be checked. /// Depth that was requested. @@ -493,8 +440,7 @@ private IEnumerable> GetItems(IEnumerable> child else { int newDepth = currentDepth + 1; - var childOptions = item.ChildItems.Select(child => lookupTable.Values.FirstOrDefault(x => x.Item == child)).Where(x => x != null); - requestedItems.AddRange(GetItems(childOptions, requestedDepth, newDepth)); + requestedItems.AddRange(GetItems(item.ChildItems, requestedDepth, newDepth)); } } @@ -511,10 +457,7 @@ private void RegisterExpandedItems(IEnumerable expandedItemKeys) foreach (string newlyExpandedItemKey in newlyExpandedItems) { - if (lookupTable.TryGetValue(newlyExpandedItemKey, out var item)) - { - expandedItems.Add(item); - } + expandedItems.Add(lookupTable[newlyExpandedItemKey]); } } } @@ -529,10 +472,7 @@ private void RegisterCollapsedItems(IEnumerable expandedItemKeys) foreach (string newlyCollapsedItemKey in newlyCollapsedItems) { - if (lookupTable.TryGetValue(newlyCollapsedItemKey, out var item)) - { - collapsedItems.Add(item); - } + collapsedItems.Add(lookupTable[newlyCollapsedItemKey]); } } } @@ -547,10 +487,7 @@ private List RegisterCheckedItems(IEnumerable checkedItemKeys) foreach (string newlyCheckedItemKey in newlyCheckedItemKeys) { - if (lookupTable.TryGetValue(newlyCheckedItemKey, out var item)) - { - checkedItems.Add(item); - } + checkedItems.Add(lookupTable[newlyCheckedItemKey]); } } @@ -567,92 +504,11 @@ private List RegisterUncheckedItems(IEnumerable checkedItemKeys) foreach (string newlyUncheckedItemKey in newlyUncheckedItemKeys) { - if (lookupTable.TryGetValue(newlyUncheckedItemKey, out var item)) - { - uncheckedItems.Add(item); - } + uncheckedItems.Add(lookupTable[newlyUncheckedItemKey]); } } return newlyUncheckedItemKeys; } - - private void UpdateItemCache(IEnumerable> items) - { - // Initialize new caches but preserve the existing lookup table if items is null or empty - var newCheckedItemCache = new Dictionary(); - var newCollapsedItemCache = new Dictionary(); - - // Only rebuild lookup table if we have new items to process - if (items != null && items.Any()) - { - lookupTable = new Dictionary>(); - BuildLookupTable(items); - } - else if (lookupTable == null) - { - // Initialize empty lookup table only if it doesn't exist - lookupTable = new Dictionary>(); - } - - // Update caches based on current lookup table - foreach (var item in lookupTable.Values) - { - try - { - newCheckedItemCache.Add(item.KeyValue, item.IsChecked); - if (item.SupportsLazyLoading) - { - newCollapsedItemCache.Add(item.KeyValue, item.IsCollapsed); - } - } - catch (Exception e) - { - throw new TreeViewDuplicateItemsException(item.KeyValue, e); - } - } - - // Replace caches atomically - checkedItemCache = newCheckedItemCache; - collapsedItemCache = newCollapsedItemCache; - } - - private void BuildLookupTable(IEnumerable> items) - { - foreach (var item in items) - { - lookupTable[item.KeyValue] = item; - - // Recursively process children - var childOptions = item.ChildItems.Select(child => - { - // Try to find existing wrapper or create a temporary one - var existing = lookupTable.Values.FirstOrDefault(x => x.Item == child); - return existing; - }).Where(x => x != null); - - if (item.ChildItems.Any()) - { - // For children that don't have wrappers yet, we need to handle them - // This is a limitation - child items added directly to TreeViewItem won't have metadata - BuildLookupTableFromTreeViewItems(item.ChildItems); - } - } - } - - private void BuildLookupTableFromTreeViewItems(IEnumerable items) - { - foreach (var item in items) - { - if (!lookupTable.ContainsKey(item.KeyValue)) - { - // Create a wrapper with default value for items without explicit metadata - var wrapper = new TreeViewItem(item, default(T)); - lookupTable[item.KeyValue] = wrapper; - } - - BuildLookupTableFromTreeViewItems(item.ChildItems); - } - } } } diff --git a/InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs b/InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs index 8b1a2dc..9457310 100644 --- a/InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs +++ b/InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs @@ -80,6 +80,21 @@ public interface ITreeView : ITreeViewBase /// IEnumerable> CheckedNodes { get; } + /// + /// Gets the values of all items in the tree view that are selected. + /// + IEnumerable CheckedValues { get; } + + /// + /// Gets the values of all leaves in the tree view that are selected. + /// + IEnumerable CheckedLeafValues { get; } + + /// + /// Gets the values of all nodes in the tree view that are selected. + /// + IEnumerable CheckedNodeValues { get; } + /// /// Iterates over all items in the tree and returns them in a flat collection. /// diff --git a/InteractiveAutomationToolkit/Components/TreeView.cs b/InteractiveAutomationToolkit/Components/TreeView.cs index 1eef587..e6e7898 100644 --- a/InteractiveAutomationToolkit/Components/TreeView.cs +++ b/InteractiveAutomationToolkit/Components/TreeView.cs @@ -12,6 +12,7 @@ /// public class TreeView : TreeViewBase, ITreeView { + private readonly List rootItems = new List(); private Dictionary checkedItemCache; private Dictionary collapsedItemCache; // TODO: should only contain Items with LazyLoading set to true private Dictionary lookupTable; @@ -44,9 +45,7 @@ public TreeView() : this(Enumerable.Empty()) /// Root nodes of the tree view. public TreeView(IEnumerable treeViewItems) { - Type = UIBlockType.TreeView; Items = treeViewItems; - IsReadOnly = false; } /// @@ -165,17 +164,21 @@ public IEnumerable Items { get { - return BlockDefinition.TreeViewItems; + return rootItems; } set { if (value == null) { - throw new ArgumentNullException("value"); + throw new ArgumentNullException(nameof(value)); } - BlockDefinition.TreeViewItems = new List(value); + rootItems.Clear(); + rootItems.AddRange(value); + + BlockDefinition.TreeViewItems = rootItems; + UpdateItemCache(); } } @@ -228,8 +231,7 @@ public override void Expand() /// public bool TryFindTreeViewItem(string key, out TreeViewItem item) { - item = GetAllItems().FirstOrDefault(x => x.KeyValue.Equals(key)); - return item != null; + return lookupTable.TryGetValue(key, out item); } /// @@ -239,7 +241,7 @@ public override void UpdateItemCache() collapsedItemCache = new Dictionary(); lookupTable = new Dictionary(); - foreach (var item in GetAllItems()) + foreach (var item in GetAllItems(rootItems)) { try { @@ -261,14 +263,7 @@ public override void UpdateItemCache() /// public IEnumerable GetAllItems() { - List allItems = new List(); - foreach (var item in Items) - { - allItems.Add(item); - allItems.AddRange(GetAllItems(item.ChildItems)); - } - - return allItems; + return lookupTable.Values; } /// @@ -277,14 +272,7 @@ public IEnumerable GetItems(int depth) return GetItems(Items, depth, 0); } - /// - /// Load any changes made through user interaction. - /// - /// - /// Represents the information a user has entered or selected in a dialog box of an interactive - /// Automation script. - /// - /// should be used as key to get the changes for this widget. + /// protected internal override void LoadResult(IUIResults uiResults) { var checkedItemKeys = uiResults.GetCheckedItemKeys(this); // this includes all checked items @@ -328,11 +316,7 @@ protected internal override void LoadResult(IUIResults uiResults) UpdateItemCache(); } - /// - /// Raises zero or more events of the widget. - /// This method is called after was called on all widgets. - /// - /// It is up to the implementer to determine if an event must be raised. + /// protected internal override void RaiseResultEvents() { // Expanded items @@ -390,14 +374,21 @@ private IEnumerable GetCheckedItems() /// Flat collection containing every item in the provided children collection and all underlying items. private IEnumerable GetAllItems(IEnumerable children) { - List allItems = new List(); - foreach (var item in children) + if (children == null) + yield break; + + var queue = new Queue(children); + + while (queue.Count > 0) { - allItems.Add(item); - allItems.AddRange(GetAllItems(item.ChildItems)); - } + var item = queue.Dequeue(); + yield return item; - return allItems; + foreach (var child in item.ChildItems) + { + queue.Enqueue(child); + } + } } /// @@ -450,9 +441,9 @@ private void RegisterCollapsedItems(IEnumerable expandedItemKeys) itemsCollapsed = true; collapsedItems = new List(); - foreach (string newyCollapsedItemKey in newlyCollapsedItems) + foreach (string newlyCollapsedItemKey in newlyCollapsedItems) { - collapsedItems.Add(lookupTable[newyCollapsedItemKey]); + collapsedItems.Add(lookupTable[newlyCollapsedItemKey]); } } } diff --git a/InteractiveAutomationToolkit/Components/TreeViewItem.cs b/InteractiveAutomationToolkit/Components/TreeViewItem.cs index 923585f..1f51603 100644 --- a/InteractiveAutomationToolkit/Components/TreeViewItem.cs +++ b/InteractiveAutomationToolkit/Components/TreeViewItem.cs @@ -7,39 +7,62 @@ namespace Skyline.DataMiner.Utils.InteractiveAutomationScript using Skyline.DataMiner.Net.AutomationUI.Objects; /// - /// Represents a TreeViewItem with an associated value of type . + /// Represents a with an associated value of type . /// This allows attaching custom metadata to tree view items. /// /// The type of the value associated with this tree view item. public sealed class TreeViewItem : IEquatable> { + private readonly List> _childItems; + /// /// Initializes a new instance of the class. /// - /// The underlying TreeViewItem. + /// The underlying . /// The value to associate with this tree view item. + /// The child items of this tree view item. /// When item is null. - public TreeViewItem(TreeViewItem item, T value) + public TreeViewItem( + TreeViewItem item, + T value, + IEnumerable> childItems = null) { Item = item ?? throw new ArgumentNullException(nameof(item)); Value = value; + + _childItems = childItems?.ToList() ?? new List>(); + + // Use hashset for fast lookup of already wrapped items + var existingChildren = _childItems.Select(x => x.Item).ToHashSet(); + + // Wrap any children from the underlying item that weren't included + foreach (var child in item.ChildItems) + { + if (!existingChildren.Contains(child)) + { + var wrappedChild = new TreeViewItem(child, default); + _childItems.Add(wrappedChild); + existingChildren.Add(child); + } + } + + // Sync underlying TreeViewItem + Item.ChildItems = _childItems.Select(x => x.Item).ToList(); } /// /// Initializes a new instance of the class. - /// Creates a TreeViewItem with the specified parameters. + /// Creates a with the specified parameters. /// /// The unique key for the tree view item. /// The text to display for the tree view item. /// The value to associate with this tree view item. - /// The type of the tree view item. /// The child items of this tree view item. /// When keyValue or displayValue is null. public TreeViewItem( string keyValue, string displayValue, T value, - TreeViewItem.TreeViewItemType itemType = TreeViewItem.TreeViewItemType.CheckBox, IEnumerable> childItems = null) { if (keyValue == null) @@ -52,16 +75,14 @@ public TreeViewItem( throw new ArgumentNullException(nameof(displayValue)); } - var children = childItems?.Select(x => x.Item).ToList() ?? new List(); - Item = new TreeViewItem(displayValue, keyValue, children) - { - ItemType = itemType - }; + _childItems = childItems?.ToList() ?? new List>(); + + Item = new TreeViewItem(displayValue, keyValue, _childItems.Select(x => x.Item).ToList()); Value = value; } /// - /// Gets the underlying TreeViewItem. + /// Gets the underlying . /// public TreeViewItem Item { get; } @@ -127,7 +148,7 @@ public bool SupportsLazyLoading /// /// Gets the child items of this tree view item. /// - public IEnumerable ChildItems => Item.ChildItems; + public IEnumerable> ChildItems => _childItems; /// /// Determines whether the specified object is equal to the current object. @@ -140,16 +161,16 @@ public override bool Equals(object obj) } /// - /// Determines whether the specified TreeViewItemOption is equal to the current TreeViewItemOption. + /// Determines whether the specified is equal to the current . /// - /// The TreeViewItemOption to compare with the current TreeViewItemOption. - /// true if the specified TreeViewItemOption is equal to the current TreeViewItemOption; otherwise, false. + /// The to compare with the current . + /// true if the specified is equal to the current ; otherwise, false. public bool Equals(TreeViewItem other) { if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(null, other)) return false; - return Item.KeyValue.Equals(other.Item.KeyValue) && + return EqualityComparer.Default.Equals(Item, other.Item) && EqualityComparer.Default.Equals(Value, other.Value); } @@ -160,7 +181,7 @@ public bool Equals(TreeViewItem other) public override int GetHashCode() { int hashCode = 11; - hashCode ^= 13 * Item.KeyValue.GetHashCode(); + hashCode ^= 13 * Item.GetHashCode(); hashCode ^= 13 * (Value != null ? Value.GetHashCode() : 0); return hashCode; } @@ -175,7 +196,7 @@ public override string ToString() } /// - /// Determines whether two TreeViewItemOption objects are equal. + /// Determines whether two objects are equal. /// /// The first object to compare. /// The second object to compare. @@ -186,7 +207,7 @@ public override string ToString() } /// - /// Determines whether two TreeViewItemOption objects are not equal. + /// Determines whether two objects are not equal. /// /// The first object to compare. /// The second object to compare. diff --git a/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs b/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs index 463e28f..3c95595 100644 --- a/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs +++ b/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs @@ -2,6 +2,8 @@ using Skyline.DataMiner.Utils.InteractiveAutomationScript; using System.Linq; +using static Skyline.DataMiner.Net.AutomationUI.Objects.TreeViewItem; + namespace InteractiveAutomationToolkitTests { [TestClass] @@ -129,9 +131,9 @@ public void GetItems_Depth1_Test() [TestMethod] public void CheckedValues_Test() { - var item1 = new TreeViewItem("item1", "Item 1", 100); - var item2 = new TreeViewItem("item2", "Item 2", 200); - var item3 = new TreeViewItem("item3", "Item 3", 300); + var item1 = new TreeViewItem("item1", "Item 1", 100) { ItemType = TreeViewItemType.CheckBox }; + var item2 = new TreeViewItem("item2", "Item 2", 200) { ItemType = TreeViewItemType.CheckBox }; + var item3 = new TreeViewItem("item3", "Item 3", 300) { ItemType = TreeViewItemType.CheckBox }; var treeView = new TreeView(new[] { item1, item2, item3 }); treeView.UpdateItemCache(); @@ -170,7 +172,7 @@ public void CollapseAndExpand_Test() } [TestMethod] - public void TreeViewItemOption_Equals_Test() + public void TreeViewItem_Equals_Test() { var item1 = new TreeViewItem("key1", "Display 1", 100); var item2 = new TreeViewItem("key1", "Display 1", 100); @@ -181,7 +183,7 @@ public void TreeViewItemOption_Equals_Test() } [TestMethod] - public void TreeViewItemOption_Properties_Test() + public void TreeViewItem_Properties_Test() { var item = new TreeViewItem("key", "display", "value"); @@ -200,9 +202,9 @@ public void TreeViewItemOption_Properties_Test() [TestMethod] public void CheckedLeaves_And_CheckedNodes_Test() { - var leaf1 = new TreeViewItem("leaf1", "Leaf 1", 10); - var leaf2 = new TreeViewItem("leaf2", "Leaf 2", 20); - var node = new TreeViewItem("node", "Node", 100, childItems: new[] { leaf1, leaf2 }); + var leaf1 = new TreeViewItem("leaf1", "Leaf 1", 10) { ItemType = TreeViewItemType.CheckBox }; + var leaf2 = new TreeViewItem("leaf2", "Leaf 2", 20) { ItemType = TreeViewItemType.CheckBox }; + var node = new TreeViewItem("node", "Node", 100, childItems: new[] { leaf1, leaf2 }) { ItemType = TreeViewItemType.CheckBox }; var treeView = new TreeView(new[] { node }); treeView.UpdateItemCache(); From 530e783a331d7972899d82aabb5d0a1493388787 Mon Sep 17 00:00:00 2001 From: Tom Waterbley Date: Thu, 23 Oct 2025 10:20:34 +0200 Subject: [PATCH 3/7] Add generic TreeView control --- .../Components/Generics/GenericTreeView.cs | 658 ++++++++++++++++++ .../Components/Interfaces/ITreeView.cs | 105 +++ .../Components/Interfaces/ITreeViewBase.cs | 30 + .../Components/TreeView.cs | 94 +-- .../Components/TreeViewBase.cs | 58 ++ .../Components/TreeViewItem.cs | 199 ++++++ .../GenericTreeViewTests.cs | 274 ++++++++ 7 files changed, 1337 insertions(+), 81 deletions(-) create mode 100644 InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs create mode 100644 InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs create mode 100644 InteractiveAutomationToolkit/Components/Interfaces/ITreeViewBase.cs create mode 100644 InteractiveAutomationToolkit/Components/TreeViewBase.cs create mode 100644 InteractiveAutomationToolkit/Components/TreeViewItem.cs create mode 100644 InteractiveAutomationToolkitTests/GenericTreeViewTests.cs diff --git a/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs b/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs new file mode 100644 index 0000000..87bcd9f --- /dev/null +++ b/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs @@ -0,0 +1,658 @@ +namespace Skyline.DataMiner.Utils.InteractiveAutomationScript +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using Skyline.DataMiner.Automation; + using Skyline.DataMiner.Net.AutomationUI.Objects; + + /// + /// A generic tree view structure that allows attaching custom metadata to each item. + /// + /// The type of the value associated with each tree view item. + public class TreeView : TreeViewBase, ITreeView + { + private Dictionary checkedItemCache; + private Dictionary collapsedItemCache; + private Dictionary> lookupTable; + + private bool itemsChanged = false; + private List> changedItems = new List>(); + + private bool itemsChecked = false; + private List> checkedItems = new List>(); + + private bool itemsUnchecked = false; + private List> uncheckedItems = new List>(); + + private bool itemsExpanded = false; + private List> expandedItems = new List>(); + + private bool itemsCollapsed = false; + private List> collapsedItems = new List>(); + + /// + /// Initializes a new instance of the class. + /// + public TreeView() : this(Enumerable.Empty>()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Root nodes of the tree view. + public TreeView(IEnumerable> treeViewItems) + { + Type = UIBlockType.TreeView; + Items = treeViewItems; + IsReadOnly = false; + } + + /// + /// Triggered when a different item is selected or no longer selected. + /// WantsOnChange will be set to true when this event is subscribed to. + /// + public event EventHandler>> Changed + { + add + { + OnChanged += value; + BlockDefinition.WantsOnChange = true; + } + + remove + { + OnChanged -= value; + if (OnChanged == null || !OnChanged.GetInvocationList().Any()) + { + BlockDefinition.WantsOnChange = false; + } + } + } + + /// + /// Triggered whenever an item is selected. + /// WantsOnChange will be set to true when this event is subscribed to. + /// + public event EventHandler>> Checked + { + add + { + OnChecked += value; + BlockDefinition.WantsOnChange = true; + } + + remove + { + OnChecked -= value; + if (OnChecked == null || !OnChecked.GetInvocationList().Any()) + { + BlockDefinition.WantsOnChange = false; + } + } + } + + /// + /// Triggered whenever an item is no longer selected. + /// WantsOnChange will be set to true when this event is subscribed to. + /// + public event EventHandler>> Unchecked + { + add + { + OnUnchecked += value; + BlockDefinition.WantsOnChange = true; + } + + remove + { + OnUnchecked -= value; + if (OnUnchecked == null || !OnUnchecked.GetInvocationList().Any()) + { + BlockDefinition.WantsOnChange = false; + } + } + } + + /// + /// Triggered whenever an item is expanded. + /// Can be used for lazy loading. + /// Will be triggered whenever a node with SupportsLazyLoading set to true is expanded. + /// + public event EventHandler>> Expanded + { + add + { + OnExpanded += value; + } + + remove + { + OnExpanded -= value; + } + } + + /// + /// Triggered whenever an item is collapsed. + /// Will be triggered whenever a node with SupportsLazyLoading set to true is collapsed. + /// + public event EventHandler>> Collapsed + { + add + { + OnCollapsed += value; + } + + remove + { + OnCollapsed -= value; + } + } + + private event EventHandler>> OnChanged; + + private event EventHandler>> OnChecked; + + private event EventHandler>> OnUnchecked; + + private event EventHandler>> OnExpanded; + + private event EventHandler>> OnCollapsed; + + /// + /// Gets or sets the top-level items in the tree view. + /// The TreeViewItemOption.Item.ChildItems property can be used to navigate further down the tree. + /// + public IEnumerable> Items + { + get + { + // Return the TreeViewItemOption wrappers based on the underlying TreeViewItems + return BlockDefinition.TreeViewItems.Select(item => lookupTable.Values.FirstOrDefault(x => x.Item == item)) + .Where(x => x != null); + } + + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + BlockDefinition.TreeViewItems = new List(value.Select(x => x.Item)); + UpdateItemCache(value); + } + } + + /// + /// Gets all items in the tree view that are selected. + /// + public IEnumerable> CheckedItems + { + get + { + return GetCheckedItems(); + } + } + + /// + /// Gets all leaves (= items without children) in the tree view that are selected. + /// + public IEnumerable> CheckedLeaves + { + get + { + return GetCheckedItems().Where(x => !x.ChildItems.Any()); + } + } + + /// + /// Gets all nodes (= items with children) in the tree view that are selected. + /// + public IEnumerable> CheckedNodes + { + get + { + return GetCheckedItems().Where(x => x.ChildItems.Any()); + } + } + + /// + /// Gets the values of all items in the tree view that are selected. + /// + public IEnumerable CheckedValues + { + get + { + return GetCheckedItems().Select(x => x.Value); + } + } + + /// + /// Gets the values of all leaves in the tree view that are selected. + /// + public IEnumerable CheckedLeafValues + { + get + { + return CheckedLeaves.Select(x => x.Value); + } + } + + /// + /// Gets the values of all nodes in the tree view that are selected. + /// + public IEnumerable CheckedNodeValues + { + get + { + return CheckedNodes.Select(x => x.Value); + } + } + + /// + public override void Collapse() + { + foreach (var item in GetAllItems()) + { + item.IsCollapsed = true; + } + } + + /// + public override void Expand() + { + foreach (var item in GetAllItems()) + { + item.IsCollapsed = false; + } + } + + /// + /// Can be used to retrieve an item from the tree view based on its key value. + /// + /// Key used to search for the item. + /// Item in the tree that matches the provided key. + /// True if the item was found, otherwise false. + public bool TryFindTreeViewItem(string key, out TreeViewItem item) + { + return lookupTable.TryGetValue(key, out item); + } + + /// + /// This method is used to update the cached TreeViewItems and lookup table. + /// This is done after loading the results from the UI Block, after handling the Events or when setting the Items. + /// This method should only be called from outside the TreeView if you are checking or collapsing items from outside of the TreeView and need to access the CheckedItems or CollapsedItems. + /// + public override void UpdateItemCache() + { + // Don't pass Items here as it would cause recursion - just update the existing cache + var newCheckedItemCache = new Dictionary(); + var newCollapsedItemCache = new Dictionary(); + + // Ensure lookupTable exists + if (lookupTable == null) + { + lookupTable = new Dictionary>(); + } + + // Update caches based on current lookup table + foreach (var item in lookupTable.Values) + { + try + { + newCheckedItemCache.Add(item.KeyValue, item.IsChecked); + if (item.SupportsLazyLoading) + { + newCollapsedItemCache.Add(item.KeyValue, item.IsCollapsed); + } + } + catch (Exception e) + { + throw new TreeViewDuplicateItemsException(item.KeyValue, e); + } + } + + // Replace caches atomically + checkedItemCache = newCheckedItemCache; + collapsedItemCache = newCollapsedItemCache; + } + + /// + /// Iterates over all items in the tree and returns them in a flat collection. + /// + /// A flat collection containing all items in the tree view. + public IEnumerable> GetAllItems() + { + return lookupTable.Values; + } + + /// + /// Returns all items in the tree view that are located at the provided depth. + /// Whenever the requested depth is greater than the longest branch in the tree, an empty collection will be returned. + /// + /// Depth of the requested items. + /// All items in the tree view that are located at the provided depth. + public IEnumerable> GetItems(int depth) + { + return GetItems(Items, depth, 0); + } + + /// + /// Load any changes made through user interaction. + /// + /// + /// Represents the information a user has entered or selected in a dialog box of an interactive + /// Automation script. + /// + /// should be used as key to get the changes for this widget. + protected internal override void LoadResult(IUIResults uiResults) + { + var checkedItemKeys = uiResults.GetCheckedItemKeys(this); + var expandedItemKeys = uiResults.GetExpandedItemKeys(this); + + // Check for changes + // Expanded Items + RegisterExpandedItems(expandedItemKeys); + + // Collapsed Items + RegisterCollapsedItems(expandedItemKeys); + + // Checked Items + List newlyCheckedItemKeys = RegisterCheckedItems(checkedItemKeys); + + // Unchecked Items + List newlyUncheckedItemKeys = RegisterUncheckedItems(checkedItemKeys); + + // Changed Items + List changedItemKeys = new List(); + changedItemKeys.AddRange(newlyCheckedItemKeys); + changedItemKeys.AddRange(newlyUncheckedItemKeys); + if (changedItemKeys.Any() && OnChanged != null) + { + itemsChanged = true; + changedItems = new List>(); + + foreach (string changedItemKey in changedItemKeys) + { + if (lookupTable.TryGetValue(changedItemKey, out var item)) + { + changedItems.Add(item); + } + } + } + + // Persist states + foreach (TreeViewItem item in lookupTable.Values) + { + item.Item.IsChecked = checkedItemKeys.Contains(item.KeyValue); + item.Item.IsCollapsed = !expandedItemKeys.Contains(item.KeyValue); + } + + UpdateItemCache(); + } + + /// + /// Raises zero or more events of the widget. + /// This method is called after was called on all widgets. + /// + /// It is up to the implementer to determine if an event must be raised. + protected internal override void RaiseResultEvents() + { + // Expanded items + if (itemsExpanded && OnExpanded != null) + { + OnExpanded(this, expandedItems); + } + + // Collapsed items + if (itemsCollapsed && OnCollapsed != null) + { + OnCollapsed(this, collapsedItems); + } + + // Checked items + if (itemsChecked && OnChecked != null) + { + OnChecked(this, checkedItems); + } + + // Unchecked items + if (itemsUnchecked && OnUnchecked != null) + { + OnUnchecked(this, uncheckedItems); + } + + // Changed items + if (itemsChanged && OnChanged != null) + { + OnChanged(this, changedItems); + } + + itemsExpanded = false; + itemsCollapsed = false; + itemsChecked = false; + itemsUnchecked = false; + itemsChanged = false; + + UpdateItemCache(); + } + + /// + /// Returns all items in the TreeView that are checked. + /// + /// All checked TreeViewItems in the TreeView. + private IEnumerable> GetCheckedItems() + { + return lookupTable.Values.Where(x => x.ItemType == TreeViewItem.TreeViewItemType.CheckBox && x.IsChecked); + } + + /// + /// This method is used to recursively go through all the items in the TreeView. + /// + /// List of TreeViewItems to be visited. + /// Parent TreeViewItemOption wrappers. + /// Flat collection containing every item in the provided children collection and all underlying items. + private IEnumerable> GetAllItemsRecursive(IEnumerable children, IEnumerable> parentOptions) + { + List> allItems = new List>(); + foreach (var item in children) + { + var option = parentOptions.FirstOrDefault(x => x.Item == item); + if (option != null) + { + allItems.Add(option); + // For child items, we need to search in the lookup table + var childOptions = option.ChildItems.Select(child => lookupTable.Values.FirstOrDefault(x => x.Item == child)).Where(x => x != null); + allItems.AddRange(GetAllItemsRecursive(option.ChildItems, childOptions)); + } + } + + return allItems; + } + + /// + /// Returns all TreeViewItems in the TreeView that are located on the provided depth. + /// + /// Items to be checked. + /// Depth that was requested. + /// Current depth in the tree. + /// All TreeViewItems in the TreeView that are located on the provided depth. + private IEnumerable> GetItems(IEnumerable> children, int requestedDepth, int currentDepth) + { + List> requestedItems = new List>(); + bool depthReached = requestedDepth == currentDepth; + foreach (TreeViewItem item in children) + { + if (depthReached) + { + requestedItems.Add(item); + } + else + { + int newDepth = currentDepth + 1; + var childOptions = item.ChildItems.Select(child => lookupTable.Values.FirstOrDefault(x => x.Item == child)).Where(x => x != null); + requestedItems.AddRange(GetItems(childOptions, requestedDepth, newDepth)); + } + } + + return requestedItems; + } + + private void RegisterExpandedItems(IEnumerable expandedItemKeys) + { + List newlyExpandedItems = collapsedItemCache.Where(x => expandedItemKeys.Contains(x.Key) && x.Value).Select(x => x.Key).ToList(); + if (newlyExpandedItems.Any() && OnExpanded != null) + { + itemsExpanded = true; + expandedItems = new List>(); + + foreach (string newlyExpandedItemKey in newlyExpandedItems) + { + if (lookupTable.TryGetValue(newlyExpandedItemKey, out var item)) + { + expandedItems.Add(item); + } + } + } + } + + private void RegisterCollapsedItems(IEnumerable expandedItemKeys) + { + List newlyCollapsedItems = collapsedItemCache.Where(x => !expandedItemKeys.Contains(x.Key) && !x.Value).Select(x => x.Key).ToList(); + if (newlyCollapsedItems.Any() && OnCollapsed != null) + { + itemsCollapsed = true; + collapsedItems = new List>(); + + foreach (string newlyCollapsedItemKey in newlyCollapsedItems) + { + if (lookupTable.TryGetValue(newlyCollapsedItemKey, out var item)) + { + collapsedItems.Add(item); + } + } + } + } + + private List RegisterCheckedItems(IEnumerable checkedItemKeys) + { + List newlyCheckedItemKeys = checkedItemCache.Where(x => checkedItemKeys.Contains(x.Key) && !x.Value).Select(x => x.Key).ToList(); + if (newlyCheckedItemKeys.Any() && OnChecked != null) + { + itemsChecked = true; + checkedItems = new List>(); + + foreach (string newlyCheckedItemKey in newlyCheckedItemKeys) + { + if (lookupTable.TryGetValue(newlyCheckedItemKey, out var item)) + { + checkedItems.Add(item); + } + } + } + + return newlyCheckedItemKeys; + } + + private List RegisterUncheckedItems(IEnumerable checkedItemKeys) + { + List newlyUncheckedItemKeys = checkedItemCache.Where(x => !checkedItemKeys.Contains(x.Key) && x.Value).Select(x => x.Key).ToList(); + if (newlyUncheckedItemKeys.Any() && OnUnchecked != null) + { + itemsUnchecked = true; + uncheckedItems = new List>(); + + foreach (string newlyUncheckedItemKey in newlyUncheckedItemKeys) + { + if (lookupTable.TryGetValue(newlyUncheckedItemKey, out var item)) + { + uncheckedItems.Add(item); + } + } + } + + return newlyUncheckedItemKeys; + } + + private void UpdateItemCache(IEnumerable> items) + { + // Initialize new caches but preserve the existing lookup table if items is null or empty + var newCheckedItemCache = new Dictionary(); + var newCollapsedItemCache = new Dictionary(); + + // Only rebuild lookup table if we have new items to process + if (items != null && items.Any()) + { + lookupTable = new Dictionary>(); + BuildLookupTable(items); + } + else if (lookupTable == null) + { + // Initialize empty lookup table only if it doesn't exist + lookupTable = new Dictionary>(); + } + + // Update caches based on current lookup table + foreach (var item in lookupTable.Values) + { + try + { + newCheckedItemCache.Add(item.KeyValue, item.IsChecked); + if (item.SupportsLazyLoading) + { + newCollapsedItemCache.Add(item.KeyValue, item.IsCollapsed); + } + } + catch (Exception e) + { + throw new TreeViewDuplicateItemsException(item.KeyValue, e); + } + } + + // Replace caches atomically + checkedItemCache = newCheckedItemCache; + collapsedItemCache = newCollapsedItemCache; + } + + private void BuildLookupTable(IEnumerable> items) + { + foreach (var item in items) + { + lookupTable[item.KeyValue] = item; + + // Recursively process children + var childOptions = item.ChildItems.Select(child => + { + // Try to find existing wrapper or create a temporary one + var existing = lookupTable.Values.FirstOrDefault(x => x.Item == child); + return existing; + }).Where(x => x != null); + + if (item.ChildItems.Any()) + { + // For children that don't have wrappers yet, we need to handle them + // This is a limitation - child items added directly to TreeViewItem won't have metadata + BuildLookupTableFromTreeViewItems(item.ChildItems); + } + } + } + + private void BuildLookupTableFromTreeViewItems(IEnumerable items) + { + foreach (var item in items) + { + if (!lookupTable.ContainsKey(item.KeyValue)) + { + // Create a wrapper with default value for items without explicit metadata + var wrapper = new TreeViewItem(item, default(T)); + lookupTable[item.KeyValue] = wrapper; + } + + BuildLookupTableFromTreeViewItems(item.ChildItems); + } + } + } +} diff --git a/InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs b/InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs new file mode 100644 index 0000000..8b1a2dc --- /dev/null +++ b/InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs @@ -0,0 +1,105 @@ +namespace Skyline.DataMiner.Utils.InteractiveAutomationScript +{ + using System.Collections.Generic; + + using Skyline.DataMiner.Net.AutomationUI.Objects; + + /// + /// Defines a tree view widget with basic operations. + /// + public interface ITreeView : ITreeViewBase + { + /// + /// Gets or sets the top-level items in the tree view. + /// The TreeViewItem.ChildItems property can be used to navigate further down the tree. + /// + IEnumerable Items { get; set; } + + /// + /// Gets all items in the tree view that are selected. + /// + IEnumerable CheckedItems { get; } + + /// + /// Gets all leaves (= items without children) in the tree view that are selected. + /// + IEnumerable CheckedLeaves { get; } + + /// + /// Gets all nodes (= items with children) in the tree view that are selected. + /// + IEnumerable CheckedNodes { get; } + + /// + /// Iterates over all items in the tree and returns them in a flat collection. + /// + /// A flat collection containing all items in the tree view. + IEnumerable GetAllItems(); + + /// + /// Returns all items in the tree view that are located at the provided depth. + /// Whenever the requested depth is greater than the longest branch in the tree, an empty collection will be returned. + /// + /// Depth of the requested items. + /// All items in the tree view that are located at the provided depth. + IEnumerable GetItems(int depth); + + /// + /// Can be used to retrieve an item from the tree view based on its key value. + /// + /// Key used to search for the item. + /// Item in the tree that matches the provided key. + /// True if the item was found, otherwise false. + bool TryFindTreeViewItem(string key, out TreeViewItem item); + } + + /// + /// Defines a generic tree view widget with support for typed treeview items. + /// + /// The type of the value associated with each treeview item. + public interface ITreeView : ITreeViewBase + { + /// + /// Gets or sets the top-level items in the tree view. + /// The TreeViewItem.ChildItems property can be used to navigate further down the tree. + /// + IEnumerable> Items { get; set; } + + /// + /// Gets all items in the tree view that are selected. + /// + IEnumerable> CheckedItems { get; } + + /// + /// Gets all leaves (= items without children) in the tree view that are selected. + /// + IEnumerable> CheckedLeaves { get; } + + /// + /// Gets all nodes (= items with children) in the tree view that are selected. + /// + IEnumerable> CheckedNodes { get; } + + /// + /// Iterates over all items in the tree and returns them in a flat collection. + /// + /// A flat collection containing all items in the tree view. + IEnumerable> GetAllItems(); + + /// + /// Returns all items in the tree view that are located at the provided depth. + /// Whenever the requested depth is greater than the longest branch in the tree, an empty collection will be returned. + /// + /// Depth of the requested items. + /// All items in the tree view that are located at the provided depth. + IEnumerable> GetItems(int depth); + + /// + /// Can be used to retrieve an item from the tree view based on its key value. + /// + /// Key used to search for the item. + /// Item in the tree that matches the provided key. + /// True if the item was found, otherwise false. + bool TryFindTreeViewItem(string key, out TreeViewItem item); + } +} \ No newline at end of file diff --git a/InteractiveAutomationToolkit/Components/Interfaces/ITreeViewBase.cs b/InteractiveAutomationToolkit/Components/Interfaces/ITreeViewBase.cs new file mode 100644 index 0000000..72dc57e --- /dev/null +++ b/InteractiveAutomationToolkit/Components/Interfaces/ITreeViewBase.cs @@ -0,0 +1,30 @@ +namespace Skyline.DataMiner.Utils.InteractiveAutomationScript +{ + /// + /// Defines the base functionality for a treeview widget. + /// + public interface ITreeViewBase : IIsReadonlyWidget + { + /// + /// Gets or sets the tooltip text associated with the treeview. + /// + string Tooltip { get; set; } + + /// + /// Sets the IsCollapsed state for all items in the tree view to false, causing the entire tree view to be expanded. + /// + void Expand(); + + /// + /// Sets the IsCollapsed state for all items in the tree view to true, causing the entire tree view to be collapsed. + /// + void Collapse(); + + /// + /// This method is used to update the cached TreeViewItems and lookup table. + /// This is done after loading the results from the UI Block, after handling the Events or when setting the Items. + /// This method should only be called from outside the TreeView if you are checking or collapsing items from outside of the TreeView and need to access the CheckedItems or CollapsedItems. + /// + void UpdateItemCache(); + } +} \ No newline at end of file diff --git a/InteractiveAutomationToolkit/Components/TreeView.cs b/InteractiveAutomationToolkit/Components/TreeView.cs index c9622ad..1eef587 100644 --- a/InteractiveAutomationToolkit/Components/TreeView.cs +++ b/InteractiveAutomationToolkit/Components/TreeView.cs @@ -10,7 +10,7 @@ /// /// A tree view structure. /// - public class TreeView : InteractiveWidget, IIsReadonlyWidget + public class TreeView : TreeViewBase, ITreeView { private Dictionary checkedItemCache; private Dictionary collapsedItemCache; // TODO: should only contain Items with LazyLoading set to true @@ -160,10 +160,7 @@ public event EventHandler> Collapsed private event EventHandler> OnCollapsed; - /// - /// Gets or sets the top-level items in the tree view. - /// The TreeViewItem.ChildItems property can be used to navigate further down the tree. - /// + /// public IEnumerable Items { get @@ -183,9 +180,7 @@ public IEnumerable Items } } - /// - /// Gets all items in the tree view that are selected. - /// + /// public IEnumerable CheckedItems { get @@ -194,9 +189,7 @@ public IEnumerable CheckedItems } } - /// - /// Gets all leaves (= items without children) in the tree view that are selected. - /// + /// public IEnumerable CheckedLeaves { get @@ -205,9 +198,7 @@ public IEnumerable CheckedLeaves } } - /// - /// Gets all nodes (= items with children) in the tree view that are selected. - /// + /// public IEnumerable CheckedNodes { get @@ -216,48 +207,8 @@ public IEnumerable CheckedNodes } } - /// - /// Gets or sets the tooltip. - /// - /// When the value is null. - public string Tooltip - { - get - { - return BlockDefinition.TooltipText; - } - - set - { - if (value == null) - { - throw new ArgumentNullException("value"); - } - - BlockDefinition.TooltipText = value; - } - } - - /// /// - /// - public virtual bool IsReadOnly - { - get - { - return BlockDefinition.IsReadOnly; - } - - set - { - BlockDefinition.IsReadOnly = value; - } - } - - /// - /// Sets the IsCollapsed state for all items in the tree view to true, causing the entire tree view to be collapsed. - /// - public void Collapse() + public override void Collapse() { foreach (var item in GetAllItems()) { @@ -265,10 +216,8 @@ public void Collapse() } } - /// - /// Sets the IsCollapsed state for all items in the tree view to false, causing the entire tree view to be expanded. - /// - public void Expand() + /// + public override void Expand() { foreach (var item in GetAllItems()) { @@ -276,24 +225,15 @@ public void Expand() } } - /// - /// Can be used to retrieve an item from the tree view based on its key value. - /// - /// Key used to search for the item. - /// Item in the tree that matches the provided key. - /// True if the item was found, otherwise false. + /// public bool TryFindTreeViewItem(string key, out TreeViewItem item) { item = GetAllItems().FirstOrDefault(x => x.KeyValue.Equals(key)); return item != null; } - /// - /// This method is used to update the cached TreeViewItems and lookup table. - /// This is done after loading the results from the UI Block, after handling the Events or when setting the Items. - /// This method should only be called from outside the TreeView if you are checking or collapsing items from outside of the TreeView and need to access the CheckedItems or CollapsedItems. - /// - public void UpdateItemCache() + /// + public override void UpdateItemCache() { checkedItemCache = new Dictionary(); collapsedItemCache = new Dictionary(); @@ -318,10 +258,7 @@ public void UpdateItemCache() } } - /// - /// Iterates over all items in the tree and returns them in a flat collection. - /// - /// A flat collection containing all items in the tree view. + /// public IEnumerable GetAllItems() { List allItems = new List(); @@ -334,12 +271,7 @@ public IEnumerable GetAllItems() return allItems; } - /// - /// Returns all items in the tree view that are located at the provided depth. - /// Whenever the requested depth is greater than the longest branch in the tree, an empty collection will be returned. - /// - /// Depth of the requested items. - /// All items in the tree view that are located at the provided depth. + /// public IEnumerable GetItems(int depth) { return GetItems(Items, depth, 0); diff --git a/InteractiveAutomationToolkit/Components/TreeViewBase.cs b/InteractiveAutomationToolkit/Components/TreeViewBase.cs new file mode 100644 index 0000000..09e5e51 --- /dev/null +++ b/InteractiveAutomationToolkit/Components/TreeViewBase.cs @@ -0,0 +1,58 @@ +namespace Skyline.DataMiner.Utils.InteractiveAutomationScript +{ + using System; + + using Skyline.DataMiner.Automation; + + public abstract class TreeViewBase : InteractiveWidget, ITreeViewBase + { + protected TreeViewBase() + { + Type = UIBlockType.TreeView; + IsReadOnly = false; + } + + /// + /// When the value is null. + public string Tooltip + { + get + { + return BlockDefinition.TooltipText; + } + + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + BlockDefinition.TooltipText = value; + } + } + + /// + public virtual bool IsReadOnly + { + get + { + return BlockDefinition.IsReadOnly; + } + + set + { + BlockDefinition.IsReadOnly = value; + } + } + + /// + public abstract void Collapse(); + + /// + public abstract void Expand(); + + /// + public abstract void UpdateItemCache(); + } +} diff --git a/InteractiveAutomationToolkit/Components/TreeViewItem.cs b/InteractiveAutomationToolkit/Components/TreeViewItem.cs new file mode 100644 index 0000000..923585f --- /dev/null +++ b/InteractiveAutomationToolkit/Components/TreeViewItem.cs @@ -0,0 +1,199 @@ +namespace Skyline.DataMiner.Utils.InteractiveAutomationScript +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using Skyline.DataMiner.Net.AutomationUI.Objects; + + /// + /// Represents a TreeViewItem with an associated value of type . + /// This allows attaching custom metadata to tree view items. + /// + /// The type of the value associated with this tree view item. + public sealed class TreeViewItem : IEquatable> + { + /// + /// Initializes a new instance of the class. + /// + /// The underlying TreeViewItem. + /// The value to associate with this tree view item. + /// When item is null. + public TreeViewItem(TreeViewItem item, T value) + { + Item = item ?? throw new ArgumentNullException(nameof(item)); + Value = value; + } + + /// + /// Initializes a new instance of the class. + /// Creates a TreeViewItem with the specified parameters. + /// + /// The unique key for the tree view item. + /// The text to display for the tree view item. + /// The value to associate with this tree view item. + /// The type of the tree view item. + /// The child items of this tree view item. + /// When keyValue or displayValue is null. + public TreeViewItem( + string keyValue, + string displayValue, + T value, + TreeViewItem.TreeViewItemType itemType = TreeViewItem.TreeViewItemType.CheckBox, + IEnumerable> childItems = null) + { + if (keyValue == null) + { + throw new ArgumentNullException(nameof(keyValue)); + } + + if (displayValue == null) + { + throw new ArgumentNullException(nameof(displayValue)); + } + + var children = childItems?.Select(x => x.Item).ToList() ?? new List(); + Item = new TreeViewItem(displayValue, keyValue, children) + { + ItemType = itemType + }; + Value = value; + } + + /// + /// Gets the underlying TreeViewItem. + /// + public TreeViewItem Item { get; } + + /// + /// Gets the value associated with this tree view item. + /// + public T Value { get; } + + /// + /// Gets or sets the unique key for the tree view item. + /// + public string KeyValue + { + get => Item.KeyValue; + set => Item.KeyValue = value; + } + + /// + /// Gets or sets the text to display for the tree view item. + /// + public string DisplayValue + { + get => Item.DisplayValue; + set => Item.DisplayValue = value; + } + + /// + /// Gets or sets the type of the tree view item. + /// + public TreeViewItem.TreeViewItemType ItemType + { + get => Item.ItemType; + set => Item.ItemType = value; + } + + /// + /// Gets or sets a value indicating whether the tree view item is checked. + /// + public bool IsChecked + { + get => Item.IsChecked; + set => Item.IsChecked = value; + } + + /// + /// Gets or sets a value indicating whether the tree view item is collapsed. + /// + public bool IsCollapsed + { + get => Item.IsCollapsed; + set => Item.IsCollapsed = value; + } + + /// + /// Gets or sets a value indicating whether the tree view item supports lazy loading. + /// + public bool SupportsLazyLoading + { + get => Item.SupportsLazyLoading; + set => Item.SupportsLazyLoading = value; + } + + /// + /// Gets the child items of this tree view item. + /// + public IEnumerable ChildItems => Item.ChildItems; + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public override bool Equals(object obj) + { + return obj is TreeViewItem option && Equals(option); + } + + /// + /// Determines whether the specified TreeViewItemOption is equal to the current TreeViewItemOption. + /// + /// The TreeViewItemOption to compare with the current TreeViewItemOption. + /// true if the specified TreeViewItemOption is equal to the current TreeViewItemOption; otherwise, false. + public bool Equals(TreeViewItem other) + { + if (ReferenceEquals(this, other)) return true; + if (ReferenceEquals(null, other)) return false; + + return Item.KeyValue.Equals(other.Item.KeyValue) && + EqualityComparer.Default.Equals(Value, other.Value); + } + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + public override int GetHashCode() + { + int hashCode = 11; + hashCode ^= 13 * Item.KeyValue.GetHashCode(); + hashCode ^= 13 * (Value != null ? Value.GetHashCode() : 0); + return hashCode; + } + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + public override string ToString() + { + return $"{DisplayValue} ({KeyValue}) => {Value}"; + } + + /// + /// Determines whether two TreeViewItemOption objects are equal. + /// + /// The first object to compare. + /// The second object to compare. + /// true if the objects are equal; otherwise, false. + public static bool operator ==(TreeViewItem left, TreeViewItem right) + { + return Equals(left, right); + } + + /// + /// Determines whether two TreeViewItemOption objects are not equal. + /// + /// The first object to compare. + /// The second object to compare. + /// true if the objects are not equal; otherwise, false. + public static bool operator !=(TreeViewItem left, TreeViewItem right) + { + return !Equals(left, right); + } + } +} diff --git a/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs b/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs new file mode 100644 index 0000000..463e28f --- /dev/null +++ b/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs @@ -0,0 +1,274 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Skyline.DataMiner.Utils.InteractiveAutomationScript; +using System.Linq; + +namespace InteractiveAutomationToolkitTests +{ + [TestClass] + public class GenericTreeViewTests + { + [TestMethod] + public void Constructor_Test() + { + var rootItems = new[] + { + new TreeViewItem("root1", "Root Item 1", 1), + new TreeViewItem("root2", "Root Item 2", 2) + }; + + var treeView = new TreeView(rootItems); + + Assert.IsNotNull(treeView); + Assert.AreEqual(2, treeView.Items.Count()); + Assert.IsFalse(treeView.IsReadOnly); + } + + [TestMethod] + public void Constructor_WithChildItems_Test() + { + var childItems = new[] + { + new TreeViewItem("child1", "Child Item 1", "child1data"), + new TreeViewItem("child2", "Child Item 2", "child2data") + }; + + var rootItem = new TreeViewItem("root", "Root Item", "rootdata", childItems: childItems); + var treeView = new TreeView(new[] { rootItem }); + + Assert.IsNotNull(treeView); + Assert.AreEqual(1, treeView.Items.Count()); + Assert.AreEqual(3, treeView.GetAllItems().Count()); // 1 root + 2 children + } + + [TestMethod] + public void TryFindTreeViewItem_Test() + { + var rootItems = new[] + { + new TreeViewItem("root1", "Root Item 1", 100), + new TreeViewItem("root2", "Root Item 2", 200) + }; + + var treeView = new TreeView(rootItems); + treeView.UpdateItemCache(); + + bool found = treeView.TryFindTreeViewItem("root1", out var item); + + Assert.IsTrue(found); + Assert.IsNotNull(item); + Assert.AreEqual("root1", item.KeyValue); + Assert.AreEqual("Root Item 1", item.DisplayValue); + Assert.AreEqual(100, item.Value); + } + + [TestMethod] + public void TryFindTreeViewItem_NotFound_Test() + { + var rootItems = new[] + { + new TreeViewItem("root1", "Root Item 1", 100) + }; + + var treeView = new TreeView(rootItems); + treeView.UpdateItemCache(); + + bool found = treeView.TryFindTreeViewItem("nonexistent", out var item); + + Assert.IsFalse(found); + Assert.IsNull(item); + } + + [TestMethod] + public void GetAllItems_Test() + { + var child1 = new TreeViewItem("child1", "Child 1", 10); + var child2 = new TreeViewItem("child2", "Child 2", 20); + var rootItem = new TreeViewItem("root", "Root", 1, childItems: new[] { child1, child2 }); + + var treeView = new TreeView(new[] { rootItem }); + treeView.UpdateItemCache(); + + var allItems = treeView.GetAllItems().ToList(); + + Assert.AreEqual(3, allItems.Count); + Assert.IsTrue(allItems.Any(x => x.KeyValue == "root")); + Assert.IsTrue(allItems.Any(x => x.KeyValue == "child1")); + Assert.IsTrue(allItems.Any(x => x.KeyValue == "child2")); + } + + [TestMethod] + public void GetItems_Depth0_Test() + { + var child = new TreeViewItem("child", "Child", 10); + var root = new TreeViewItem("root", "Root", 1, childItems: new[] { child }); + + var treeView = new TreeView(new[] { root }); + treeView.UpdateItemCache(); + + var depth0Items = treeView.GetItems(0).ToList(); + + Assert.AreEqual(1, depth0Items.Count); + Assert.AreEqual("root", depth0Items[0].KeyValue); + } + + [TestMethod] + public void GetItems_Depth1_Test() + { + var child = new TreeViewItem("child", "Child", 10); + var root = new TreeViewItem("root", "Root", 1, childItems: new[] { child }); + + var treeView = new TreeView(new[] { root }); + treeView.UpdateItemCache(); + + var depth1Items = treeView.GetItems(1).ToList(); + + Assert.AreEqual(1, depth1Items.Count); + Assert.AreEqual("child", depth1Items[0].KeyValue); + } + + [TestMethod] + public void CheckedValues_Test() + { + var item1 = new TreeViewItem("item1", "Item 1", 100); + var item2 = new TreeViewItem("item2", "Item 2", 200); + var item3 = new TreeViewItem("item3", "Item 3", 300); + + var treeView = new TreeView(new[] { item1, item2, item3 }); + treeView.UpdateItemCache(); + + // Simulate checking items + item1.IsChecked = true; + item3.IsChecked = true; + treeView.UpdateItemCache(); + + var checkedValues = treeView.CheckedValues.ToList(); + + Assert.AreEqual(2, checkedValues.Count); + Assert.IsTrue(checkedValues.Contains(100)); + Assert.IsTrue(checkedValues.Contains(300)); + Assert.IsFalse(checkedValues.Contains(200)); + } + + [TestMethod] + public void CollapseAndExpand_Test() + { + var child = new TreeViewItem("child", "Child", 10); + var root = new TreeViewItem("root", "Root", 1, childItems: new[] { child }); + + var treeView = new TreeView(new[] { root }); + treeView.UpdateItemCache(); + + treeView.Collapse(); + + Assert.IsTrue(root.IsCollapsed); + Assert.IsTrue(child.IsCollapsed); + + treeView.Expand(); + + Assert.IsFalse(root.IsCollapsed); + Assert.IsFalse(child.IsCollapsed); + } + + [TestMethod] + public void TreeViewItemOption_Equals_Test() + { + var item1 = new TreeViewItem("key1", "Display 1", 100); + var item2 = new TreeViewItem("key1", "Display 1", 100); + var item3 = new TreeViewItem("key2", "Display 2", 200); + + Assert.AreEqual(item1, item2); + Assert.AreNotEqual(item1, item3); + } + + [TestMethod] + public void TreeViewItemOption_Properties_Test() + { + var item = new TreeViewItem("key", "display", "value"); + + Assert.AreEqual("key", item.KeyValue); + Assert.AreEqual("display", item.DisplayValue); + Assert.AreEqual("value", item.Value); + + item.KeyValue = "newKey"; + item.DisplayValue = "newDisplay"; + + Assert.AreEqual("newKey", item.KeyValue); + Assert.AreEqual("newDisplay", item.DisplayValue); + Assert.AreEqual("value", item.Value); // Value is read-only + } + + [TestMethod] + public void CheckedLeaves_And_CheckedNodes_Test() + { + var leaf1 = new TreeViewItem("leaf1", "Leaf 1", 10); + var leaf2 = new TreeViewItem("leaf2", "Leaf 2", 20); + var node = new TreeViewItem("node", "Node", 100, childItems: new[] { leaf1, leaf2 }); + + var treeView = new TreeView(new[] { node }); + treeView.UpdateItemCache(); + + // Check both node and one leaf + node.IsChecked = true; + leaf1.IsChecked = true; + treeView.UpdateItemCache(); + + var checkedNodes = treeView.CheckedNodes.ToList(); + var checkedLeaves = treeView.CheckedLeaves.ToList(); + var checkedNodeValues = treeView.CheckedNodeValues.ToList(); + var checkedLeafValues = treeView.CheckedLeafValues.ToList(); + + Assert.AreEqual(1, checkedNodes.Count); + Assert.AreEqual("node", checkedNodes[0].KeyValue); + Assert.AreEqual(1, checkedNodeValues.Count); + Assert.AreEqual(100, checkedNodeValues[0]); + + Assert.AreEqual(1, checkedLeaves.Count); + Assert.AreEqual("leaf1", checkedLeaves[0].KeyValue); + Assert.AreEqual(1, checkedLeafValues.Count); + Assert.AreEqual(10, checkedLeafValues[0]); + } + + [TestMethod] + public void EmptyConstructor_Test() + { + var treeView = new TreeView(System.Linq.Enumerable.Empty>()); + + Assert.IsNotNull(treeView); + Assert.AreEqual(0, treeView.Items.Count()); + Assert.AreEqual(0, treeView.GetAllItems().Count()); + } + + [TestMethod] + public void ParameterlessConstructor_Test() + { + var treeView = new TreeView(); + + Assert.IsNotNull(treeView); + Assert.AreEqual(0, treeView.Items.Count()); + Assert.AreEqual(0, treeView.GetAllItems().Count()); + Assert.IsFalse(treeView.IsReadOnly); + } + + [TestMethod] + public void NullValue_Test() + { + // Test that null values are handled correctly + var item = new TreeViewItem("key", "display", null); + + Assert.IsNull(item.Value); + Assert.AreEqual("key", item.KeyValue); + Assert.AreEqual("display", item.DisplayValue); + } + + [TestMethod] + public void ComplexType_Test() + { + // Test with a complex custom type + var customData = new { Id = 123, Name = "Test", Active = true }; + var item = new TreeViewItem("key", "display", customData); + + Assert.IsNotNull(item.Value); + Assert.AreEqual(customData, item.Value); + } + } +} From 4705f3b7c255538708ffd1800b8afc8982042cdd Mon Sep 17 00:00:00 2001 From: Tom Waterbley Date: Thu, 23 Oct 2025 12:45:02 +0200 Subject: [PATCH 4/7] Update --- .../Components/Generics/GenericTreeView.cs | 272 +++++------------- .../Components/Interfaces/ITreeView.cs | 15 + .../Components/TreeView.cs | 65 ++--- .../Components/TreeViewItem.cs | 61 ++-- .../GenericTreeViewTests.cs | 18 +- 5 files changed, 158 insertions(+), 273 deletions(-) diff --git a/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs b/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs index 87bcd9f..de460d8 100644 --- a/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs +++ b/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs @@ -8,11 +8,13 @@ namespace Skyline.DataMiner.Utils.InteractiveAutomationScript using Skyline.DataMiner.Net.AutomationUI.Objects; /// - /// A generic tree view structure that allows attaching custom metadata to each item. + /// A generic tree view structure that allows attaching custom metadata to each item. /// /// The type of the value associated with each tree view item. public class TreeView : TreeViewBase, ITreeView { + private readonly List> rootItems = new List>(); + private Dictionary checkedItemCache; private Dictionary collapsedItemCache; private Dictionary> lookupTable; @@ -33,21 +35,19 @@ public class TreeView : TreeViewBase, ITreeView private List> collapsedItems = new List>(); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public TreeView() : this(Enumerable.Empty>()) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Root nodes of the tree view. public TreeView(IEnumerable> treeViewItems) { - Type = UIBlockType.TreeView; Items = treeViewItems; - IsReadOnly = false; } /// @@ -73,8 +73,8 @@ public event EventHandler>> Changed } /// - /// Triggered whenever an item is selected. - /// WantsOnChange will be set to true when this event is subscribed to. + /// Triggered whenever an item is selected. + /// WantsOnChange will be set to true when this event is subscribed to. /// public event EventHandler>> Checked { @@ -95,8 +95,8 @@ public event EventHandler>> Checked } /// - /// Triggered whenever an item is no longer selected. - /// WantsOnChange will be set to true when this event is subscribed to. + /// Triggered whenever an item is no longer selected. + /// WantsOnChange will be set to true when this event is subscribed to. /// public event EventHandler>> Unchecked { @@ -117,9 +117,9 @@ public event EventHandler>> Unchecked } /// - /// Triggered whenever an item is expanded. - /// Can be used for lazy loading. - /// Will be triggered whenever a node with SupportsLazyLoading set to true is expanded. + /// Triggered whenever an item is expanded. + /// Can be used for lazy loading. + /// Will be triggered whenever a node with SupportsLazyLoading set to true is expanded. /// public event EventHandler>> Expanded { @@ -135,8 +135,8 @@ public event EventHandler>> Expanded } /// - /// Triggered whenever an item is collapsed. - /// Will be triggered whenever a node with SupportsLazyLoading set to true is collapsed. + /// Triggered whenever an item is collapsed. + /// Will be triggered whenever a node with SupportsLazyLoading set to true is collapsed. /// public event EventHandler>> Collapsed { @@ -161,17 +161,12 @@ public event EventHandler>> Collapsed private event EventHandler>> OnCollapsed; - /// - /// Gets or sets the top-level items in the tree view. - /// The TreeViewItemOption.Item.ChildItems property can be used to navigate further down the tree. - /// + /// public IEnumerable> Items { get { - // Return the TreeViewItemOption wrappers based on the underlying TreeViewItems - return BlockDefinition.TreeViewItems.Select(item => lookupTable.Values.FirstOrDefault(x => x.Item == item)) - .Where(x => x != null); + return rootItems; } set @@ -181,14 +176,16 @@ public IEnumerable> Items throw new ArgumentNullException(nameof(value)); } + rootItems.Clear(); + rootItems.AddRange(value); + BlockDefinition.TreeViewItems = new List(value.Select(x => x.Item)); - UpdateItemCache(value); + + UpdateItemCache(); } } - /// - /// Gets all items in the tree view that are selected. - /// + /// public IEnumerable> CheckedItems { get @@ -197,9 +194,7 @@ public IEnumerable> CheckedItems } } - /// - /// Gets all leaves (= items without children) in the tree view that are selected. - /// + /// public IEnumerable> CheckedLeaves { get @@ -208,9 +203,7 @@ public IEnumerable> CheckedLeaves } } - /// - /// Gets all nodes (= items with children) in the tree view that are selected. - /// + /// public IEnumerable> CheckedNodes { get @@ -219,9 +212,7 @@ public IEnumerable> CheckedNodes } } - /// - /// Gets the values of all items in the tree view that are selected. - /// + /// public IEnumerable CheckedValues { get @@ -230,9 +221,7 @@ public IEnumerable CheckedValues } } - /// - /// Gets the values of all leaves in the tree view that are selected. - /// + /// public IEnumerable CheckedLeafValues { get @@ -241,9 +230,7 @@ public IEnumerable CheckedLeafValues } } - /// - /// Gets the values of all nodes in the tree view that are selected. - /// + /// public IEnumerable CheckedNodeValues { get @@ -270,88 +257,55 @@ public override void Expand() } } - /// - /// Can be used to retrieve an item from the tree view based on its key value. - /// - /// Key used to search for the item. - /// Item in the tree that matches the provided key. - /// True if the item was found, otherwise false. + /// public bool TryFindTreeViewItem(string key, out TreeViewItem item) { return lookupTable.TryGetValue(key, out item); } - /// - /// This method is used to update the cached TreeViewItems and lookup table. - /// This is done after loading the results from the UI Block, after handling the Events or when setting the Items. - /// This method should only be called from outside the TreeView if you are checking or collapsing items from outside of the TreeView and need to access the CheckedItems or CollapsedItems. - /// + /// public override void UpdateItemCache() { - // Don't pass Items here as it would cause recursion - just update the existing cache - var newCheckedItemCache = new Dictionary(); - var newCollapsedItemCache = new Dictionary(); - - // Ensure lookupTable exists - if (lookupTable == null) - { - lookupTable = new Dictionary>(); - } + checkedItemCache = new Dictionary(); + collapsedItemCache = new Dictionary(); + lookupTable = new Dictionary>(); - // Update caches based on current lookup table - foreach (var item in lookupTable.Values) + foreach (var item in GetAllItems(rootItems)) { try { - newCheckedItemCache.Add(item.KeyValue, item.IsChecked); + checkedItemCache.Add(item.KeyValue, item.IsChecked); if (item.SupportsLazyLoading) { - newCollapsedItemCache.Add(item.KeyValue, item.IsCollapsed); + collapsedItemCache.Add(item.KeyValue, item.IsCollapsed); } + + lookupTable.Add(item.KeyValue, item); } catch (Exception e) { throw new TreeViewDuplicateItemsException(item.KeyValue, e); } } - - // Replace caches atomically - checkedItemCache = newCheckedItemCache; - collapsedItemCache = newCollapsedItemCache; } - /// - /// Iterates over all items in the tree and returns them in a flat collection. - /// - /// A flat collection containing all items in the tree view. + /// public IEnumerable> GetAllItems() { return lookupTable.Values; } - /// - /// Returns all items in the tree view that are located at the provided depth. - /// Whenever the requested depth is greater than the longest branch in the tree, an empty collection will be returned. - /// - /// Depth of the requested items. - /// All items in the tree view that are located at the provided depth. + /// public IEnumerable> GetItems(int depth) { return GetItems(Items, depth, 0); } - /// - /// Load any changes made through user interaction. - /// - /// - /// Represents the information a user has entered or selected in a dialog box of an interactive - /// Automation script. - /// - /// should be used as key to get the changes for this widget. + /// protected internal override void LoadResult(IUIResults uiResults) { - var checkedItemKeys = uiResults.GetCheckedItemKeys(this); - var expandedItemKeys = uiResults.GetExpandedItemKeys(this); + var checkedItemKeys = uiResults.GetCheckedItemKeys(this); // this includes all checked items + var expandedItemKeys = uiResults.GetExpandedItemKeys(this); // this includes all expanded items with LazyLoading set to true // Check for changes // Expanded Items @@ -377,10 +331,7 @@ protected internal override void LoadResult(IUIResults uiResults) foreach (string changedItemKey in changedItemKeys) { - if (lookupTable.TryGetValue(changedItemKey, out var item)) - { - changedItems.Add(item); - } + changedItems.Add(lookupTable[changedItemKey]); } } @@ -394,11 +345,7 @@ protected internal override void LoadResult(IUIResults uiResults) UpdateItemCache(); } - /// - /// Raises zero or more events of the widget. - /// This method is called after was called on all widgets. - /// - /// It is up to the implementer to determine if an event must be raised. + /// protected internal override void RaiseResultEvents() { // Expanded items @@ -441,7 +388,7 @@ protected internal override void RaiseResultEvents() } /// - /// Returns all items in the TreeView that are checked. + /// Returns all items in the TreeView that are checked. /// /// All checked TreeViewItems in the TreeView. private IEnumerable> GetCheckedItems() @@ -450,31 +397,31 @@ private IEnumerable> GetCheckedItems() } /// - /// This method is used to recursively go through all the items in the TreeView. + /// This method is used to recursively go through all the items in the TreeView. /// /// List of TreeViewItems to be visited. - /// Parent TreeViewItemOption wrappers. /// Flat collection containing every item in the provided children collection and all underlying items. - private IEnumerable> GetAllItemsRecursive(IEnumerable children, IEnumerable> parentOptions) + private IEnumerable> GetAllItems(IEnumerable> children) { - List> allItems = new List>(); - foreach (var item in children) + if (children == null) + yield break; + + var queue = new Queue>(children); + + while (queue.Count > 0) { - var option = parentOptions.FirstOrDefault(x => x.Item == item); - if (option != null) + var item = queue.Dequeue(); + yield return item; + + foreach (var child in item.ChildItems) { - allItems.Add(option); - // For child items, we need to search in the lookup table - var childOptions = option.ChildItems.Select(child => lookupTable.Values.FirstOrDefault(x => x.Item == child)).Where(x => x != null); - allItems.AddRange(GetAllItemsRecursive(option.ChildItems, childOptions)); + queue.Enqueue(child); } } - - return allItems; } /// - /// Returns all TreeViewItems in the TreeView that are located on the provided depth. + /// Returns all TreeViewItems in the TreeView that are located on the provided depth. /// /// Items to be checked. /// Depth that was requested. @@ -493,8 +440,7 @@ private IEnumerable> GetItems(IEnumerable> child else { int newDepth = currentDepth + 1; - var childOptions = item.ChildItems.Select(child => lookupTable.Values.FirstOrDefault(x => x.Item == child)).Where(x => x != null); - requestedItems.AddRange(GetItems(childOptions, requestedDepth, newDepth)); + requestedItems.AddRange(GetItems(item.ChildItems, requestedDepth, newDepth)); } } @@ -511,10 +457,7 @@ private void RegisterExpandedItems(IEnumerable expandedItemKeys) foreach (string newlyExpandedItemKey in newlyExpandedItems) { - if (lookupTable.TryGetValue(newlyExpandedItemKey, out var item)) - { - expandedItems.Add(item); - } + expandedItems.Add(lookupTable[newlyExpandedItemKey]); } } } @@ -529,10 +472,7 @@ private void RegisterCollapsedItems(IEnumerable expandedItemKeys) foreach (string newlyCollapsedItemKey in newlyCollapsedItems) { - if (lookupTable.TryGetValue(newlyCollapsedItemKey, out var item)) - { - collapsedItems.Add(item); - } + collapsedItems.Add(lookupTable[newlyCollapsedItemKey]); } } } @@ -547,10 +487,7 @@ private List RegisterCheckedItems(IEnumerable checkedItemKeys) foreach (string newlyCheckedItemKey in newlyCheckedItemKeys) { - if (lookupTable.TryGetValue(newlyCheckedItemKey, out var item)) - { - checkedItems.Add(item); - } + checkedItems.Add(lookupTable[newlyCheckedItemKey]); } } @@ -567,92 +504,11 @@ private List RegisterUncheckedItems(IEnumerable checkedItemKeys) foreach (string newlyUncheckedItemKey in newlyUncheckedItemKeys) { - if (lookupTable.TryGetValue(newlyUncheckedItemKey, out var item)) - { - uncheckedItems.Add(item); - } + uncheckedItems.Add(lookupTable[newlyUncheckedItemKey]); } } return newlyUncheckedItemKeys; } - - private void UpdateItemCache(IEnumerable> items) - { - // Initialize new caches but preserve the existing lookup table if items is null or empty - var newCheckedItemCache = new Dictionary(); - var newCollapsedItemCache = new Dictionary(); - - // Only rebuild lookup table if we have new items to process - if (items != null && items.Any()) - { - lookupTable = new Dictionary>(); - BuildLookupTable(items); - } - else if (lookupTable == null) - { - // Initialize empty lookup table only if it doesn't exist - lookupTable = new Dictionary>(); - } - - // Update caches based on current lookup table - foreach (var item in lookupTable.Values) - { - try - { - newCheckedItemCache.Add(item.KeyValue, item.IsChecked); - if (item.SupportsLazyLoading) - { - newCollapsedItemCache.Add(item.KeyValue, item.IsCollapsed); - } - } - catch (Exception e) - { - throw new TreeViewDuplicateItemsException(item.KeyValue, e); - } - } - - // Replace caches atomically - checkedItemCache = newCheckedItemCache; - collapsedItemCache = newCollapsedItemCache; - } - - private void BuildLookupTable(IEnumerable> items) - { - foreach (var item in items) - { - lookupTable[item.KeyValue] = item; - - // Recursively process children - var childOptions = item.ChildItems.Select(child => - { - // Try to find existing wrapper or create a temporary one - var existing = lookupTable.Values.FirstOrDefault(x => x.Item == child); - return existing; - }).Where(x => x != null); - - if (item.ChildItems.Any()) - { - // For children that don't have wrappers yet, we need to handle them - // This is a limitation - child items added directly to TreeViewItem won't have metadata - BuildLookupTableFromTreeViewItems(item.ChildItems); - } - } - } - - private void BuildLookupTableFromTreeViewItems(IEnumerable items) - { - foreach (var item in items) - { - if (!lookupTable.ContainsKey(item.KeyValue)) - { - // Create a wrapper with default value for items without explicit metadata - var wrapper = new TreeViewItem(item, default(T)); - lookupTable[item.KeyValue] = wrapper; - } - - BuildLookupTableFromTreeViewItems(item.ChildItems); - } - } } } diff --git a/InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs b/InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs index 8b1a2dc..9457310 100644 --- a/InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs +++ b/InteractiveAutomationToolkit/Components/Interfaces/ITreeView.cs @@ -80,6 +80,21 @@ public interface ITreeView : ITreeViewBase /// IEnumerable> CheckedNodes { get; } + /// + /// Gets the values of all items in the tree view that are selected. + /// + IEnumerable CheckedValues { get; } + + /// + /// Gets the values of all leaves in the tree view that are selected. + /// + IEnumerable CheckedLeafValues { get; } + + /// + /// Gets the values of all nodes in the tree view that are selected. + /// + IEnumerable CheckedNodeValues { get; } + /// /// Iterates over all items in the tree and returns them in a flat collection. /// diff --git a/InteractiveAutomationToolkit/Components/TreeView.cs b/InteractiveAutomationToolkit/Components/TreeView.cs index 1eef587..e6e7898 100644 --- a/InteractiveAutomationToolkit/Components/TreeView.cs +++ b/InteractiveAutomationToolkit/Components/TreeView.cs @@ -12,6 +12,7 @@ /// public class TreeView : TreeViewBase, ITreeView { + private readonly List rootItems = new List(); private Dictionary checkedItemCache; private Dictionary collapsedItemCache; // TODO: should only contain Items with LazyLoading set to true private Dictionary lookupTable; @@ -44,9 +45,7 @@ public TreeView() : this(Enumerable.Empty()) /// Root nodes of the tree view. public TreeView(IEnumerable treeViewItems) { - Type = UIBlockType.TreeView; Items = treeViewItems; - IsReadOnly = false; } /// @@ -165,17 +164,21 @@ public IEnumerable Items { get { - return BlockDefinition.TreeViewItems; + return rootItems; } set { if (value == null) { - throw new ArgumentNullException("value"); + throw new ArgumentNullException(nameof(value)); } - BlockDefinition.TreeViewItems = new List(value); + rootItems.Clear(); + rootItems.AddRange(value); + + BlockDefinition.TreeViewItems = rootItems; + UpdateItemCache(); } } @@ -228,8 +231,7 @@ public override void Expand() /// public bool TryFindTreeViewItem(string key, out TreeViewItem item) { - item = GetAllItems().FirstOrDefault(x => x.KeyValue.Equals(key)); - return item != null; + return lookupTable.TryGetValue(key, out item); } /// @@ -239,7 +241,7 @@ public override void UpdateItemCache() collapsedItemCache = new Dictionary(); lookupTable = new Dictionary(); - foreach (var item in GetAllItems()) + foreach (var item in GetAllItems(rootItems)) { try { @@ -261,14 +263,7 @@ public override void UpdateItemCache() /// public IEnumerable GetAllItems() { - List allItems = new List(); - foreach (var item in Items) - { - allItems.Add(item); - allItems.AddRange(GetAllItems(item.ChildItems)); - } - - return allItems; + return lookupTable.Values; } /// @@ -277,14 +272,7 @@ public IEnumerable GetItems(int depth) return GetItems(Items, depth, 0); } - /// - /// Load any changes made through user interaction. - /// - /// - /// Represents the information a user has entered or selected in a dialog box of an interactive - /// Automation script. - /// - /// should be used as key to get the changes for this widget. + /// protected internal override void LoadResult(IUIResults uiResults) { var checkedItemKeys = uiResults.GetCheckedItemKeys(this); // this includes all checked items @@ -328,11 +316,7 @@ protected internal override void LoadResult(IUIResults uiResults) UpdateItemCache(); } - /// - /// Raises zero or more events of the widget. - /// This method is called after was called on all widgets. - /// - /// It is up to the implementer to determine if an event must be raised. + /// protected internal override void RaiseResultEvents() { // Expanded items @@ -390,14 +374,21 @@ private IEnumerable GetCheckedItems() /// Flat collection containing every item in the provided children collection and all underlying items. private IEnumerable GetAllItems(IEnumerable children) { - List allItems = new List(); - foreach (var item in children) + if (children == null) + yield break; + + var queue = new Queue(children); + + while (queue.Count > 0) { - allItems.Add(item); - allItems.AddRange(GetAllItems(item.ChildItems)); - } + var item = queue.Dequeue(); + yield return item; - return allItems; + foreach (var child in item.ChildItems) + { + queue.Enqueue(child); + } + } } /// @@ -450,9 +441,9 @@ private void RegisterCollapsedItems(IEnumerable expandedItemKeys) itemsCollapsed = true; collapsedItems = new List(); - foreach (string newyCollapsedItemKey in newlyCollapsedItems) + foreach (string newlyCollapsedItemKey in newlyCollapsedItems) { - collapsedItems.Add(lookupTable[newyCollapsedItemKey]); + collapsedItems.Add(lookupTable[newlyCollapsedItemKey]); } } } diff --git a/InteractiveAutomationToolkit/Components/TreeViewItem.cs b/InteractiveAutomationToolkit/Components/TreeViewItem.cs index 923585f..1f51603 100644 --- a/InteractiveAutomationToolkit/Components/TreeViewItem.cs +++ b/InteractiveAutomationToolkit/Components/TreeViewItem.cs @@ -7,39 +7,62 @@ namespace Skyline.DataMiner.Utils.InteractiveAutomationScript using Skyline.DataMiner.Net.AutomationUI.Objects; /// - /// Represents a TreeViewItem with an associated value of type . + /// Represents a with an associated value of type . /// This allows attaching custom metadata to tree view items. /// /// The type of the value associated with this tree view item. public sealed class TreeViewItem : IEquatable> { + private readonly List> _childItems; + /// /// Initializes a new instance of the class. /// - /// The underlying TreeViewItem. + /// The underlying . /// The value to associate with this tree view item. + /// The child items of this tree view item. /// When item is null. - public TreeViewItem(TreeViewItem item, T value) + public TreeViewItem( + TreeViewItem item, + T value, + IEnumerable> childItems = null) { Item = item ?? throw new ArgumentNullException(nameof(item)); Value = value; + + _childItems = childItems?.ToList() ?? new List>(); + + // Use hashset for fast lookup of already wrapped items + var existingChildren = _childItems.Select(x => x.Item).ToHashSet(); + + // Wrap any children from the underlying item that weren't included + foreach (var child in item.ChildItems) + { + if (!existingChildren.Contains(child)) + { + var wrappedChild = new TreeViewItem(child, default); + _childItems.Add(wrappedChild); + existingChildren.Add(child); + } + } + + // Sync underlying TreeViewItem + Item.ChildItems = _childItems.Select(x => x.Item).ToList(); } /// /// Initializes a new instance of the class. - /// Creates a TreeViewItem with the specified parameters. + /// Creates a with the specified parameters. /// /// The unique key for the tree view item. /// The text to display for the tree view item. /// The value to associate with this tree view item. - /// The type of the tree view item. /// The child items of this tree view item. /// When keyValue or displayValue is null. public TreeViewItem( string keyValue, string displayValue, T value, - TreeViewItem.TreeViewItemType itemType = TreeViewItem.TreeViewItemType.CheckBox, IEnumerable> childItems = null) { if (keyValue == null) @@ -52,16 +75,14 @@ public TreeViewItem( throw new ArgumentNullException(nameof(displayValue)); } - var children = childItems?.Select(x => x.Item).ToList() ?? new List(); - Item = new TreeViewItem(displayValue, keyValue, children) - { - ItemType = itemType - }; + _childItems = childItems?.ToList() ?? new List>(); + + Item = new TreeViewItem(displayValue, keyValue, _childItems.Select(x => x.Item).ToList()); Value = value; } /// - /// Gets the underlying TreeViewItem. + /// Gets the underlying . /// public TreeViewItem Item { get; } @@ -127,7 +148,7 @@ public bool SupportsLazyLoading /// /// Gets the child items of this tree view item. /// - public IEnumerable ChildItems => Item.ChildItems; + public IEnumerable> ChildItems => _childItems; /// /// Determines whether the specified object is equal to the current object. @@ -140,16 +161,16 @@ public override bool Equals(object obj) } /// - /// Determines whether the specified TreeViewItemOption is equal to the current TreeViewItemOption. + /// Determines whether the specified is equal to the current . /// - /// The TreeViewItemOption to compare with the current TreeViewItemOption. - /// true if the specified TreeViewItemOption is equal to the current TreeViewItemOption; otherwise, false. + /// The to compare with the current . + /// true if the specified is equal to the current ; otherwise, false. public bool Equals(TreeViewItem other) { if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(null, other)) return false; - return Item.KeyValue.Equals(other.Item.KeyValue) && + return EqualityComparer.Default.Equals(Item, other.Item) && EqualityComparer.Default.Equals(Value, other.Value); } @@ -160,7 +181,7 @@ public bool Equals(TreeViewItem other) public override int GetHashCode() { int hashCode = 11; - hashCode ^= 13 * Item.KeyValue.GetHashCode(); + hashCode ^= 13 * Item.GetHashCode(); hashCode ^= 13 * (Value != null ? Value.GetHashCode() : 0); return hashCode; } @@ -175,7 +196,7 @@ public override string ToString() } /// - /// Determines whether two TreeViewItemOption objects are equal. + /// Determines whether two objects are equal. /// /// The first object to compare. /// The second object to compare. @@ -186,7 +207,7 @@ public override string ToString() } /// - /// Determines whether two TreeViewItemOption objects are not equal. + /// Determines whether two objects are not equal. /// /// The first object to compare. /// The second object to compare. diff --git a/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs b/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs index 463e28f..3c95595 100644 --- a/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs +++ b/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs @@ -2,6 +2,8 @@ using Skyline.DataMiner.Utils.InteractiveAutomationScript; using System.Linq; +using static Skyline.DataMiner.Net.AutomationUI.Objects.TreeViewItem; + namespace InteractiveAutomationToolkitTests { [TestClass] @@ -129,9 +131,9 @@ public void GetItems_Depth1_Test() [TestMethod] public void CheckedValues_Test() { - var item1 = new TreeViewItem("item1", "Item 1", 100); - var item2 = new TreeViewItem("item2", "Item 2", 200); - var item3 = new TreeViewItem("item3", "Item 3", 300); + var item1 = new TreeViewItem("item1", "Item 1", 100) { ItemType = TreeViewItemType.CheckBox }; + var item2 = new TreeViewItem("item2", "Item 2", 200) { ItemType = TreeViewItemType.CheckBox }; + var item3 = new TreeViewItem("item3", "Item 3", 300) { ItemType = TreeViewItemType.CheckBox }; var treeView = new TreeView(new[] { item1, item2, item3 }); treeView.UpdateItemCache(); @@ -170,7 +172,7 @@ public void CollapseAndExpand_Test() } [TestMethod] - public void TreeViewItemOption_Equals_Test() + public void TreeViewItem_Equals_Test() { var item1 = new TreeViewItem("key1", "Display 1", 100); var item2 = new TreeViewItem("key1", "Display 1", 100); @@ -181,7 +183,7 @@ public void TreeViewItemOption_Equals_Test() } [TestMethod] - public void TreeViewItemOption_Properties_Test() + public void TreeViewItem_Properties_Test() { var item = new TreeViewItem("key", "display", "value"); @@ -200,9 +202,9 @@ public void TreeViewItemOption_Properties_Test() [TestMethod] public void CheckedLeaves_And_CheckedNodes_Test() { - var leaf1 = new TreeViewItem("leaf1", "Leaf 1", 10); - var leaf2 = new TreeViewItem("leaf2", "Leaf 2", 20); - var node = new TreeViewItem("node", "Node", 100, childItems: new[] { leaf1, leaf2 }); + var leaf1 = new TreeViewItem("leaf1", "Leaf 1", 10) { ItemType = TreeViewItemType.CheckBox }; + var leaf2 = new TreeViewItem("leaf2", "Leaf 2", 20) { ItemType = TreeViewItemType.CheckBox }; + var node = new TreeViewItem("node", "Node", 100, childItems: new[] { leaf1, leaf2 }) { ItemType = TreeViewItemType.CheckBox }; var treeView = new TreeView(new[] { node }); treeView.UpdateItemCache(); From 1464e90fa0ba37836a9b4fc2ae131e93a031d85a Mon Sep 17 00:00:00 2001 From: Tom Waterbley Date: Thu, 23 Oct 2025 12:54:18 +0200 Subject: [PATCH 5/7] Fixes --- .../Components/Generics/GenericTreeView.cs | 2 +- InteractiveAutomationToolkit/UiResultsExtensions.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs b/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs index de460d8..5dcb6fe 100644 --- a/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs +++ b/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs @@ -179,7 +179,7 @@ public IEnumerable> Items rootItems.Clear(); rootItems.AddRange(value); - BlockDefinition.TreeViewItems = new List(value.Select(x => x.Item)); + BlockDefinition.TreeViewItems = new List(rootItems.Select(x => x.Item)); UpdateItemCache(); } diff --git a/InteractiveAutomationToolkit/UiResultsExtensions.cs b/InteractiveAutomationToolkit/UiResultsExtensions.cs index ec55d0b..d1f1873 100644 --- a/InteractiveAutomationToolkit/UiResultsExtensions.cs +++ b/InteractiveAutomationToolkit/UiResultsExtensions.cs @@ -173,7 +173,7 @@ public static TimeSpan GetTime(this IUIResults uiResults, TimePicker time) /// Represents the information a user has entered or selected in a dialog box of an interactive Automation script. /// The tree view widget. /// The names of tree view items that are expanded. - public static IEnumerable GetExpandedItemKeys(this IUIResults uiResults, TreeView treeView) + public static IEnumerable GetExpandedItemKeys(this IUIResults uiResults, TreeViewBase treeView) { string[] expandedItems = uiResults.GetExpanded(treeView.DestVar); if (expandedItems == null) @@ -190,7 +190,7 @@ public static IEnumerable GetExpandedItemKeys(this IUIResults uiResults, /// Represents the information a user has entered or selected in a dialog box of an interactive Automation script. /// The tree view widget. /// The names of tree view items that are checked. - public static IEnumerable GetCheckedItemKeys(this IUIResults uiResults, TreeView treeView) + public static IEnumerable GetCheckedItemKeys(this IUIResults uiResults, TreeViewBase treeView) { string result = uiResults.GetString(treeView.DestVar); if (String.IsNullOrEmpty(result)) From ab029387523373a5a9d73221e3fa305b32364a1a Mon Sep 17 00:00:00 2001 From: Tom Waterbley Date: Thu, 23 Oct 2025 13:01:37 +0200 Subject: [PATCH 6/7] Update InteractiveAutomationToolkit/Components/TreeViewBase.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- InteractiveAutomationToolkit/Components/TreeViewBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InteractiveAutomationToolkit/Components/TreeViewBase.cs b/InteractiveAutomationToolkit/Components/TreeViewBase.cs index 09e5e51..c34bb77 100644 --- a/InteractiveAutomationToolkit/Components/TreeViewBase.cs +++ b/InteractiveAutomationToolkit/Components/TreeViewBase.cs @@ -25,7 +25,7 @@ public string Tooltip { if (value == null) { - throw new ArgumentNullException("value"); + throw new ArgumentNullException(nameof(value)); } BlockDefinition.TooltipText = value; From d6082b1d70003951d4b3ffc0159d0a5c0fa1223c Mon Sep 17 00:00:00 2001 From: Tom Waterbley Date: Thu, 23 Oct 2025 14:51:37 +0200 Subject: [PATCH 7/7] Refactor TreeView state management and improve efficiency Simplified state management by removing redundant variables and dynamically creating `changedItems` in `RaiseResultEvents`. Replaced `List` with `ISet` for better performance in key operations. Streamlined list initialization using LINQ for improved readability and reduced boilerplate code. Removed unused `Skyline.DataMiner.Automation` namespace to clean up dependencies. Refactored `LoadResult` and event handling logic for consistency and efficiency. Reformatted LINQ queries for better readability. Overall, these changes enhance maintainability, performance, and clarity. --- .../Components/Generics/GenericTreeView.cs | 100 +++++++----------- .../Components/TreeView.cs | 98 +++++++---------- 2 files changed, 75 insertions(+), 123 deletions(-) diff --git a/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs b/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs index 5dcb6fe..b0a303b 100644 --- a/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs +++ b/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs @@ -4,7 +4,6 @@ namespace Skyline.DataMiner.Utils.InteractiveAutomationScript using System.Collections.Generic; using System.Linq; - using Skyline.DataMiner.Automation; using Skyline.DataMiner.Net.AutomationUI.Objects; /// @@ -19,9 +18,6 @@ public class TreeView : TreeViewBase, ITreeView private Dictionary collapsedItemCache; private Dictionary> lookupTable; - private bool itemsChanged = false; - private List> changedItems = new List>(); - private bool itemsChecked = false; private List> checkedItems = new List>(); @@ -179,7 +175,7 @@ public IEnumerable> Items rootItems.Clear(); rootItems.AddRange(value); - BlockDefinition.TreeViewItems = new List(rootItems.Select(x => x.Item)); + BlockDefinition.TreeViewItems = rootItems.Select(x => x.Item).ToList(); UpdateItemCache(); } @@ -304,8 +300,8 @@ public IEnumerable> GetItems(int depth) /// protected internal override void LoadResult(IUIResults uiResults) { - var checkedItemKeys = uiResults.GetCheckedItemKeys(this); // this includes all checked items - var expandedItemKeys = uiResults.GetExpandedItemKeys(this); // this includes all expanded items with LazyLoading set to true + var checkedItemKeys = uiResults.GetCheckedItemKeys(this).ToHashSet(); // this includes all checked items + var expandedItemKeys = uiResults.GetExpandedItemKeys(this).ToHashSet(); // this includes all expanded items with LazyLoading set to true // Check for changes // Expanded Items @@ -315,25 +311,10 @@ protected internal override void LoadResult(IUIResults uiResults) RegisterCollapsedItems(expandedItemKeys); // Checked Items - List newlyCheckedItemKeys = RegisterCheckedItems(checkedItemKeys); + RegisterCheckedItems(checkedItemKeys); // Unchecked Items - List newlyUncheckedItemKeys = RegisterUncheckedItems(checkedItemKeys); - - // Changed Items - List changedItemKeys = new List(); - changedItemKeys.AddRange(newlyCheckedItemKeys); - changedItemKeys.AddRange(newlyUncheckedItemKeys); - if (changedItemKeys.Any() && OnChanged != null) - { - itemsChanged = true; - changedItems = new List>(); - - foreach (string changedItemKey in changedItemKeys) - { - changedItems.Add(lookupTable[changedItemKey]); - } - } + RegisterUncheckedItems(checkedItemKeys); // Persist states foreach (TreeViewItem item in lookupTable.Values) @@ -348,6 +329,8 @@ protected internal override void LoadResult(IUIResults uiResults) /// protected internal override void RaiseResultEvents() { + var changedItems = new List>(); + // Expanded items if (itemsExpanded && OnExpanded != null) { @@ -363,17 +346,19 @@ protected internal override void RaiseResultEvents() // Checked items if (itemsChecked && OnChecked != null) { + changedItems.AddRange(checkedItems); OnChecked(this, checkedItems); } // Unchecked items if (itemsUnchecked && OnUnchecked != null) { + changedItems.AddRange(uncheckedItems); OnUnchecked(this, uncheckedItems); } // Changed items - if (itemsChanged && OnChanged != null) + if (changedItems.Any() && OnChanged != null) { OnChanged(this, changedItems); } @@ -382,7 +367,6 @@ protected internal override void RaiseResultEvents() itemsCollapsed = false; itemsChecked = false; itemsUnchecked = false; - itemsChanged = false; UpdateItemCache(); } @@ -447,68 +431,60 @@ private IEnumerable> GetItems(IEnumerable> child return requestedItems; } - private void RegisterExpandedItems(IEnumerable expandedItemKeys) + private void RegisterExpandedItems(ISet expandedItemKeys) { - List newlyExpandedItems = collapsedItemCache.Where(x => expandedItemKeys.Contains(x.Key) && x.Value).Select(x => x.Key).ToList(); + var newlyExpandedItems = collapsedItemCache + .Where(x => expandedItemKeys.Contains(x.Key) && x.Value) + .Select(x => x.Key) + .ToList(); + if (newlyExpandedItems.Any() && OnExpanded != null) { itemsExpanded = true; - expandedItems = new List>(); - - foreach (string newlyExpandedItemKey in newlyExpandedItems) - { - expandedItems.Add(lookupTable[newlyExpandedItemKey]); - } + expandedItems = newlyExpandedItems.Select(x => lookupTable[x]).ToList(); } } - private void RegisterCollapsedItems(IEnumerable expandedItemKeys) + private void RegisterCollapsedItems(ISet expandedItemKeys) { - List newlyCollapsedItems = collapsedItemCache.Where(x => !expandedItemKeys.Contains(x.Key) && !x.Value).Select(x => x.Key).ToList(); + var newlyCollapsedItems = collapsedItemCache + .Where(x => !expandedItemKeys.Contains(x.Key) && !x.Value) + .Select(x => x.Key) + .ToList(); + if (newlyCollapsedItems.Any() && OnCollapsed != null) { itemsCollapsed = true; - collapsedItems = new List>(); - - foreach (string newlyCollapsedItemKey in newlyCollapsedItems) - { - collapsedItems.Add(lookupTable[newlyCollapsedItemKey]); - } + collapsedItems = newlyCollapsedItems.Select(x => lookupTable[x]).ToList(); } } - private List RegisterCheckedItems(IEnumerable checkedItemKeys) + private void RegisterCheckedItems(ISet checkedItemKeys) { - List newlyCheckedItemKeys = checkedItemCache.Where(x => checkedItemKeys.Contains(x.Key) && !x.Value).Select(x => x.Key).ToList(); + var newlyCheckedItemKeys = checkedItemCache + .Where(x => checkedItemKeys.Contains(x.Key) && !x.Value) + .Select(x => x.Key) + .ToList(); + if (newlyCheckedItemKeys.Any() && OnChecked != null) { itemsChecked = true; - checkedItems = new List>(); - - foreach (string newlyCheckedItemKey in newlyCheckedItemKeys) - { - checkedItems.Add(lookupTable[newlyCheckedItemKey]); - } + checkedItems = newlyCheckedItemKeys.Select(x => lookupTable[x]).ToList(); } - - return newlyCheckedItemKeys; } - private List RegisterUncheckedItems(IEnumerable checkedItemKeys) + private void RegisterUncheckedItems(ISet checkedItemKeys) { - List newlyUncheckedItemKeys = checkedItemCache.Where(x => !checkedItemKeys.Contains(x.Key) && x.Value).Select(x => x.Key).ToList(); + var newlyUncheckedItemKeys = checkedItemCache + .Where(x => !checkedItemKeys.Contains(x.Key) && x.Value) + .Select(x => x.Key) + .ToList(); + if (newlyUncheckedItemKeys.Any() && OnUnchecked != null) { itemsUnchecked = true; - uncheckedItems = new List>(); - - foreach (string newlyUncheckedItemKey in newlyUncheckedItemKeys) - { - uncheckedItems.Add(lookupTable[newlyUncheckedItemKey]); - } + uncheckedItems = newlyUncheckedItemKeys.Select(x => lookupTable[x]).ToList(); } - - return newlyUncheckedItemKeys; } } } diff --git a/InteractiveAutomationToolkit/Components/TreeView.cs b/InteractiveAutomationToolkit/Components/TreeView.cs index e6e7898..1cd32bd 100644 --- a/InteractiveAutomationToolkit/Components/TreeView.cs +++ b/InteractiveAutomationToolkit/Components/TreeView.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; - using Skyline.DataMiner.Automation; using Skyline.DataMiner.Net.AutomationUI.Objects; /// @@ -17,9 +16,6 @@ public class TreeView : TreeViewBase, ITreeView private Dictionary collapsedItemCache; // TODO: should only contain Items with LazyLoading set to true private Dictionary lookupTable; - private bool itemsChanged = false; - private List changedItems = new List(); - private bool itemsChecked = false; private List checkedItems = new List(); @@ -275,8 +271,8 @@ public IEnumerable GetItems(int depth) /// protected internal override void LoadResult(IUIResults uiResults) { - var checkedItemKeys = uiResults.GetCheckedItemKeys(this); // this includes all checked items - var expandedItemKeys = uiResults.GetExpandedItemKeys(this); // this includes all expanded items with LazyLoading set to true + var checkedItemKeys = uiResults.GetCheckedItemKeys(this).ToHashSet(); // this includes all checked items + var expandedItemKeys = uiResults.GetExpandedItemKeys(this).ToHashSet(); // this includes all expanded items with LazyLoading set to true // Check for changes // Expanded Items @@ -286,25 +282,10 @@ protected internal override void LoadResult(IUIResults uiResults) RegisterCollapsedItems(expandedItemKeys); // Checked Items - List newlyCheckedItemKeys = RegisterCheckedItems(checkedItemKeys); + RegisterCheckedItems(checkedItemKeys); // Unchecked Items - List newlyUncheckedItemKeys = RegisterUncheckedItems(checkedItemKeys); - - // Changed Items - List changedItemKeys = new List(); - changedItemKeys.AddRange(newlyCheckedItemKeys); - changedItemKeys.AddRange(newlyUncheckedItemKeys); - if (changedItemKeys.Any() && OnChanged != null) - { - itemsChanged = true; - changedItems = new List(); - - foreach (string changedItemKey in changedItemKeys) - { - changedItems.Add(lookupTable[changedItemKey]); - } - } + RegisterUncheckedItems(checkedItemKeys); // Persist states foreach (TreeViewItem item in lookupTable.Values) @@ -319,6 +300,8 @@ protected internal override void LoadResult(IUIResults uiResults) /// protected internal override void RaiseResultEvents() { + var changedItems = new List(); + // Expanded items if (itemsExpanded && OnExpanded != null) { @@ -334,17 +317,19 @@ protected internal override void RaiseResultEvents() // Checked items if (itemsChecked && OnChecked != null) { + changedItems.AddRange(checkedItems); OnChecked(this, checkedItems); } // Unchecked items if (itemsUnchecked && OnUnchecked != null) { + changedItems.AddRange(uncheckedItems); OnUnchecked(this, uncheckedItems); } // Changed items - if (itemsChanged && OnChanged != null) + if (changedItems.Any() && OnChanged != null) { OnChanged(this, changedItems); } @@ -353,7 +338,6 @@ protected internal override void RaiseResultEvents() itemsCollapsed = false; itemsChecked = false; itemsUnchecked = false; - itemsChanged = false; UpdateItemCache(); } @@ -418,68 +402,60 @@ private IEnumerable GetItems(IEnumerable children, i return requestedItems; } - private void RegisterExpandedItems(IEnumerable expandedItemKeys) + private void RegisterExpandedItems(ISet expandedItemKeys) { - List newlyExpandedItems = collapsedItemCache.Where(x => expandedItemKeys.Contains(x.Key) && x.Value).Select(x => x.Key).ToList(); + var newlyExpandedItems = collapsedItemCache + .Where(x => expandedItemKeys.Contains(x.Key) && x.Value) + .Select(x => x.Key) + .ToList(); + if (newlyExpandedItems.Any() && OnExpanded != null) { itemsExpanded = true; - expandedItems = new List(); - - foreach (string newlyExpandedItemKey in newlyExpandedItems) - { - expandedItems.Add(lookupTable[newlyExpandedItemKey]); - } + expandedItems = newlyExpandedItems.Select(x => lookupTable[x]).ToList(); } } - private void RegisterCollapsedItems(IEnumerable expandedItemKeys) + private void RegisterCollapsedItems(ISet expandedItemKeys) { - List newlyCollapsedItems = collapsedItemCache.Where(x => !expandedItemKeys.Contains(x.Key) && !x.Value).Select(x => x.Key).ToList(); + var newlyCollapsedItems = collapsedItemCache + .Where(x => !expandedItemKeys.Contains(x.Key) && !x.Value) + .Select(x => x.Key) + .ToList(); + if (newlyCollapsedItems.Any() && OnCollapsed != null) { itemsCollapsed = true; - collapsedItems = new List(); - - foreach (string newlyCollapsedItemKey in newlyCollapsedItems) - { - collapsedItems.Add(lookupTable[newlyCollapsedItemKey]); - } + collapsedItems = newlyCollapsedItems.Select(x => lookupTable[x]).ToList(); } } - private List RegisterCheckedItems(IEnumerable checkedItemKeys) + private void RegisterCheckedItems(ISet checkedItemKeys) { - List newlyCheckedItemKeys = checkedItemCache.Where(x => checkedItemKeys.Contains(x.Key) && !x.Value).Select(x => x.Key).ToList(); + var newlyCheckedItemKeys = checkedItemCache + .Where(x => checkedItemKeys.Contains(x.Key) && !x.Value) + .Select(x => x.Key) + .ToList(); + if (newlyCheckedItemKeys.Any() && OnChecked != null) { itemsChecked = true; - checkedItems = new List(); - - foreach (string newlyCheckedItemKey in newlyCheckedItemKeys) - { - checkedItems.Add(lookupTable[newlyCheckedItemKey]); - } + checkedItems = newlyCheckedItemKeys.Select(x => lookupTable[x]).ToList(); } - - return newlyCheckedItemKeys; } - private List RegisterUncheckedItems(IEnumerable checkedItemKeys) + private void RegisterUncheckedItems(ISet checkedItemKeys) { - List newlyUncheckedItemKeys = checkedItemCache.Where(x => !checkedItemKeys.Contains(x.Key) && x.Value).Select(x => x.Key).ToList(); + var newlyUncheckedItemKeys = checkedItemCache + .Where(x => !checkedItemKeys.Contains(x.Key) && x.Value) + .Select(x => x.Key) + .ToList(); + if (newlyUncheckedItemKeys.Any() && OnUnchecked != null) { itemsUnchecked = true; - uncheckedItems = new List(); - - foreach (string newlyUncheckedItemKey in newlyUncheckedItemKeys) - { - uncheckedItems.Add(lookupTable[newlyUncheckedItemKey]); - } + uncheckedItems = newlyUncheckedItemKeys.Select(x => lookupTable[x]).ToList(); } - - return newlyUncheckedItemKeys; } } }