diff --git a/Microsoft.Toolkit.Uwp.UI/Enums/SearchType.cs b/Microsoft.Toolkit.Uwp.UI/Enums/SearchType.cs new file mode 100644 index 00000000000..f17d680e858 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI/Enums/SearchType.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Toolkit.Uwp.UI +{ + /// + /// Indicates a type of search for elements in a visual or logical tree. + /// + public enum SearchType + { + /// + /// Depth-first search, where each branch is recursively explored until the end before moving to the next one. + /// + DepthFirst, + + /// + /// Breadth-first search, where each depthwise level is completely explored before moving to the next one. + /// This is particularly useful if the target element to find is known to not be too distant from the starting + /// point and the whole visual/logical tree from the root is large enough, as it can reduce the traversal time. + /// + BreadthFirst + } +} diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/DependencyObjectExtensions.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/DependencyObjectExtensions.cs index 3caf8c97527..0fccee4bdfa 100644 --- a/Microsoft.Toolkit.Uwp.UI/Extensions/DependencyObjectExtensions.cs +++ b/Microsoft.Toolkit.Uwp.UI/Extensions/DependencyObjectExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using Microsoft.Toolkit.Uwp.UI.Helpers.Internals; using Microsoft.Toolkit.Uwp.UI.Predicates; using Windows.UI.Xaml; using Windows.UI.Xaml.Media; @@ -28,7 +29,22 @@ public static class DependencyObjectExtensions { PredicateByName predicateByName = new(name, comparisonType); - return FindDescendant(element, ref predicateByName); + return FindDescendant(element, ref predicateByName, SearchType.DepthFirst); + } + + /// + /// Find the first descendant of type with a given name. + /// + /// The root element. + /// The name of the element to look for. + /// The comparison type to use to match . + /// The search type to use to explore the visual tree. + /// The descendant that was found, or . + public static FrameworkElement? FindDescendant(this DependencyObject element, string name, StringComparison comparisonType, SearchType searchType) + { + PredicateByName predicateByName = new(name, comparisonType); + + return FindDescendant(element, ref predicateByName, searchType); } /// @@ -42,7 +58,22 @@ public static class DependencyObjectExtensions { PredicateByAny predicateByAny = default; - return FindDescendant>(element, ref predicateByAny); + return FindDescendant>(element, ref predicateByAny, SearchType.DepthFirst); + } + + /// + /// Find the first descendant element of a given type. + /// + /// The type of elements to match. + /// The root element. + /// The search type to use to explore the visual tree. + /// The descendant that was found, or . + public static T? FindDescendant(this DependencyObject element, SearchType searchType) + where T : notnull, DependencyObject + { + PredicateByAny predicateByAny = default; + + return FindDescendant>(element, ref predicateByAny, searchType); } /// @@ -55,7 +86,21 @@ public static class DependencyObjectExtensions { PredicateByType predicateByType = new(type); - return FindDescendant(element, ref predicateByType); + return FindDescendant(element, ref predicateByType, SearchType.DepthFirst); + } + + /// + /// Find the first descendant element of a given type. + /// + /// The root element. + /// The type of element to match. + /// The search type to use to explore the visual tree. + /// The descendant that was found, or . + public static DependencyObject? FindDescendant(this DependencyObject element, Type type, SearchType searchType) + { + PredicateByType predicateByType = new(type); + + return FindDescendant(element, ref predicateByType, searchType); } /// @@ -63,14 +108,30 @@ public static class DependencyObjectExtensions /// /// The type of elements to match. /// The root element. - /// The predicatee to use to match the descendant nodes. + /// The predicate to use to match the descendant nodes. /// The descendant that was found, or . public static T? FindDescendant(this DependencyObject element, Func predicate) where T : notnull, DependencyObject { PredicateByFunc predicateByFunc = new(predicate); - return FindDescendant>(element, ref predicateByFunc); + return FindDescendant>(element, ref predicateByFunc, SearchType.DepthFirst); + } + + /// + /// Find the first descendant element matching a given predicate. + /// + /// The type of elements to match. + /// The root element. + /// The predicate to use to match the descendant nodes. + /// The search type to use to explore the visual tree. + /// The descendant that was found, or . + public static T? FindDescendant(this DependencyObject element, Func predicate, SearchType searchType) + where T : notnull, DependencyObject + { + PredicateByFunc predicateByFunc = new(predicate); + + return FindDescendant>(element, ref predicateByFunc, searchType); } /// @@ -80,48 +141,129 @@ public static class DependencyObjectExtensions /// The type of state to use when matching nodes. /// The root element. /// The state to give as input to . - /// The predicatee to use to match the descendant nodes. + /// The predicate to use to match the descendant nodes. /// The descendant that was found, or . public static T? FindDescendant(this DependencyObject element, TState state, Func predicate) where T : notnull, DependencyObject { PredicateByFunc predicateByFunc = new(state, predicate); - return FindDescendant>(element, ref predicateByFunc); + return FindDescendant>(element, ref predicateByFunc, SearchType.DepthFirst); } /// - /// Find the first descendant element matching a given predicate, using a depth-first search. + /// Find the first descendant element matching a given predicate. + /// + /// The type of elements to match. + /// The type of state to use when matching nodes. + /// The root element. + /// The state to give as input to . + /// The predicate to use to match the descendant nodes. + /// The search type to use to explore the visual tree. + /// The descendant that was found, or . + public static T? FindDescendant(this DependencyObject element, TState state, Func predicate, SearchType searchType) + where T : notnull, DependencyObject + { + PredicateByFunc predicateByFunc = new(state, predicate); + + return FindDescendant>(element, ref predicateByFunc, searchType); + } + + /// + /// Find the first descendant element matching a given predicate. /// /// The type of elements to match. /// The type of predicate in use. /// The root element. - /// The predicatee to use to match the descendant nodes. + /// The predicate to use to match the descendant nodes. + /// The search type to use to explore the visual tree. /// The descendant that was found, or . - private static T? FindDescendant(this DependencyObject element, ref TPredicate predicate) + private static T? FindDescendant(this DependencyObject element, ref TPredicate predicate, SearchType searchType) where T : notnull, DependencyObject where TPredicate : struct, IPredicate { - int childrenCount = VisualTreeHelper.GetChildrenCount(element); - - for (var i = 0; i < childrenCount; i++) + // Depth-first search, with recursive implementation + static T? FindDescendantWithDepthFirstSearch(DependencyObject element, ref TPredicate predicate) { - DependencyObject child = VisualTreeHelper.GetChild(element, i); + int childrenCount = VisualTreeHelper.GetChildrenCount(element); - if (child is T result && predicate.Match(result)) + for (int i = 0; i < childrenCount; i++) { - return result; + DependencyObject child = VisualTreeHelper.GetChild(element, i); + + if (child is T result && predicate.Match(result)) + { + return result; + } + + T? descendant = FindDescendantWithDepthFirstSearch(child, ref predicate); + + if (descendant is not null) + { + return descendant; + } } - T? descendant = FindDescendant(child, ref predicate); + return null; + } + + // Breadth-first search, with iterative implementation and pooled local stack + static T? FindDescendantWithBreadthFirstSearch(DependencyObject element, ref TPredicate predicate) + { + // We're using a pooled buffer writer to amortize allocations for the temporary collection of children + // to visit for each level. The underlying array is deliberately just of type object and not DependencyObject + // to reduce the number of generic instantiations and allow the rented arrays to be reused more. + using ArrayPoolBufferWriter bufferWriter = ArrayPoolBufferWriter.Create(); + + int childrenCount = VisualTreeHelper.GetChildrenCount(element); - if (descendant is not null) + // Add the top level children + for (int i = 0; i < childrenCount; i++) { - return descendant; + DependencyObject child = VisualTreeHelper.GetChild(element, i); + + if (child is T result && predicate.Match(result)) + { + return result; + } + + bufferWriter.Add(child); + } + + // Explore each depth level + for (int i = 0; i < bufferWriter.Count; i++) + { + DependencyObject parent = (DependencyObject)bufferWriter[i]; + + childrenCount = VisualTreeHelper.GetChildrenCount(parent); + + for (int j = 0; j < childrenCount; j++) + { + DependencyObject child = VisualTreeHelper.GetChild(parent, j); + + if (child is T result && predicate.Match(result)) + { + return result; + } + + bufferWriter.Add(child); + } } + + return null; } - return null; + static T? ThrowArgumentOutOfRangeExceptionForInvalidSearchType() + { + throw new ArgumentOutOfRangeException(nameof(searchType), "The input search type is not valid"); + } + + return searchType switch + { + SearchType.DepthFirst => FindDescendantWithDepthFirstSearch(element, ref predicate), + SearchType.BreadthFirst => FindDescendantWithBreadthFirstSearch(element, ref predicate), + _ => ThrowArgumentOutOfRangeExceptionForInvalidSearchType() + }; } /// @@ -141,6 +283,24 @@ public static class DependencyObjectExtensions return FindDescendant(element, name, comparisonType); } + /// + /// Find the first descendant (or self) of type with a given name. + /// + /// The root element. + /// The name of the element to look for. + /// The comparison type to use to match . + /// The search type to use to explore the visual tree. + /// The descendant (or self) that was found, or . + public static FrameworkElement? FindDescendantOrSelf(this DependencyObject element, string name, StringComparison comparisonType, SearchType searchType) + { + if (element is FrameworkElement result && name.Equals(result.Name, comparisonType)) + { + return result; + } + + return FindDescendant(element, name, comparisonType, searchType); + } + /// /// Find the first descendant (or self) element of a given type, using a depth-first search. /// @@ -158,6 +318,24 @@ public static class DependencyObjectExtensions return FindDescendant(element); } + /// + /// Find the first descendant (or self) element of a given type. + /// + /// The type of elements to match. + /// The root element. + /// The search type to use to explore the visual tree. + /// The descendant (or self) that was found, or . + public static T? FindDescendantOrSelf(this DependencyObject element, SearchType searchType) + where T : notnull, DependencyObject + { + if (element is T result) + { + return result; + } + + return FindDescendant(element, searchType); + } + /// /// Find the first descendant (or self) element of a given type, using a depth-first search. /// @@ -174,12 +352,29 @@ public static class DependencyObjectExtensions return FindDescendant(element, type); } + /// + /// Find the first descendant (or self) element of a given type. + /// + /// The root element. + /// The type of element to match. + /// The search type to use to explore the visual tree. + /// The descendant (or self) that was found, or . + public static DependencyObject? FindDescendantOrSelf(this DependencyObject element, Type type, SearchType searchType) + { + if (element.GetType() == type) + { + return element; + } + + return FindDescendant(element, type, searchType); + } + /// /// Find the first descendant (or self) element matching a given predicate, using a depth-first search. /// /// The type of elements to match. /// The root element. - /// The predicatee to use to match the descendant nodes. + /// The predicate to use to match the descendant nodes. /// The descendant (or self) that was found, or . public static T? FindDescendantOrSelf(this DependencyObject element, Func predicate) where T : notnull, DependencyObject @@ -192,6 +387,25 @@ public static class DependencyObjectExtensions return FindDescendant(element, predicate); } + /// + /// Find the first descendant (or self) element matching a given predicate. + /// + /// The type of elements to match. + /// The root element. + /// The predicate to use to match the descendant nodes. + /// The search type to use to explore the visual tree. + /// The descendant (or self) that was found, or . + public static T? FindDescendantOrSelf(this DependencyObject element, Func predicate, SearchType searchType) + where T : notnull, DependencyObject + { + if (element is T result && predicate(result)) + { + return result; + } + + return FindDescendant(element, predicate, searchType); + } + /// /// Find the first descendant (or self) element matching a given predicate, using a depth-first search. /// @@ -199,7 +413,7 @@ public static class DependencyObjectExtensions /// The type of state to use when matching nodes. /// The root element. /// The state to give as input to . - /// The predicatee to use to match the descendant nodes. + /// The predicate to use to match the descendant nodes. /// The descendant (or self) that was found, or . public static T? FindDescendantOrSelf(this DependencyObject element, TState state, Func predicate) where T : notnull, DependencyObject @@ -212,19 +426,130 @@ public static class DependencyObjectExtensions return FindDescendant(element, state, predicate); } + /// + /// Find the first descendant (or self) element matching a given predicate. + /// + /// The type of elements to match. + /// The type of state to use when matching nodes. + /// The root element. + /// The state to give as input to . + /// The predicate to use to match the descendant nodes. + /// The search type to use to explore the visual tree. + /// The descendant (or self) that was found, or . + public static T? FindDescendantOrSelf(this DependencyObject element, TState state, Func predicate, SearchType searchType) + where T : notnull, DependencyObject + { + if (element is T result && predicate(result, state)) + { + return result; + } + + return FindDescendant(element, state, predicate, searchType); + } + /// /// Find all descendant elements of the specified element. This method can be chained with /// LINQ calls to add additional filters or projections on top of the returned results. /// /// This method is meant to provide extra flexibility in specific scenarios and it should not /// be used when only the first item is being looked for. In those cases, use one of the - /// available overloads instead, which will - /// offer a more compact syntax as well as better performance in those cases. + /// available overloads instead, + /// which will offer a more compact syntax as well as better performance in those cases. /// /// /// The root element. /// All the descendant instance from . public static IEnumerable FindDescendants(this DependencyObject element) + { + return FindDescendants(element, SearchType.DepthFirst); + } + + /// + /// Find all descendant elements of the specified element. This method can be chained with + /// LINQ calls to add additional filters or projections on top of the returned results. + /// + /// This method is meant to provide extra flexibility in specific scenarios and it should not + /// be used when only the first item is being looked for. In those cases, use one of the + /// available overloads instead, + /// which will offer a more compact syntax as well as better performance in those cases. + /// + /// + /// The root element. + /// The search type to use to explore the visual tree. + /// All the descendant instance from . + public static IEnumerable FindDescendants(this DependencyObject element, SearchType searchType) + { + // Depth-first traversal, with recursion + static IEnumerable FindDescendantsWithDepthFirstSearch(DependencyObject element) + { + int childrenCount = VisualTreeHelper.GetChildrenCount(element); + + for (var i = 0; i < childrenCount; i++) + { + DependencyObject child = VisualTreeHelper.GetChild(element, i); + + yield return child; + + foreach (DependencyObject childOfChild in FindDescendants(child)) + { + yield return childOfChild; + } + } + } + + // Breadth-first traversal, with pooled local stack + static IEnumerable FindDescendantsWithBreadthFirstSearch(DependencyObject element) + { + using ArrayPoolBufferWriter bufferWriter = ArrayPoolBufferWriter.Create(); + + int childrenCount = VisualTreeHelper.GetChildrenCount(element); + + for (int i = 0; i < childrenCount; i++) + { + DependencyObject child = VisualTreeHelper.GetChild(element, i); + + yield return child; + + bufferWriter.Add(child); + } + + for (int i = 0; i < bufferWriter.Count; i++) + { + DependencyObject parent = (DependencyObject)bufferWriter[i]; + + childrenCount = VisualTreeHelper.GetChildrenCount(parent); + + for (int j = 0; j < childrenCount; j++) + { + DependencyObject child = VisualTreeHelper.GetChild(parent, j); + + yield return child; + + bufferWriter.Add(child); + } + } + } + + static IEnumerable ThrowArgumentOutOfRangeExceptionForInvalidSearchType() + { + throw new ArgumentOutOfRangeException(nameof(searchType), "The input search type is not valid"); + } + + return searchType switch + { + SearchType.DepthFirst => FindDescendantsWithDepthFirstSearch(element), + SearchType.BreadthFirst => FindDescendantsWithBreadthFirstSearch(element), + _ => ThrowArgumentOutOfRangeExceptionForInvalidSearchType() + }; + } + + /// + /// Find all first level descendant elements of the specified element. This method can be chained + /// with LINQ calls to add additional filters or projections on top of the returned results. + /// + /// The root element. + /// All the first level descendant instance from . + public static IEnumerable FindFirstLevelDescendants(this DependencyObject element) { int childrenCount = VisualTreeHelper.GetChildrenCount(element); @@ -233,12 +558,127 @@ public static IEnumerable FindDescendants(this DependencyObjec DependencyObject child = VisualTreeHelper.GetChild(element, i); yield return child; + } + } + + /// + /// Find all first level descendant elements of the specified element. This method can be chained + /// with LINQ calls to add additional filters or projections on top of the returned results. + /// + /// The root element. + /// All the first level descendant instance from . + public static IEnumerable FindFirstLevelDescendantsOrSelf(this DependencyObject element) + { + yield return element; + + int childrenCount = VisualTreeHelper.GetChildrenCount(element); - foreach (DependencyObject childOfChild in FindDescendants(child)) + for (var i = 0; i < childrenCount; i++) + { + DependencyObject child = VisualTreeHelper.GetChild(element, i); + + yield return child; + } + } + + /// + /// Find all descendant elements of the specified element (or self). This method can be chained + /// with LINQ calls to add additional filters or projections on top of the returned results. + /// + /// This method is meant to provide extra flexibility in specific scenarios and it should not + /// be used when only the first item is being looked for. In those cases, use one of the + /// available overloads instead, + /// which will offer a more compact syntax as well as better performance in those cases. + /// + /// + /// The root element. + /// All the descendant instance from . + public static IEnumerable FindDescendantsOrSelf(this DependencyObject element) + { + return FindDescendantsOrSelf(element, SearchType.DepthFirst); + } + + /// + /// Find all descendant elements of the specified element (or self). This method can be chained + /// with LINQ calls to add additional filters or projections on top of the returned results. + /// + /// This method is meant to provide extra flexibility in specific scenarios and it should not + /// be used when only the first item is being looked for. In those cases, use one of the + /// available overloads instead, + /// which will offer a more compact syntax as well as better performance in those cases. + /// + /// + /// The root element. + /// The search type to use to explore the visual tree. + /// All the descendant instance from . + public static IEnumerable FindDescendantsOrSelf(this DependencyObject element, SearchType searchType) + { + // Depth-first traversal, with recursion + static IEnumerable FindDescendantsWithDepthFirstSearch(DependencyObject element) + { + yield return element; + + int childrenCount = VisualTreeHelper.GetChildrenCount(element); + + for (var i = 0; i < childrenCount; i++) { - yield return childOfChild; + DependencyObject child = VisualTreeHelper.GetChild(element, i); + + yield return child; + + foreach (DependencyObject childOfChild in FindDescendants(child)) + { + yield return childOfChild; + } } } + + // Breadth-first traversal, with pooled local stack + static IEnumerable FindDescendantsWithBreadthFirstSearch(DependencyObject element) + { + yield return element; + + using ArrayPoolBufferWriter bufferWriter = ArrayPoolBufferWriter.Create(); + + int childrenCount = VisualTreeHelper.GetChildrenCount(element); + + for (int i = 0; i < childrenCount; i++) + { + DependencyObject child = VisualTreeHelper.GetChild(element, i); + + yield return child; + + bufferWriter.Add(child); + } + + for (int i = 0; i < bufferWriter.Count; i++) + { + DependencyObject parent = (DependencyObject)bufferWriter[i]; + + childrenCount = VisualTreeHelper.GetChildrenCount(parent); + + for (int j = 0; j < childrenCount; j++) + { + DependencyObject child = VisualTreeHelper.GetChild(parent, j); + + yield return child; + + bufferWriter.Add(child); + } + } + } + + static IEnumerable ThrowArgumentOutOfRangeExceptionForInvalidSearchType() + { + throw new ArgumentOutOfRangeException(nameof(searchType), "The input search type is not valid"); + } + + return searchType switch + { + SearchType.DepthFirst => FindDescendantsWithDepthFirstSearch(element), + SearchType.BreadthFirst => FindDescendantsWithBreadthFirstSearch(element), + _ => ThrowArgumentOutOfRangeExceptionForInvalidSearchType() + }; } /// @@ -287,7 +727,7 @@ public static IEnumerable FindDescendants(this DependencyObjec /// /// The type of elements to match. /// The starting element. - /// The predicatee to use to match the ascendant nodes. + /// The predicate to use to match the ascendant nodes. /// The ascendant that was found, or . public static T? FindAscendant(this DependencyObject element, Func predicate) where T : notnull, DependencyObject @@ -304,7 +744,7 @@ public static IEnumerable FindDescendants(this DependencyObjec /// The type of state to use when matching nodes. /// The starting element. /// The state to give as input to . - /// The predicatee to use to match the ascendant nodes. + /// The predicate to use to match the ascendant nodes. /// The ascendant that was found, or . public static T? FindAscendant(this DependencyObject element, TState state, Func predicate) where T : notnull, DependencyObject @@ -320,7 +760,7 @@ public static IEnumerable FindDescendants(this DependencyObjec /// The type of elements to match. /// The type of predicate in use. /// The starting element. - /// The predicatee to use to match the ascendant nodes. + /// The predicate to use to match the ascendant nodes. /// The ascendant that was found, or . private static T? FindAscendant(this DependencyObject element, ref TPredicate predicate) where T : notnull, DependencyObject @@ -399,7 +839,7 @@ public static IEnumerable FindDescendants(this DependencyObjec /// /// The type of elements to match. /// The starting element. - /// The predicatee to use to match the ascendant nodes. + /// The predicate to use to match the ascendant nodes. /// The ascendant (or self) that was found, or . public static T? FindAscendantOrSelf(this DependencyObject element, Func predicate) where T : notnull, DependencyObject @@ -419,7 +859,7 @@ public static IEnumerable FindDescendants(this DependencyObjec /// The type of state to use when matching nodes. /// The starting element. /// The state to give as input to . - /// The predicatee to use to match the ascendant nodes. + /// The predicate to use to match the ascendant nodes. /// The ascendant (or self) that was found, or . public static T? FindAscendantOrSelf(this DependencyObject element, TState state, Func predicate) where T : notnull, DependencyObject @@ -438,8 +878,8 @@ public static IEnumerable FindDescendants(this DependencyObjec /// /// This method is meant to provide extra flexibility in specific scenarios and it should not /// be used when only the first item is being looked for. In those cases, use one of the - /// available overloads instead, which will - /// offer a more compact syntax as well as better performance in those cases. + /// available overloads instead, + /// which will offer a more compact syntax as well as better performance in those cases. /// /// /// The root element. @@ -460,5 +900,73 @@ public static IEnumerable FindAscendants(this DependencyObject element = parent; } } + + /// + /// Find all ascendant elements of the specified element (or self). This method can be chained + /// with LINQ calls to add additional filters or projections on top of the returned results. + /// + /// This method is meant to provide extra flexibility in specific scenarios and it should not + /// be used when only the first item is being looked for. In those cases, use one of the + /// available overloads instead, + /// which will offer a more compact syntax as well as better performance in those cases. + /// + /// + /// The root element. + /// All the descendant instance from . + public static IEnumerable FindAscendantsOrSelf(this DependencyObject element) + { + yield return element; + + while (true) + { + DependencyObject? parent = VisualTreeHelper.GetParent(element); + + if (parent is null) + { + yield break; + } + + yield return parent; + + element = parent; + } + } + + /// + /// Checks whether or not a given instance is a descendant of another one. + /// + /// The input child element. + /// The element to look for in the ascendants hierarchy for . + /// Whether or not is a descendant of . + public static bool IsDescendantOf(this DependencyObject child, DependencyObject element) + { + while (true) + { + DependencyObject? parent = VisualTreeHelper.GetParent(child); + + if (parent is null) + { + return false; + } + + if (parent == element) + { + return true; + } + + child = parent; + } + } + + /// + /// Checks whether or not a given instance is an ascendant of another one. + /// + /// The input parent element. + /// The element to look for in the descendants hierarchy for . + /// Whether or not is an ascendant of . + public static bool IsAscendantOf(this DependencyObject parent, DependencyObject element) + { + return IsDescendantOf(element, parent); + } } } \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/FrameworkElement/FrameworkElementExtensions.LogicalTree.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/FrameworkElement/FrameworkElementExtensions.LogicalTree.cs index 71a766e1f77..998219d449c 100644 --- a/Microsoft.Toolkit.Uwp.UI/Extensions/FrameworkElement/FrameworkElementExtensions.LogicalTree.cs +++ b/Microsoft.Toolkit.Uwp.UI/Extensions/FrameworkElement/FrameworkElementExtensions.LogicalTree.cs @@ -63,7 +63,7 @@ public static partial class FrameworkElementExtensions /// /// The type of elements to match. /// The root element. - /// The predicatee to use to match the child nodes. + /// The predicate to use to match the child nodes. /// The child that was found, or . public static T? FindChild(this FrameworkElement element, Func predicate) where T : notnull, FrameworkElement @@ -80,7 +80,7 @@ public static partial class FrameworkElementExtensions /// The type of state to use when matching nodes. /// The root element. /// The state to give as input to . - /// The predicatee to use to match the child nodes. + /// The predicate to use to match the child nodes. /// The child that was found, or . public static T? FindChild(this FrameworkElement element, TState state, Func predicate) where T : notnull, FrameworkElement @@ -96,7 +96,7 @@ public static partial class FrameworkElementExtensions /// The type of elements to match. /// The type of predicate in use. /// The root element. - /// The predicatee to use to match the child nodes. + /// The predicate to use to match the child nodes. /// The child that was found, or . private static T? FindChild(this FrameworkElement element, ref TPredicate predicate) where T : notnull, FrameworkElement @@ -296,7 +296,7 @@ public static partial class FrameworkElementExtensions /// /// The type of elements to match. /// The root element. - /// The predicatee to use to match the child nodes. + /// The predicate to use to match the child nodes. /// The child (or self) that was found, or . public static T? FindChildOrSelf(this FrameworkElement element, Func predicate) where T : notnull, FrameworkElement @@ -316,7 +316,7 @@ public static partial class FrameworkElementExtensions /// The type of state to use when matching nodes. /// The root element. /// The state to give as input to . - /// The predicatee to use to match the child nodes. + /// The predicate to use to match the child nodes. /// The child (or self) that was found, or . public static T? FindChildOrSelf(this FrameworkElement element, TState state, Func predicate) where T : notnull, FrameworkElement @@ -490,7 +490,7 @@ public static IEnumerable FindChildren(this FrameworkElement e /// /// The type of elements to match. /// The starting element. - /// The predicatee to use to match the parent nodes. + /// The predicate to use to match the parent nodes. /// The parent that was found, or . public static T? FindParent(this FrameworkElement element, Func predicate) where T : notnull, FrameworkElement @@ -507,7 +507,7 @@ public static IEnumerable FindChildren(this FrameworkElement e /// The type of state to use when matching nodes. /// The starting element. /// The state to give as input to . - /// The predicatee to use to match the parent nodes. + /// The predicate to use to match the parent nodes. /// The parent that was found, or . public static T? FindParent(this FrameworkElement element, TState state, Func predicate) where T : notnull, FrameworkElement @@ -523,7 +523,7 @@ public static IEnumerable FindChildren(this FrameworkElement e /// The type of elements to match. /// The type of predicate in use. /// The starting element. - /// The predicatee to use to match the parent nodes. + /// The predicate to use to match the parent nodes. /// The parent that was found, or . private static T? FindParent(this FrameworkElement element, ref TPredicate predicate) where T : notnull, FrameworkElement @@ -600,7 +600,7 @@ public static IEnumerable FindChildren(this FrameworkElement e /// /// The type of elements to match. /// The starting element. - /// The predicatee to use to match the parent nodes. + /// The predicate to use to match the parent nodes. /// The parent (or self) that was found, or . public static T? FindParentOrSelf(this FrameworkElement element, Func predicate) where T : notnull, FrameworkElement @@ -620,7 +620,7 @@ public static IEnumerable FindChildren(this FrameworkElement e /// The type of state to use when matching nodes. /// The starting element. /// The state to give as input to . - /// The predicatee to use to match the parent nodes. + /// The predicate to use to match the parent nodes. /// The parent (or self) that was found, or . public static T? FindParentOrSelf(this FrameworkElement element, TState state, Func predicate) where T : notnull, FrameworkElement diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/Predicates/PredicateByFunc{T,TState}.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/Predicates/PredicateByFunc{T,TState}.cs index d327441568b..0e04f1531d0 100644 --- a/Microsoft.Toolkit.Uwp.UI/Extensions/Predicates/PredicateByFunc{T,TState}.cs +++ b/Microsoft.Toolkit.Uwp.UI/Extensions/Predicates/PredicateByFunc{T,TState}.cs @@ -21,7 +21,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Predicates private readonly TState state; /// - /// The predicatee to use to match items. + /// The predicate to use to match items. /// private readonly Func predicate; @@ -29,7 +29,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Predicates /// Initializes a new instance of the struct. /// /// The state to give as input to . - /// The predicatee to use to match items. + /// The predicate to use to match items. public PredicateByFunc(TState state, Func predicate) { this.state = state; diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/Predicates/PredicateByFunc{T}.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/Predicates/PredicateByFunc{T}.cs index c46b4f77203..8be22ede920 100644 --- a/Microsoft.Toolkit.Uwp.UI/Extensions/Predicates/PredicateByFunc{T}.cs +++ b/Microsoft.Toolkit.Uwp.UI/Extensions/Predicates/PredicateByFunc{T}.cs @@ -15,14 +15,14 @@ namespace Microsoft.Toolkit.Uwp.UI.Predicates where T : class { /// - /// The predicatee to use to match items. + /// The predicate to use to match items. /// private readonly Func predicate; /// /// Initializes a new instance of the struct. /// - /// The predicatee to use to match items. + /// The predicate to use to match items. public PredicateByFunc(Func predicate) { this.predicate = predicate; diff --git a/Microsoft.Toolkit.Uwp.UI/Helpers/Internals/ArrayPoolBufferWriter{T}.cs b/Microsoft.Toolkit.Uwp.UI/Helpers/Internals/ArrayPoolBufferWriter{T}.cs new file mode 100644 index 00000000000..568e7df0dab --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI/Helpers/Internals/ArrayPoolBufferWriter{T}.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; + +namespace Microsoft.Toolkit.Uwp.UI.Helpers.Internals +{ + /// + /// A simple buffer writer implementation using pooled arrays. + /// + /// The type of items to store in the list. + /// + /// This type is a to avoid the object allocation and to + /// enable the pattern-based support. We aren't worried with consumers not + /// using this type correctly since it's private and only accessible within the parent type. + /// + internal struct ArrayPoolBufferWriter : IDisposable + { + /// + /// The default buffer size to use to expand empty arrays. + /// + private const int DefaultInitialBufferSize = 128; + + /// + /// The underlying array. + /// + private T[] array; + + /// + /// The starting offset within . + /// + private int index; + + /// + /// Creates a new instance of the struct. + /// + /// A new instance. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ArrayPoolBufferWriter Create() + { + return new() { array = ArrayPool.Shared.Rent(DefaultInitialBufferSize) }; + } + + /// + /// Gets the total number of items stored in the current instance. + /// + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.array.Length; + } + + /// + /// Gets the item at the specified offset into the current buffer in use. + /// + /// The index of the element to retrieve. + /// The item at the specified offset into the current buffer in use. + /// Thrown when is out of range. + public T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if ((uint)index >= this.index) + { + static void Throw() => throw new ArgumentOutOfRangeException(nameof(index)); + + Throw(); + } + + return this.array[index]; + } + } + + /// + /// Adds a new item to the current collection. + /// + /// The item to add. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(T item) + { + if (this.index == this.array.Length) + { + ResizeBuffer(); + } + + this.array[this.index++] = item; + } + + /// + /// Resets the underlying array and the stored items. + /// + public void Reset() + { + Array.Clear(this.array, 0, this.index); + + this.index = 0; + } + + /// + /// Resizes when there is no space left for new items. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void ResizeBuffer() + { + T[] rent = ArrayPool.Shared.Rent(this.index << 2); + + Array.Copy(this.array, 0, rent, 0, this.index); + Array.Clear(this.array, 0, this.index); + + ArrayPool.Shared.Return(this.array); + + this.array = rent; + } + + /// + public void Dispose() + { + Array.Clear(this.array, 0, this.index); + + ArrayPool.Shared.Return(this.array); + } + } +} \ No newline at end of file diff --git a/UnitTests/UnitTests.UWP/Extensions/Test_VisualTreeExtensions.cs b/UnitTests/UnitTests.UWP/Extensions/Test_VisualTreeExtensions.cs index 3d59f99b0eb..725bfd9bb24 100644 --- a/UnitTests/UnitTests.UWP/Extensions/Test_VisualTreeExtensions.cs +++ b/UnitTests/UnitTests.UWP/Extensions/Test_VisualTreeExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Linq; using System.Threading.Tasks; using Microsoft.Toolkit.Uwp; @@ -52,6 +53,43 @@ await App.DispatcherQueue.EnqueueAsync(async () => }); } + [TestCategory("VisualTree")] + [TestMethod] + [DataRow(SearchType.DepthFirst)] + [DataRow(SearchType.BreadthFirst)] + public async Task Test_VisualTree_FindDescendantByName_Exists(SearchType searchType) + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + + + + + + + + + +") as Page; + + // Test Setup + Assert.IsNotNull(treeRoot, "XAML Failed to Load"); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + // Main Test + var textBlock = treeRoot.FindDescendant("TargetElement", StringComparison.Ordinal, searchType); + + Assert.IsNotNull(textBlock, "Expected to find something."); + Assert.IsInstanceOfType(textBlock, typeof(TextBlock), "Didn't find expected typed element."); + Assert.AreEqual("TargetElement", textBlock.Name, "Didn't find named element."); + }); + } + [TestCategory("VisualTree")] [TestMethod] public async Task Test_VisualTree_FindDescendantByName_NotFound() @@ -119,6 +157,42 @@ await App.DispatcherQueue.EnqueueAsync(async () => }); } + [TestCategory("VisualTree")] + [TestMethod] + [DataRow(SearchType.DepthFirst)] + [DataRow(SearchType.BreadthFirst)] + public async Task Test_VisualTree_FindDescendant_Exists(SearchType searchType) + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + + + + + + + + + +") as Page; + + // Test Setup + Assert.IsNotNull(treeRoot, "XAML Failed to Load"); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + // Main Test + var textBlock = treeRoot.FindDescendant(searchType); + + Assert.IsNotNull(textBlock, "Expected to find something."); + Assert.IsInstanceOfType(textBlock, typeof(TextBlock), "Didn't find expected typed element."); + }); + } + [TestCategory("VisualTree")] [TestMethod] public async Task Test_VisualTree_FindDescendant_ItemsControl_Exists() @@ -157,6 +231,46 @@ await App.DispatcherQueue.EnqueueAsync(async () => }); } + [TestCategory("VisualTree")] + [TestMethod] + [DataRow(SearchType.DepthFirst)] + [DataRow(SearchType.BreadthFirst)] + public async Task Test_VisualTree_FindDescendant_ItemsControl_Exists(SearchType searchType) + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + + + + + + + + + + + + + +") as Page; + + // Test Setup + Assert.IsNotNull(treeRoot, "XAML Failed to Load"); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + // Main Test + var textBlock = treeRoot.FindDescendant(searchType); + + Assert.IsNotNull(textBlock, "Expected to find something."); + Assert.IsInstanceOfType(textBlock, typeof(TextBlock), "Didn't find expected typed element."); + }); + } + [TestCategory("VisualTree")] [TestMethod] public async Task Test_VisualTree_FindDescendant_NotFound() @@ -237,6 +351,55 @@ await App.DispatcherQueue.EnqueueAsync(async () => }); } + [TestCategory("VisualTree")] + [TestMethod] + [DataRow(SearchType.DepthFirst)] + [DataRow(SearchType.BreadthFirst)] + public async Task Test_VisualTree_FindDescendants_Exists(SearchType searchType) + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + + + + + + + + + + + +") as Page; + + // Test Setup + Assert.IsNotNull(treeRoot, "XAML Failed to Load"); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + // Main Test + var textBlocks = treeRoot.FindDescendants(searchType).OfType(); + + Assert.IsNotNull(textBlocks, "Expected to find something."); + + var array = textBlocks.ToArray(); + + Assert.AreEqual(4, array.Length, "Expected to find 4 TextBlock elements."); + + // I don't think we want to guarantee order here, so just care that we can find each one. + Assert.IsTrue(array.Any((tb) => tb.Name == "One"), "Couldn't find TextBlock 'One'"); + Assert.IsTrue(array.Any((tb) => tb.Name == "Two"), "Couldn't find TextBlock 'Two'"); + Assert.IsTrue(array.Any((tb) => tb.Name == "Three"), "Couldn't find TextBlock 'Three'"); + + // TextBox has one in its template! + Assert.IsTrue(array.Any((tb) => tb.Name == "PlaceholderTextContentPresenter"), "Couldn't find hidden TextBlock from TextBox."); + }); + } + [TestCategory("VisualTree")] [TestMethod] public async Task Test_VisualTree_FindDescendants_NotFound() @@ -276,6 +439,103 @@ await App.DispatcherQueue.EnqueueAsync(async () => }); } + [TestCategory("VisualTree")] + [TestMethod] + public async Task Test_VisualTree_FindFirstLevelDescendants_Exists() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + + + + + + + + + + + + + + + + + +") as Page; + + // Test Setup + Assert.IsNotNull(treeRoot, "XAML Failed to Load"); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + // Main Test + var rootGrid = treeRoot.FindDescendant(); + var children = rootGrid.FindFirstLevelDescendants().ToArray(); + + Assert.AreEqual(5, children.Length, "Expected to find 5 children."); + + Assert.IsTrue(children.Any(c => ((FrameworkElement)c).Name == "A"), "Couldn't find child 'A'"); + Assert.IsTrue(children.Any(c => ((FrameworkElement)c).Name == "B"), "Couldn't find child 'B'"); + Assert.IsTrue(children.Any(c => ((FrameworkElement)c).Name == "C"), "Couldn't find child 'C'"); + Assert.IsTrue(children.Any(c => ((FrameworkElement)c).Name == "D"), "Couldn't find child 'D'"); + Assert.IsTrue(children.Any(c => ((FrameworkElement)c).Name == "E"), "Couldn't find child 'E'"); + }); + } + + [TestCategory("VisualTree")] + [TestMethod] + public async Task Test_VisualTree_FindFirstLevelDescendantsOrSelf_Exists() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + + + + + + + + + + + + + + + + + +") as Page; + + // Test Setup + Assert.IsNotNull(treeRoot, "XAML Failed to Load"); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + // Main Test + var rootGrid = treeRoot.FindDescendant(); + var childrenOrSelf = rootGrid.FindFirstLevelDescendantsOrSelf().ToArray(); + + Assert.AreEqual(6, childrenOrSelf.Length, "Expected to find 6 children or self."); + + Assert.IsTrue(childrenOrSelf.Any(c => ((FrameworkElement)c).Name == "RootGrid"), "Couldn't find self"); + Assert.IsTrue(childrenOrSelf.Any(c => ((FrameworkElement)c).Name == "A"), "Couldn't find child 'A'"); + Assert.IsTrue(childrenOrSelf.Any(c => ((FrameworkElement)c).Name == "B"), "Couldn't find child 'B'"); + Assert.IsTrue(childrenOrSelf.Any(c => ((FrameworkElement)c).Name == "C"), "Couldn't find child 'C'"); + Assert.IsTrue(childrenOrSelf.Any(c => ((FrameworkElement)c).Name == "D"), "Couldn't find child 'D'"); + Assert.IsTrue(childrenOrSelf.Any(c => ((FrameworkElement)c).Name == "E"), "Couldn't find child 'E'"); + }); + } + [TestCategory("VisualTree")] [TestMethod] public async Task Test_VisualTree_FindAscendant_Exists() @@ -520,6 +780,54 @@ await App.DispatcherQueue.EnqueueAsync(async () => }); } + [TestCategory("VisualTree")] + [TestMethod] + public async Task Test_VisualTree_IsAscendantOrDescendantOf() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var treeRoot = XamlReader.Load(@" + + + + + + + + + +") as Page; + + // Test Setup + Assert.IsNotNull(treeRoot, "XAML Failed to Load"); + + // Initialize Visual Tree + await SetTestContentAsync(treeRoot); + + // Get the root and target elements + var rootGrid = treeRoot.FindDescendant("RootGrid"); + var nestedBorder = treeRoot.FindDescendant("NestedBorder"); + var textBlock = treeRoot.FindDescendant("TargetElement"); + + Assert.IsTrue(rootGrid.IsAscendantOf(nestedBorder)); + Assert.IsTrue(rootGrid.IsAscendantOf(textBlock)); + Assert.IsFalse(rootGrid.IsDescendantOf(nestedBorder)); + Assert.IsFalse(rootGrid.IsDescendantOf(textBlock)); + + Assert.IsTrue(nestedBorder.IsDescendantOf(rootGrid)); + Assert.IsFalse(nestedBorder.IsDescendantOf(textBlock)); + Assert.IsFalse(nestedBorder.IsAscendantOf(rootGrid)); + Assert.IsFalse(nestedBorder.IsAscendantOf(textBlock)); + + Assert.IsTrue(textBlock.IsDescendantOf(rootGrid)); + Assert.IsFalse(textBlock.IsDescendantOf(nestedBorder)); + Assert.IsFalse(textBlock.IsAscendantOf(rootGrid)); + Assert.IsFalse(textBlock.IsAscendantOf(nestedBorder)); + }); + } + // TODO: Add another Ascendants test where we have something like a ListView which creates a container. } } \ No newline at end of file