Page 1 of 1

EventTriggers inside ControlTemplates - avoiding leaks

Posted: 05 May 2022, 23:53
by steveh
Hi guys,

So I'm attempting to fix the last remaining leaks in our game and I can't find a nice solution to the following problem.

So currently I'm using the NsApp EventTrigger inside a ControlTemplate to invoke a custom command action when a bespoke routed event is raised:
    <ControlTemplate x:Key="CustomControl.Template"  TargetType="{x:Type Namespace:CustomControl}">
        <Grid x:Name="Root" 
              Background="{TemplateBinding Background}">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="CustomEvent" SourceObject="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}}">
                    <i:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Namespace:CustomControl}}, Path=(Namespace:CustomControl.CustomAction)}" />
                </i:EventTrigger>
                <i:EventTrigger EventName="CustomEvent2" SourceObject="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}}">
                    <i:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Namespace:CustomControl}}, Path=(Namespace:CustomControl.CustomAction2)}" CommandParameter="{Binding}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Grid>
    </ControlTemplate>
The issue is that SourceObject takes a ref counted pointer to the templated parent so it creates a cyclic reference which prevents the custom control from being cleaned up. I've managed to fix this in most other cases by using the NsGui.TriggerAction instead, i.e:
    <ControlTemplate x:Key="CustomControl.Template"  TargetType="{x:Type Namespace:CustomControl}">
        <ControlTemplate.Triggers>
            <EventTrigger RoutedEvent="Namespace:CustomControl.CustomEvent">
                <BeginStoryboard x:Name="CustomEventStoryboard" Storyboard="{StaticResource Storyboard}" />
            </EventTrigger>
            <EventTrigger RoutedEvent="Namespace:CustomControl.CustomEvent2">
                <BeginStoryboard x:Name="CustomEvent2Storyboard" Storyboard="{StaticResource Storyboard}" />
            </EventTrigger>
        </ControlTemplate.Triggers>
        <Grid x:Name="Root" 
                  Background="{TemplateBinding Background}" >
        </Grid>
    </ControlTemplate>
This fixes the bugs and allows me to replace NsApp.EventTrigger so long as the only action I do is BeginStoryboard, StopStoryboard or ControlStoryboard (as those are the only implemented actions which derive from NsGui.TriggerAction).

Is there a replacement for InvokeCommandAction? How would you suggest I go about doing this? Is there any other way for me to reference an event on the control template's templated parent without causing a cyclic dependency? A relative source binding or template binding do not work.

Cheers,

-Steven

Re: EventTriggers inside ControlTemplates - avoiding leaks

Posted: 06 May 2022, 10:54
by sfernandez
Hi Steve,

If the event is going to be raised by the templated parent you should be able to do the same by moving the EventTrigger to the control, using a Style if it needs to be reused:
<Grid>
  <Grid.Resources>
    <ControlTemplate x:Key="CustomControl.Template"  TargetType="{x:Type local:CustomControl}">
      <Grid x:Name="Root" Background="{TemplateBinding Background}" .../>
    </ControlTemplate>
    <Style x:Key="CustomControl.Style" TargetType="{x:Type local:CustomControl}">
      <Setter Property="Background" Value="Silver"/>
      <Setter Property="Template" Value="{StaticResource CustomControl.Template}"/>
      <Setter Property="noesis:StyleInteraction.Triggers">
        <Setter.Value>
          <noesis:StyleTriggerCollection>
            <b:EventTrigger EventName="CustomEvent">
              <b:InvokeCommandAction
                Command="{Binding (local:CustomControl.CustomAction),
                    RelativeSource={RelativeSource AncestorType={x:Type local:CustomControl}}}"/>
            </i:EventTrigger>
            <b:EventTrigger EventName="CustomEvent2">
              <b:InvokeCommandAction
                Command="{Binding (local:CustomControl.CustomAction2),
                    RelativeSource={RelativeSource AncestorType={x:Type local:CustomControl}}}"
                CommandParameter="{Binding}"/>
            </i:EventTrigger>
          </noesis:StyleTriggerCollection>
        </Setter.Value>
      </Setter>
    </Style>
  </Grid.Resources>
  <local:CustomControl Width="200" Height="100" Style="{StaticResource CustomControl.Style}"/>
</Grid>
Hope this helps.