问题
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 forIsVisible
changing. There's no appropriateEventTrigger
and other triggers can't contain element names. - Adding an event handler to
IsVisibleChanged
in a subclass ofHeaderedContentControl
. The event fires, butPopup.IsOpen
remainstrue
after being set tofalse
.
回答1:
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.
来源:https://stackoverflow.com/questions/29900410/how-to-close-a-popup-when-its-parent-is-collapsed