3

Background

I have a control template for a HeaderedContentControl which contains a button as the header, which opens a popup containing the main content when clicked (via an EventTrigger).

This works as I expect, until I try to add a button to the popup which collapses the control. The main button (the header) disappears, but the popup (the content) stays where it is until I take mouse focus from it (by clicking somewhere).

Here's a shortened example demonstrating the issue:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="WpfTest.View"
    Height="300" Width="400" >

    <Window.Resources>
        <ControlTemplate x:Key="ControlTemplate" TargetType="{x:Type HeaderedContentControl}" >
            <Grid Width="100" Height="25" >
                <Button Content="{TemplateBinding Header}" >
                    <Button.Triggers>
                        <EventTrigger RoutedEvent="Button.Click" >
                            <BeginStoryboard>
                                <Storyboard>
                                    <BooleanAnimationUsingKeyFrames Storyboard.TargetName="Popup"
                                        Storyboard.TargetProperty="(Popup.IsOpen)" >
                                        <DiscreteBooleanKeyFrame KeyTime="0" Value="True" />
                                    </BooleanAnimationUsingKeyFrames>
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>
                    </Button.Triggers>
                </Button>
                <Popup x:Name="Popup" StaysOpen="False" >
                    <ContentPresenter Content="{TemplateBinding Content}" />
                </Popup>
            </Grid>
        </ControlTemplate>
    </Window.Resources>

    <HeaderedContentControl x:Name="MainControl" Template="{StaticResource ControlTemplate}" Header="Show Popup" >
        <HeaderedContentControl.Content>
            <Border Background="White" BorderThickness="1" BorderBrush="Black" >
                <Button Margin="2" Content="Hide All" >
                    <Button.Triggers>
                        <EventTrigger RoutedEvent="Button.Click" >
                            <BeginStoryboard>
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="MainControl"
                                        Storyboard.TargetProperty="(UIElement.Visibility)" >
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}" />
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>
                    </Button.Triggers>
                </Button>
            </Border>
        </HeaderedContentControl.Content>
    </HeaderedContentControl>
</Window>

In my real code, the ControlTemplate isn't in the same place as the control's usage. It's in a ResourceDictionary somewhere else entirely, and shouldn't know anything about the contents of its popup - it passes it straight onto a ContentPresenter.

Question

Is there a way, ideally without letting the ControlTemplate know anything about the possibility of the popup containing a "Hide" button, of making the popup disappear when the HeaderedContentControl is collapsed?

What I've tried

  • Adding another trigger to the HeaderedContentControl to watch for IsVisible changing. There's no appropriate EventTrigger and other triggers can't contain element names.
  • Adding an event handler to IsVisibleChanged in a subclass of HeaderedContentControl. The event fires, but Popup.IsOpen remains true after being set to false.
Philip C
  • 1,819
  • 28
  • 50
  • It may not suit your requirements, but I think I prefer John Melville's approach here: http://stackoverflow.com/questions/361209/how-to-open-a-wpf-popup-when-another-control-is-clicked-using-xaml-markup-only . No storyboards, simpler binding. – goobering Apr 28 '15 at 12:14

1 Answers1

2

The fundamental problem here is that you are using a Storyboard to control the popup state. This means that when the Click event occurs, you are telling WPF to begin an animation, in which it sets the IsOpen property for the popup to true.

Note that the animation has no fixed duration, and so effectively never stops. Your attempt to set Popup.IsOpen to false fails, because WPF is still animating the property and so immediately sets it back to `true.

As proof of concept, try pausing the animation immediately after you start it:

<EventTrigger RoutedEvent="Button.Click" >
  <BeginStoryboard Name="ShowPopupStoryBoard">
    <Storyboard>
      <BooleanAnimationUsingKeyFrames Storyboard.TargetName="Popup"
                            Storyboard.TargetProperty="(Popup.IsOpen)" >
        <DiscreteBooleanKeyFrame KeyTime="0" Value="True" />
      </BooleanAnimationUsingKeyFrames>
    </Storyboard>
  </BeginStoryboard>
  <PauseStoryboard BeginStoryboardName="ShowPopupStoryBoard"/>
</EventTrigger>

By pausing the animation, you prevent WPF from resetting the Popup.IsOpen property. So you can go ahead and react to the IsVisibleChanged (or other appropriate event), setting the Popup.IsOpen property false, and have that actually have an effect.

Unfortunately, this only pauses the animation. And in repeat tests, the above does not work consistently — that is, it always works on the first click, but on subsequent clicks (I changed the demo code to not hide the button), sometimes it does and sometimes it doesn't — which I presume is a timing issue with respect to when the various EventTrigger children are invoked.

In any case, the Storyboard seems like a heavy-weight and, in at least some obvious way, inappropriate mechanism for showing the popup. An alternative that I know will work consistently would be to simply add an event handler to the template's Button control's Click event, and set the Popup.IsOpen property to true there. In that case, you'd just be toggling it, rather than animating it, so WPF isn't going to do anything to try to force it to stay set to true later when you want to set it back to false.

The main thing though, is to choose some mechanism among those available to ensure that the Popup.IsOpen property isn't continually being animated. As long as WPF's animating the property to the true value, setting it to false isn't going to have any effect (indeed, it's a little surprising that the popup disappears at all, even when it loses focus…I guess the lack of focus has priority over the IsOpen property value).

Unfortunately, because the main issue here is related to the current implementation of the template, I think that contrary to your stated preference, any correct fix will require you to modify the template. I guess you could add logic in the popup's content object to track down its storyboard and actually stop it. But that seems like the cure might be worse than the disease in that case.

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • Fantastic - I hadn't considered that the animation could be causing the problem. I realise reading your answer that I was a bit over-restrictive on not modifying the template. I meant I didn't want to add anything specific to a "Hide" button into the template, but adding a pair of event handlers to open on click and close on becoming invisible is fine. Thanks for the very thorough answer! – Philip C Apr 29 '15 at 06:30