diff --git a/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs b/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs new file mode 100644 index 0000000..17ab95b --- /dev/null +++ b/InteractiveAutomationToolkit/Components/Generics/GenericTreeView.cs @@ -0,0 +1,654 @@ +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 : InteractiveWidget, IIsReadonlyWidget + { + 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); + } + } + + /// + /// 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() + { + foreach (var item in GetAllItems()) + { + item.IsCollapsed = true; + } + } + + /// + /// Sets the IsCollapsed state for all items in the tree view to false, causing the entire tree view to be expanded. + /// + public 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 TreeViewItemOption 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 void UpdateItemCache() + { + UpdateItemCache(Items); + } + + /// + /// 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 (TreeViewItemOption 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 (TreeViewItemOption 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) + { + checkedItemCache = new Dictionary(); + collapsedItemCache = new Dictionary(); + lookupTable = new Dictionary>(); + + BuildLookupTable(items); + + foreach (var item in lookupTable.Values) + { + try + { + checkedItemCache.Add(item.KeyValue, item.IsChecked); + if (item.SupportsLazyLoading) + { + collapsedItemCache.Add(item.KeyValue, item.IsCollapsed); + } + } + catch (Exception e) + { + throw new TreeViewDuplicateItemsException(item.KeyValue, e); + } + } + } + + 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 TreeViewItemOption(item, default(T)); + lookupTable[item.KeyValue] = wrapper; + } + + BuildLookupTableFromTreeViewItems(item.ChildItems); + } + } + } +} diff --git a/InteractiveAutomationToolkit/Components/TreeViewItemOption.cs b/InteractiveAutomationToolkit/Components/TreeViewItemOption.cs new file mode 100644 index 0000000..423b9ff --- /dev/null +++ b/InteractiveAutomationToolkit/Components/TreeViewItemOption.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 TreeViewItemOption : 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 TreeViewItemOption(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 TreeViewItemOption( + 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 TreeViewItemOption 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(TreeViewItemOption 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 ==(TreeViewItemOption left, TreeViewItemOption 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 !=(TreeViewItemOption left, TreeViewItemOption right) + { + return !Equals(left, right); + } + } +} diff --git a/InteractiveAutomationToolkit/UiResultsExtensions.cs b/InteractiveAutomationToolkit/UiResultsExtensions.cs index ec55d0b..92b8456 100644 --- a/InteractiveAutomationToolkit/UiResultsExtensions.cs +++ b/InteractiveAutomationToolkit/UiResultsExtensions.cs @@ -201,6 +201,42 @@ public static IEnumerable GetCheckedItemKeys(this IUIResults uiResults, return result.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); } + /// + /// Gets the names of the tree view items that are expanded according to the UI results. + /// + /// Represents the information a user has entered or selected in a dialog box of an interactive Automation script. + /// The generic tree view widget. + /// The type of the value associated with each tree view item. + /// The names of tree view items that are expanded. + public static IEnumerable GetExpandedItemKeys(this IUIResults uiResults, TreeView treeView) + { + string[] expandedItems = uiResults.GetExpanded(treeView.DestVar); + if (expandedItems == null) + { + return Array.Empty(); + } + + return expandedItems.Where(x => !String.IsNullOrWhiteSpace(x)).ToList(); + } + + /// + /// Gets the names of the tree view items that are checked according to the UI results. + /// + /// Represents the information a user has entered or selected in a dialog box of an interactive Automation script. + /// The generic tree view widget. + /// The type of the value associated with each tree view item. + /// The names of tree view items that are checked. + public static IEnumerable GetCheckedItemKeys(this IUIResults uiResults, TreeView treeView) + { + string result = uiResults.GetString(treeView.DestVar); + if (String.IsNullOrEmpty(result)) + { + return Array.Empty(); + } + + return result.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + } + /// /// Gets the time zone info of the client in which the calendar is displayed. /// diff --git a/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs b/InteractiveAutomationToolkitTests/GenericTreeViewTests.cs new file mode 100644 index 0000000..7c60a33 --- /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 TreeViewItemOption("root1", "Root Item 1", 1), + new TreeViewItemOption("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 TreeViewItemOption("child1", "Child Item 1", "child1data"), + new TreeViewItemOption("child2", "Child Item 2", "child2data") + }; + + var rootItem = new TreeViewItemOption("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 TreeViewItemOption("root1", "Root Item 1", 100), + new TreeViewItemOption("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 TreeViewItemOption("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 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 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 TreeViewItemOption("child", "Child", 10); + var root = new TreeViewItemOption("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 TreeViewItemOption("child", "Child", 10); + var root = new TreeViewItemOption("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 TreeViewItemOption("item1", "Item 1", 100); + var item2 = new TreeViewItemOption("item2", "Item 2", 200); + var item3 = new TreeViewItemOption("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 TreeViewItemOption("child", "Child", 10); + var root = new TreeViewItemOption("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 TreeViewItemOption("key1", "Display 1", 100); + var item2 = new TreeViewItemOption("key1", "Display 1", 100); + var item3 = new TreeViewItemOption("key2", "Display 2", 200); + + Assert.AreEqual(item1, item2); + Assert.AreNotEqual(item1, item3); + } + + [TestMethod] + public void TreeViewItemOption_Properties_Test() + { + var item = new TreeViewItemOption("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 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 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 TreeViewItemOption("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 TreeViewItemOption("key", "display", customData); + + Assert.IsNotNull(item.Value); + Assert.AreEqual(customData, item.Value); + } + } +}