I have a custom control with an override for OnApplyTemplate. In it I tried to access children of children templates but they seem to not be loaded. What I wish: when the PART_IncreaseButton
inside the Popup
of a xtk:SplitButton
is clicked, the Popup
will not close but just let the Button
react to the click.
CustomIntegerUpDown
and CustomSplitButton
are derived from the Xceed Extended WPF Toolkit. CustomIntegerUpDown has no changed style or template or code-behind and currently its only purpose is to do what I said above but I am just at the beginning of it. Below is all the relevant source.
I tried this:
IncrementButton = Utils.FindChild<RepeatButton>(PartPopup, "PART_IncreaseButton")
and IncrementButton is null after that, although in the Immediate Window:
Utils.FindChild<Popup>(this, "PART_Popup")
returns the Popup
as obtained from GetTemplateChild("PART_Popup")
.
then
Utils.FindChild<ButtonSpinner>(PartPopup, "PART_Spinner")
returns null
.
Utils.FindChild<CustomIntegerUpDown>(PartPopup, "MyCustomIntegerUpDown")
return null
.
VisualTreeHelper.GetChildrenCount(PartPopup)
returns 0
.
PartPopup.ApplyTemplate()
returns false
.
I've also seen this and I am not sure if it is worth trying that way.
FindChild
is this (took from here):
/// <summary>
/// Finds a Child of a given item in the visual tree.
/// </summary>
/// <param name="parent">A direct parent of the queried item.</param>
/// <typeparam name="T">The type of the queried item.</typeparam>
/// <param name="childName">x:Name or Name of child. </param>
/// <returns>The first parent item that matches the submitted type parameter.
/// If not matching item can be found,
/// a null parent is being returned.</returns>
public static T FindChild<T>(System.Windows.DependencyObject parent, string childName)
where T : System.Windows.DependencyObject
{
// Confirm parent and childName are valid.
if (parent == null) return null;
T foundChild = null;
int childrenCount = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, i);
// If the child is not of the request child type child
T childType = child as T;
if (childType == null)
{
// recursively drill down the tree
foundChild = FindChild<T>(child, childName);
// If the child is found, break so we do not overwrite the found child.
if (foundChild != null) break;
}
else if (!string.IsNullOrEmpty(childName))
{
var frameworkElement = child as System.Windows.FrameworkElement;
// If the child's name is set for search
if (frameworkElement != null && frameworkElement.Name == childName)
{
// if the child's name is of the request name
foundChild = (T)child;
break;
}
}
else
{
// child element found.
foundChild = (T)child;
break;
}
}
return foundChild;
}
In CustomSplitButton.xaml.cs I have this:
internal Popup PartPopup;
internal Button PartButtonWith1, PartButtonWith5, PartButtonWith10, PartButtonWithCustom;
internal RepeatButton IncrementButton;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
PartPopup = (Popup)GetTemplateChild("PART_Popup");
PartButtonWith1 = (Button)GetTemplateChild("PART_ButtonWith1");
PartButtonWith5 = (Button)GetTemplateChild("PART_ButtonWith5");
PartButtonWith10 = (Button)GetTemplateChild("PART_ButtonWith10");
PartButtonWithCustom = (Button)GetTemplateChild("PART_ButtonWithCustom");
PartPopup.ApplyTemplate();
IncrementButton = Utils.FindChild<RepeatButton>(PartPopup, "PART_IncreaseButton");
if (PartPopup != null)
{
PartPopup.PreviewMouseDown += PART_Popup_PreviewMouseUp;
PartPopup.PreviewMouseUp += PART_Popup_PreviewMouseUp;
}
if (PartButtonWith1 != null)
{
PartButtonWith1.Click += Btns_NewTimer_Click;
}
if (PartButtonWith5 != null)
{
PartButtonWith5.Click += Btns_NewTimer_Click;
}
if (PartButtonWith10 != null)
{
PartButtonWith10.Click += Btns_NewTimer_Click;
}
if (PartButtonWithCustom != null)
{
PartButtonWithCustom.Click += BtnCustom_Click;
}
}
The visual tree is this:
The style of CustomSplitButton is the following (xmlns:xtkThemes="clr-namespace:Xceed.Wpf.Toolkit.Themes;assembly=Xceed.Wpf.Toolkit"
):
<Style x:Key="AddCountSplitButtonStyle" TargetType="{x:Type xtk:SplitButton}">
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Background" Value="{DynamicResource {ComponentResourceKey ResourceId=ButtonNormalBackgroundKey, TypeInTargetAssembly={x:Type xtkThemes:ResourceKeys}}}"/>
<Setter Property="BorderBrush" Value="{DynamicResource {ComponentResourceKey ResourceId=ButtonNormalOuterBorderKey, TypeInTargetAssembly={x:Type xtkThemes:ResourceKeys}}}"/>
<Setter Property="DropDownContentBackground">
<Setter.Value>
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#FFF0F0F0" Offset="0"/>
<GradientStop Color="#FFE5E5E5" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="Padding" Value="3"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type xtk:SplitButton}">
<Grid x:Name="MainGrid" SnapsToDevicePixels="True">
<xtk:ButtonChrome x:Name="ControlChrome" BorderThickness="0" Background="{TemplateBinding Background}" RenderEnabled="{TemplateBinding IsEnabled}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button x:Name="PART_ActionButton" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="0" Padding="{TemplateBinding Padding}" Style="{x:Null}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}">
<Button.Template>
<ControlTemplate TargetType="{x:Type Button}">
<ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}"/>
</ControlTemplate>
</Button.Template>
<Grid>
<xtk:ButtonChrome x:Name="ActionButtonChrome" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" RenderMouseOver="{Binding IsMouseOver, ElementName=PART_ActionButton}" RenderPressed="{Binding IsPressed, ElementName=PART_ActionButton}" RenderEnabled="{TemplateBinding IsEnabled}">
<xtk:ButtonChrome.BorderThickness>
<Binding ConverterParameter="2" Path="BorderThickness" RelativeSource="{RelativeSource TemplatedParent}">
<Binding.Converter>
<xtk:ThicknessSideRemovalConverter/>
</Binding.Converter>
</Binding>
</xtk:ButtonChrome.BorderThickness>
<ContentPresenter x:Name="ActionButtonContent" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</xtk:ButtonChrome>
</Grid>
</Button>
<ToggleButton x:Name="PART_ToggleButton" Grid.Column="1" IsChecked="{Binding IsOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
<ToggleButton.IsHitTestVisible>
<Binding Path="IsOpen" RelativeSource="{RelativeSource TemplatedParent}">
<Binding.Converter>
<xtk:InverseBoolConverter/>
</Binding.Converter>
</Binding>
</ToggleButton.IsHitTestVisible>
<ToggleButton.Template>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}"/>
</ControlTemplate>
</ToggleButton.Template>
<Grid>
<xtk:ButtonChrome x:Name="ToggleButtonChrome" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="1,0" RenderMouseOver="{Binding IsMouseOver, ElementName=PART_ToggleButton}" RenderPressed="{Binding IsPressed, ElementName=PART_ToggleButton}" RenderChecked="{TemplateBinding IsOpen}" RenderEnabled="{TemplateBinding IsEnabled}">
<Grid x:Name="arrowGlyph" IsHitTestVisible="False" Margin="4,3">
<Path x:Name="Arrow" Data="M0,0L3,0 4.5,1.5 6,0 9,0 4.5,4.5z" Fill="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" Height="5" Margin="0,1,0,0" Width="9"/>
</Grid>
</xtk:ButtonChrome>
</Grid>
</ToggleButton>
</Grid>
</xtk:ButtonChrome>
<Popup x:Name="PART_Popup" AllowsTransparency="True" Focusable="False" HorizontalOffset="1" IsOpen="{Binding IsChecked, ElementName=PART_ToggleButton}" Placement="{TemplateBinding DropDownPosition}" VerticalOffset="1"
StaysOpen="False">
<Border BorderThickness="{DynamicResource DefaultBorderThickness}" Margin="10,0,10,10" Background="{DynamicResource DarkerBaseBrush}" BorderBrush="{DynamicResource PopupBorderBrush}" CornerRadius="{DynamicResource DefaultCornerRadius}">
<Grid MinWidth="100" Name="PART_ContentPresenter">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button x:Name="PART_ButtonWith1" Grid.Row="0" Grid.ColumnSpan="2">
1
</Button>
<Button x:Name="PART_ButtonWith5" Grid.Row="1" Grid.ColumnSpan="2">
5
</Button>
<Button x:Name="PART_ButtonWith10" Grid.Row="2" Grid.ColumnSpan="2">
10
</Button>
<local:CustomIntegerUpDown Grid.Row="3" Value="1"
Increment="1" ClipValueToMinMax="True"
x:Name="MyCustomIntegerUpDown">
</local:CustomIntegerUpDown>
<Button x:Name="PART_ButtonWithCustom" Grid.Row="3" Grid.Column="1" Padding="2,2,2,2">
>
</Button>
</Grid>
<Border.Effect>
<DropShadowEffect ShadowDepth="0" BlurRadius="10" Color="{DynamicResource Base6Color}" />
</Border.Effect>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Fill" TargetName="Arrow" Value="#FFAFAFAF"/>
<Setter Property="Foreground" TargetName="ActionButtonChrome" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
In the OnApplyTemplate
I would expect to be able to access children of templates inside templates inside this
. But I did not find a way to do this.
Related question of mine here.
Update 1
The starting point of the example, updated (it uses the TryFindVisualChildElementByName
extension method from BionicCode's answer):
internal Popup PartPopup;
internal Button PartButtonWith1, PartButtonWith5, PartButtonWith10, PartButtonWithCustom;
internal RepeatButton IncrementButton;
private void SplitButton_Loaded(object sender, RoutedEventArgs e)
{
PartPopup = (Popup)GetTemplateChild("PART_Popup");
PartButtonWith1 = (Button)GetTemplateChild("PART_ButtonWith1");
PartButtonWith5 = (Button)GetTemplateChild("PART_ButtonWith5");
PartButtonWith10 = (Button)GetTemplateChild("PART_ButtonWith10");
PartButtonWithCustom = (Button)GetTemplateChild("PART_ButtonWithCustom");
if (PartPopup != null)
{
PartPopup.ApplyTemplateRecursively();
if (PartPopup.TryFindVisualChildElementByName("PART_IncreaseButton", out FrameworkElement incButton))
{
IncrementButton = (RepeatButton)incButton;
// do something with IncrementButton here
}
PartPopup.PreviewMouseDown += PART_Popup_PreviewMouseUp;
PartPopup.PreviewMouseUp += PART_Popup_PreviewMouseUp;
}
if (PartButtonWith1 != null)
{
PartButtonWith1.Click += Btns_NewTimer_Click;
}
if (PartButtonWith5 != null)
{
PartButtonWith5.Click += Btns_NewTimer_Click;
}
if (PartButtonWith10 != null)
{
PartButtonWith10.Click += Btns_NewTimer_Click;
}
if (PartButtonWithCustom != null)
{
PartButtonWithCustom.Click += BtnCustom_Click;
}
}
The ApplyTemplateRecursively
extension method used above, in 2 versions:
Not working version
Is it possible to make this version work somehow? I think it is more efficient.
/// <summary>
/// Not working because the ApplyTemplate affects the VisualTree and when applying
/// templates recursively it does not see the correct updated visual tree to be able
/// to continue.
/// </summary>
/// <param name="root"></param>
internal static void ApplyTemplateRecursively(this System.Windows.DependencyObject root)
{
if (root is System.Windows.Controls.Primitives.Popup p)
{
p.Child.ApplyTemplateRecursively();
return;
}
if (root is FrameworkElement r)
{
r.ApplyTemplate();
}
foreach (object element in System.Windows.LogicalTreeHelper.GetChildren(root))
{
if (element is System.Windows.DependencyObject el)
{
ApplyTemplateRecursively(el);
}
}
}
Working version
/// <summary>
/// I am not sure if this is sufficiently efficient, because it goes through the entire visual tree.
/// </summary>
/// <param name="root"></param>
internal static void ApplyTemplateRecursively(this System.Windows.DependencyObject root)
{
if (root is System.Windows.Controls.Primitives.Popup p)
{
p.Child.ApplyTemplateRecursively();
return;
}
if (root is FrameworkElement r)
{
r.ApplyTemplate();
}
for (int i = 0; i < System.Windows.Media.VisualTreeHelper.GetChildrenCount(root); ++i)
{
DependencyObject d = VisualTreeHelper.GetChild(root, i);
ApplyTemplateRecursively(d);
}
}
Now I am trying to solve the actual problem.
Update 2
I have reported this issue.