Introduction
In the WPF world, the UIElement
walking methods are limited. There are times when being able to navigate between UI components can be beneficial. Using design patterns, such as MVVM, can eliminate the need for this type of control management. The advanced walking methods shown here are a good alternative to the native .NET methodology.
Background
A basic understanding of what the logical and visual trees are in WPF.
Using the Code
The methods of the LogicalVisualTreeHelper
have been made extension methods for a DependencyObject
type. This makes using them from a visual control really easy.
For example, to get the main Grid of a WPF application with the application window for a reference...
Grid grid = this.GetVisualChild<Grid>();
Or:
Grid grid = this.GetLogicalChild<Grid>();
The first will traverse through the application's Visual Tree while the second traverses through the Logical Tree. Though the underlying concepts are rather complex, these methods are extremely easy to use. Some controls in WPF, like the ContextMenu
, break the Visual and Logical Trees. Control types like these are out of the reach of these walking methods.
Points of Interest
There are a few things to keep in mind. When is it best to use the Visual Tree or Logical Tree? Is one a better choice than the other?
Logical Tree
The Logical Tree is the best choice and should be used first in an attempt to acquire children or parents. You can read more here on MSDN.
The simplified version is because the Logical Tree is a direct relationship between parent and child. Even the FrameworkElement
's Parent property documentation page says... logical parent. (Example above)
Visual Tree
The Visual Tree will be significantly larger than the Logical Tree. It contains all controls currently shown on screen. More information can be found on MSDN about it. This should be your second choice for attempting to acquire child or parent objects. (Example above)
Ancestors and Descendants
Sometimes using the Logical or Visual Trees alone is not enough to reach the desired control. Sometimes the use of both will be necessary. For these times extension methods for GetAncestor
(s) and GetDescendant
(s) have been added that will attempt to close that gap.
Grid grid = this.GetDescendant<Grid>();
This also works for going up both trees.
Window window = grid.GetAncestor<Window>();
Nameless vs Named Controls
There are overloads for each method that accept a string
that represents a name to look for. When specifying a name the type of control has to derive from FrameworkElement
or FrameworkContentElement
(where the name property originates from).
Nameless Controls
When using the signature for the methods that do not require a name to be specified, the first instance of the type is returned. If there are several nested grids, a call to GetLogicalChild<Grid>
will return the first grid encountered.
Named Controls
When using the signature for the methods that do require a string
for name, the name is matched as well as the type. If the name can't be matched, then nothing is returned.
Notes
This was originally written before the .NET Framework 4.6 was released with its improved support for tree traversal. However, the include methods with their generic signatures may prove to be more convenient even if they do not replace or improve upon the native .NET implementation.
A Quick Example
XAML
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Border BorderBrush="LightBlue"
BorderThickness="1" CornerRadius="5" Margin="5">
<TabControl Margin="5">
<TabItem Header="One">
<TextBox />
</TabItem>
<TabItem Header="Two">
<Button Content="Button"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</TabItem>
</TabControl>
</Border>
</Grid>
<Grid Grid.Column="1">
<Border BorderBrush="LightBlue"
BorderThickness="1" CornerRadius="5" Margin="5">
<Grid>
<Border BorderBrush="Black" BorderThickness="1"
CornerRadius="5" Margin="5">
<TabControl Margin="5" TabStripPlacement="Right">
<TabItem Header="Three">
<TabControl Margin="5"
TabStripPlacement="Bottom">
<TabItem Header="Five">
<Button Content="Button"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</TabItem>
<TabItem x:Name="deepTabItem"
Header="Six">
<TextBlock x:Name="deepTextBlock"
Text="More text" />
</TabItem>
</TabControl>
</TabItem>
<TabItem Header="Four">
<TextBlock Text="Text" />
</TabItem>
</TabControl>
</Border>
</Grid>
</Border>
</Grid>
</Grid>
C#
Grid grid = this.GetVisualChild<Grid>();
Grid grid2 = this.GetLogicalChild<Grid>();
Grid grid3 = this.GetDescendant<Grid>();
HashSet<TabItem> tabItems = this.GetDescendants<TabItem>();
HashSet<TextBlock> textBlocks = this.GetDescendants<TextBlock>();
TabItem firsTabItemEncounter = this.GetDescendant<TabItem>();
TextBlock firstTextBlockEncounter = this.GetDescendant<TextBlock>();
TabItem namedTabItem = this.GetDescendant<TabItem>("deepTabItem");
TextBlock namedTextBlock = this.GetDescendant<TextBlock>("deepTextBlock");
Window window = namedTextBlock.GetAncestor<Window>();