I got a new job recently and the tasks are deeper into WPF than my current expertise. The application contains different templates for CheckBoxes. They contain multiple levels of children and i need to access the values of a specific one.
To visualize:
<Style x:Key="WhiteCheckBox" TargetType="{x:Type CheckBox}">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="32" />
<Setter Property="FocusVisualStyle" Value="{StaticResource RoundedRectFocus}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<BulletDecorator>
<BulletDecorator.Bullet>
<Grid Name="grid" Height="{TemplateBinding Height}" Width="{Binding RelativeSource={RelativeSource Self}, Path=Height, UpdateSourceTrigger=PropertyChanged}" ShowGridLines="False">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="4*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="3*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="4*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
<RowDefinition Height="4*" />
</Grid.RowDefinitions>
<Border Name="MainBorder"
CornerRadius="8"
Grid.ColumnSpan="9"
Grid.RowSpan="9"
BorderThickness="1" SnapsToDevicePixels="True"
Background="Transparent" />
<Border Name="InnerBorder"
Grid.Column="1" Grid.ColumnSpan="5"
Grid.Row="2" Grid.RowSpan="5"
BorderThickness="1" SnapsToDevicePixels="True"
BorderBrush="White" />
<Path Name="InnerPath"
Grid.Column="1" Grid.ColumnSpan="5"
Grid.Row="2" Grid.RowSpan="5"
Data="M31,5 L19.5,5 19.5,19.5 34.5,19.5 34.5,11.75"
Stretch="Fill"
Stroke="White"/>
<Path Name="CheckMark"
Grid.Column="2" Grid.ColumnSpan="5"
Grid.Row="1" Grid.RowSpan="5"
Opacity="0"
Data="M504.502,75.496c-9.997-9.998-26.205-9.998-36.204,0L161.594,382.203L43.702,264.311c-9.997-9.998-26.205-9.997-36.204,0 c-9.998,9.997-9.998,26.205,0,36.203l135.994,135.992c9.994,9.997,26.214,9.99,36.204,0L504.502,111.7 C514.5,101.703,514.499,85.494,504.502,75.496z"
Fill="{StaticResource BrushCompanyGreen}"
Stretch="Fill"
Stroke="{StaticResource BrushCompanyGreen}" />
<Path Name="InderminateMark"
Grid.Column="3"
Grid.Row="4"
Data="M0,4 L1,5 5,1 4,0"
Opacity="0"
Stretch="Fill"
StrokeThickness="0"
Fill="White" />
</Grid>
</BulletDecorator.Bullet>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CheckStates">
<VisualState x:Name="Checked">
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="CheckMark" Duration="0:0:0.2" To="1" />
</Storyboard>
</VisualState>
<VisualState x:Name="Unchecked" >
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="CheckMark" Duration="0:0:0.2" To="0" />
</Storyboard>
</VisualState>
<VisualState x:Name="Indeterminate">
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="InderminateMark" Duration="0:0:0.2" To="1" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter Margin="4,0,4,0"
VerticalAlignment="Center"
HorizontalAlignment="Left"
RecognizesAccessKey="True" />
</BulletDecorator>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="InnerBorder" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="MainBorder" Property="Background" Value="{StaticResource BrushCompanyGreenLight40}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" TargetName="grid" Value="0.25"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I need the value of the size that the InnerBorder has after rendering to properly size an adorner, but i have no idea how i am able to get this value. For other objects in this context i was able to find the information i needed via the parent-child relations within the code, but here it only states Template provided checkbox
.
Currently this is the class for the adorner, that i am trying to fit to the element:
public class RectangleAdorner : Adorner
{
public RectangleAdorner(UIElement adornedElement, bool isRequired, int? widthOffset, int? heightOffset) : base(adornedElement)
{
IsHitTestVisible = false;
IsRequired = isRequired;
HeightOffset = heightOffset.HasValue ? heightOffset.Value : 0;
WidthOffset = widthOffset.HasValue ? widthOffset.Value : 0;
}
public bool IsRequired { get; set; }
public int HeightOffset { get; set; }
public int WidthOffset { get; set; }
protected override void OnRender(DrawingContext drawingContext)
{
double actualWidth = this.AdornedElement.RenderSize.Width - WidthOffset;
double actualHeight = this.AdornedElement.RenderSize.Height - HeightOffset;
Rect adornedElementRect = new Rect(0, 0, actualHeight, actualWidth);
SolidColorBrush renderBrush = new SolidColorBrush(Colors.Green);
renderBrush.Opacity = 0.3;
double render_thickness = 2.0;
Pen renderPen = IsRequired ? new Pen(new SolidColorBrush(Colors.Red), render_thickness)
: new Pen(new SolidColorBrush(Colors.Green), render_thickness);
drawingContext.DrawRectangle(renderBrush, renderPen, adornedElementRect);
}
}
TL/DR:
What do i need to do to this .xaml file, that i can call FrameworkElement checkboxElement.innerPath.Heigth
or similar in my .cs file?
I tried calling child properties and identifying the children out of all rendered Control-objects. I feel the ControlTemplate.Triggers might be helpful, but so far i do not understand a single thing about them.
Edit 1: Added more code and context
Edit 2:
After more trying i found the VisualTreeHelper
. With this i was able to do this:
if (frameworkElement is CheckBox box)
{
var bulletDecorator = (BulletDecorator) VisualTreeHelper.GetChild(box, 0);
var bullet = (Grid)bulletDecorator.Bullet;
var innerBorder = bullet.Children.OfType<Border>().SingleOrDefault(x => x.Name == "InnerBorder");
if (innerBorder is not null)
{
adornableElement = innerBorder;
}
}
This however feels very static and requires knowledge of the template and also will break if the template gets changed. Is there a way to resolve this more elegantly?
The best solution to this problem i have found is the class VisualTreeHelper
.
The method GetChild(parent, index)
will return the child at a certain point, and if you have multiple and dont know the exact number, GetChildren(parent)
will provide all to filter.
This transcends normal .Child
or .Parent
properties and looks for elements that are subordinate to the element you provide within the visual tree.