Search code examples
c#wpfuser-controlscontroltemplate

How to access from code-behind an instance of a Control defined in a ControlTemplate in a Setter in a Style in an UserControl?


Previously I had the template of the UserControl directly set, not through a style, and everything worked nicely: the root of the content could be accessed using this.Template.LoadContent() or through this.Template.FindName("MyControlName", this), both calls being done in OnApplyTemplate, after the base.OnApplyTemplate() call. Now I need a style because I use two DataTriggers to display a type of Control or another in function of a Binding value.

By debugging with the XAML below, I see that after the base.OnApplyTemplate call this.Template.LoadContent() returns a Border with its Child set to an empty ContentPresenter. I wish to get the wpf:TimeSpanPicker element.

I have read this answer and it does not help me because of the result of debugging presented above. The same with this answer.

Before, my UserControl had this directly inside (in its XAML file):

<UserControl.Template>
    <ControlTemplate>
        <wpf:TimeSpanPicker
            HorizontalAlignment="Stretch"
            VerticalAlignment="Stretch"
            HorizontalContentAlignment="Stretch"
            VerticalContentAlignment="Stretch"
            Name="MyTimeSpanPicker"
            Margin="0,0,7,0"/>
    </ControlTemplate>
</UserControl.Template>

Now I have this:

<UserControl.Style>
    <Style TargetType="UserControl">
        <Style.Triggers>
            <DataTrigger Binding="{Binding Mode=OneWay, Converter={StaticResource ClockToType}}"
                                        Value="TimerData">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="UserControl">
                            <wpf:TimeSpanPicker
                                HorizontalAlignment="Stretch"
                                VerticalAlignment="Stretch"
                                HorizontalContentAlignment="Stretch"
                                VerticalContentAlignment="Stretch"
                                Name="MyTimeSpanPicker"
                                Margin="0,0,7,0"/>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </DataTrigger>
            <DataTrigger Binding="{Binding Mode=OneWay, Converter={StaticResource ClockToType}}"
                                        Value="AlarmData">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="UserControl">
                            <Button>Not yet implemented</Button>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </DataTrigger>
        </Style.Triggers>
    </Style>
</UserControl.Style>

The code-behind includes:

internal wpf_timespanpicker.TimeSpanPicker GetMyTimeSpanPicker()
{
    return (wpf_timespanpicker.TimeSpanPicker)Template.LoadContent();
}

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    GetMyTimeSpanPicker().TimeSpanValueChanged += MyTimeSpanPicker_TimeSpanValueChanged;
}

private void MyTimeSpanPicker_TimeSpanValueChanged(object sender, EventArgs e)
{
    CurrentValue = GetMyTimeSpanPicker().TimeSpan;
}

The ClockToType value converter simply transforms one of my Clock classes' instances to their type name.

Update

Now it partially works because of the answer but I need to set the TimeSpan dependency property of the TimeSpanPicker when the CurrentValue dependency property of the UserControl is changed, and the CurrentValue dependency property can be changed when the time span picker is not yet Loaded. What is the best way to postpone this setting? Placing an ApplyTemplate call before the setting does not seem to work because the class variable in which I keep a reference to the TimeSpanPicker is null:

private static void OnCurrentValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var o = d as ClockValueScreen;
    o.ApplyTemplate();
    o.MyTimeSpanPicker.TimeSpan = (TimeSpan)e.NewValue; // but here o.MyTimeSpanPicker is null
}

public override void OnApplyTemplate()
{
    base.OnApplyTemplate(); // execution reaches this point from the ApplyTemplate call above
}

private void MyTimeSpanPicker_Loaded(object sender, RoutedEventArgs e)
{
    MyTimeSpanPicker = (wpf_timespanpicker.TimeSpanPicker)sender;
    MyTimeSpanPicker.TimeSpanValueChanged += MyTimeSpanPicker_TimeSpanValueChanged;
}

Solution

  • You can't use OnApplyTemplate() because there is no TimeSpanPicker element available until the binding has been resolved and the converter has returned a value of "TimerData".

    What you could do instead is to hook up the event handler in the XAML:

    <ControlTemplate TargetType="UserControl">
        <wpf:TimeSpanPicker
                Loaded="OnLoaded"
                HorizontalAlignment="Stretch"
                VerticalAlignment="Stretch"
                HorizontalContentAlignment="Stretch"
                VerticalContentAlignment="Stretch"
                Name="MyTimeSpanPicker"
                Margin="0,0,7,0"/>
    </ControlTemplate>
    

    ...and then handle it in your code-behind;

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
         wpf_timespanpicker.TimeSpanPicker timeSpanPicker = ( wpf_timespanpicker.TimeSpanPicker)sender;
    }
    

    Edit: If you want to do something with the TimeSpanPicker when the CurrentValue property changes, you could use the VisualTreeHelper class to find it in the visual tree:

    private static void OnCurrentValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var o = (ClockValueScreen)d;
        var timeSpanPicker = FindVisualChild<wpf_timespanpicker.TimeSpanPicker>(o);
        //...
    }
    
    
    private static T FindVisualChild<T>(Visual visual) where T : Visual
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
        {
            Visual child = (Visual)VisualTreeHelper.GetChild(visual, i);
            if (child != null)
            {
                T correctlyTyped = child as T;
                if (correctlyTyped != null)
                {
                    return correctlyTyped;
                }
    
                T descendent = FindVisualChild<T>(child);
                if (descendent != null)
                {
                    return descendent;
                }
            }
        }
        return null;