Popup, Popup.StaysOpen, ToggleButton and Data Binding -- Helpful Tip
On a recent WPF project we had need of a Popup that would display when a ToggleButton was Checked, not display when Unchecked, and also disappear whenever the user clicked anywhere in the view.
The solution was straightforward: Use Popup.StaysOpen=False, and use data binding on the Popup.IsOpen property to the ToggleButton.IsChecked property (in the below code example, assume a ToggleButton named ExpandPresetList):
<
Popup Name="PresetPopup" IsOpen="{Binding ElementName=ExpandPresetList, Path=IsChecked}" PlacementTarget="{Binding ElementName=DefaultAudiogramPresetsContainer}" AllowsTransparency="True" PopupAnimation="Slide" StaysOpen="False" />Well, this worked great, until you try to use the ToggleButton to no longer show the Popup. What would happen is that the Popup.StaysOpen property would collide with the data binding to the Popup.IsOpen property, and cause the Popup to never close when you click on the ToggleButton.
The solution was to bind to the ToggleButton.MouseEnter and ToggleButton.MouseLeave properties and modify the Popup.StaysOpen property whenever the user was using the ToggleButton.
Example:
<
ToggleButton Name="ExpandPresetList" Width="13" Height="13" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="2.5 0 2.5 0" Style="{StaticResource roundExpandGraphicToggleButtonStyle}" MouseEnter="ExpandPresetList_MouseEnter" MouseLeave="ExpandPresetList_MouseLeave" />The event handlers for MouseEnter and MouseLeave look as follows:
private
void ExpandPresetList_MouseEnter(object sender, MouseEventArgs e){
true;PresetPopup.StaysOpen =
}
private void ExpandPresetList_MouseLeave(object sender, MouseEventArgs e){
false;PresetPopup.StaysOpen =
}
The above examples let you use a Popup, data bound to a ToggleButton and have the Popup disappear whenever the user clicks anywhere, as well as use the ToggleButton to make the Popup disappear.
This seemed like an elegant solution, does anyone have a good alternate solution, maybe one that doesn't involve code, and can be done in pure XAML? I didn't see any properties on Popup that would assist in resolving the data binding event storm that results when using Popup.StaysOpen and ToggleButton.IsChecked.
All Replies
- Hi Dan,
Thanks for sharing! Came in very handy, had a somewhat similar situation, and I was trying to set StaysOpen = false in the MouseLeftButtonUp, which did not work 100%...
Regards,
Dan - I work with Dan Edgar on this project and we just solved another, nasty bug ... in regards to this Popup behavior. I hope this post helps someone else avoid the pain I had to incur while solving it.
The problem asserts itself only when the Popup is expanded (and StaysOpen=false).
Here is how you reproduce it:
1) the user toggles the ToggleButton (which expands the Popup)
2) the user (accidentally) moves the mouse off the ToggleButton
3) the user goes to toggle the ToggleButton (to close the Popup manually)
At this point, the Popup collapses, and then immediately expands (the same symptoms as the original symptoms above and what made us add the MouseEnter and MouseLeave event handlers).
Here is what is happening:
If you have StaysOpen set to false, the Popup internally captures the mouse (so that it knows if the user has clicked elsewhere ... so that it can collapse the Popup).
However, what that does is causes us not to get the MouseEnter above (and thus the necessary and needed StaysOpen=true behavior for manually toggling the Popup open and closed).
Therefore, when the user goes to toggle the Poup closed manually (step 3 above), the captured mouse tells the Popup to collapse ... however, then the ToggleButton kicks in and retoggles the Popup open.
Here is the solution:
You need to make the ToggleButton not respond to the mouse when the Popup is expanded. In this way, only the StaysOpen=false, mouse capture functionality kicks in ... and the Popup collapses ... and doesn't expand again.
How do you do this? You need to bind the IsHitTestVisible property of the ToggleButton to the !IsOpen property of the Popup.
Wait, you say. There is no such thing as the !IsOpen property. Of course not, and for that you need a converter. See below.
<appResources:GraphicToggleButton Name="ExpandPresetList" Width="13" Height="13" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="2.5 0 2.5 0" Style="{StaticResource roundExpandGraphicToggleButtonStyle}" IsHitTestVisible="{Binding ElementName=PresetPopup, Path=IsOpen, Mode=OneWay, Converter={StaticResource boolInverterConverter}}" />
public class BoolInverterConverter : IValueConverter { #region IValueConverter Members public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value is bool) { return !((bool)value); } return null; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } #endregion } - Textbook! Fit my problem like a glove!
- Nice fix. The only (minor) issue I have now is Styling/Animation based on the mouse hovering on the toggle button since it's taken out of hit test consideration.
Thanks to Dan and Cory -- glad to know I'm not the only one. - Thank you, Dan, for your post. It helped me quickly identify the problem. However, I find the proposed solutions troublesome, so I came up with an alternative.
The problem is caused by the behaviour associated with setting Popup.StaysOpen to false. My solution involves tackling the problem at its source by simply not setting StaysOpen to false. I then substitute with my own behaviour:
rootWindow = Window.GetWindow( this ); rootWindow.PreviewMouseLeftButtonDown += rootWindow_PreviewMouseLeftButtonDown;/// <summary> /// Closes the popup when the user clicks outside of this control. /// This approach is necessary since setting Popup.StaysOpen to false /// doesn't play well with binding Popup.IsOpen to ToggleButton.IsChecked.<br/> /// </summary> void rootWindow_PreviewMouseLeftButtonDown( object sender, MouseButtonEventArgs e ) { if ( !IsMouseOver ) IsChecked = false; }


