Introduction
There are many times when it might be desirable to make a UIElement
visible when another UIElement
is clicked. In my case I wanted to display a set of buttons that overlaid an image, and did not want them always visible because they would clutter up the window and interfere with viewing the image. I initially created this in code behind, with the plan to encapsulate the code in a behavior. Because the pop up is intended to contain buttons, a click on the panel will set the Visibility of the hidden control to Collapsed
. I have to admit that this functionality does make this pop behavior of limited usefulness in many applications, and maybe will enhance the design if I find a need for keeping the control open..
Background
Once this code was encapsulated in a behavior, I beefed it up a bit. One of the features I wanted is for the buttons to disappear after a set time, in this case 10 seconds. Of course did not want them just disappearing if the mouse was over the previously hidden UIElement so I attached events to the UIElement
for the MouseEnterEvent
and MouseLeaveEvent
. Basically stopped the timer as long as the mouse was in the UIElement
and then reset the timer when the mouse left the UIElement
. Also, clicking the original UIElement
again will hide the previously hidden UIElement
. I then added a Storyboard
that created a 2 second fade out so the buttons did not just disappear.
The Behavior
The actual MouseDownUiElementVisibilityBehavior
is actually fairly simple because most of the functionality is in the helper class PopupControlTimer
. There is a public DependencyProperty
for the hidden control, and a private DependencyProperty
for the associated PopupControlTimer
. The hidden control DependencyProperty
has an event handler to associate an event handler with the MouseLeftButtonDown
event, and to initialize an instance of the PopupControlTimer
class.
The MouseLeftButtonDown
event handler basically just changes the Visibility
of the hidden control, and starts the PopupControlTimer
if the Visibility
is Visible
, and otherwise it stops the PopupControlTimer
.
The PopupControlTimer
is where most of the intelligence is. It monitors the MouseEnter
and MouseLeave
event—when the MouseEnter
event occurs, the timer is stopped and when the MouseLeave
event occurs, the timer is restarted from zero. There are two public methods to start and stop the timer, and code to start the fade out of the hidden control using a Storyboard
when the timer expires, and cleanup when the fade out completes
Since it was initially used on a Control
that is not a Button
, I implemented it based on the MouseLeftButtonDown
event, not the click event. This is more flexible since it can be used on controls that do not have a Click
event. Later I started using the PopupControlTimer
that is used by the MouseDownUiElementVisibilityBehavior
for a PopUp
Control
(why create a class that would be so similar), which required some small modifications to the PopupControlTimer
since needed to make sure close the Popup
when the timer expires, and the fade had to be done on the Child
of the Popup
instead of directly on the Popup
.
public class MouseDownUiElementVisibilityBehavior
{
public static readonly DependencyProperty HiddenUiElementProperty =
DependencyProperty.RegisterAttached("HiddenUiElement",
typeof(UIElement), typeof(MouseDownUiElementVisibilityBehavior),
new PropertyMetadata(null, OnHiddenUiElementChanged));
public static UIElement GetHiddenUiElement(UIElement uiElement)
{
return (UIElement)uiElement.GetValue(HiddenUiElementProperty);
}
public static void SetHiddenUiElement(UIElement uiElement, UIElement value)
{
uiElement.Visibility = Visibility.Collapsed;
uiElement.SetValue(HiddenUiElementProperty, value);
}
private static void OnHiddenUiElementChanged(object sender,
DependencyPropertyChangedEventArgs e)
{
var uiElement = (UIElement)sender;
if (e.OldValue != null) GetVisibilityTimer((UIElement)e.OldValue).Dispose();
if (e.NewValue == null)
{
uiElement.MouseLeftButtonDown -= MouseDownEvent;
SetVisibilityTimer(uiElement, null);
}
else if (e.OldValue == null)
{
var hiddenUiElement = (UIElement)e.NewValue;
hiddenUiElement.Visibility = Visibility.Collapsed;
uiElement.MouseLeftButtonDown += MouseDownEvent;
var visibilityTimer = new PopupControlTimer(hiddenUiElement);
SetVisibilityTimer(uiElement, visibilityTimer);
}
}
private static void MouseDownEvent(object sender, RoutedEventArgs e)
{
var hiddenUiElement = GetHiddenUiElement((UIElement)sender);
hiddenUiElement.Visibility = (hiddenUiElement.Visibility == Visibility.Collapsed)
? Visibility.Visible : Visibility.Collapsed;
if (hiddenUiElement.Visibility == Visibility.Visible)
GetVisibilityTimer((UIElement)sender).Start();
else GetVisibilityTimer((UIElement)sender).Stop();
}
private static readonly DependencyProperty VisibilityTimerProperty =
DependencyProperty.RegisterAttached("VisibilityTimer",
typeof(PopupControlTimer), typeof(MouseDownUiElementVisibilityBehavior),
new PropertyMetadata(null));
private static PopupControlTimer GetVisibilityTimer(UIElement uiElement)
{
return (PopupControlTimer)uiElement.GetValue(VisibilityTimerProperty);
}
private static void SetVisibilityTimer(UIElement uiElement, PopupControlTimer value)
{
uiElement.SetValue(VisibilityTimerProperty, value);
}
}
The HiddenUiElement
is the only visible DependencyProperty
. The bahaviour is attached the control that will receive the click to show the hidden UIElement
. When the value of the HiddenUiElement
changes, an event handler associated to the MouseLeftButtonDown
event and a new instance of the PopupControlTimer
is created for the hidden UIElement
. The PopupControlTimer
needs a refernce to the hidden UIElement
since it will monitor the MouseEnter
and MouseLeave
events to ensure that the hidden UIElement
does not disappear as long at the mouse is over the hiddne UIElement
. An instance is saved in a DependencyProperty
associated with the UIElement that is initially clicked so that it can be disposed of when the hidden UIElement
is changed. The other thing that happens when the UIElement in changed is that the event handler for the MouseLeftButtonDown
is removed.
The capture of the MouseLeftButtonDown
is used to hide the Popup
control. If a different behavior is desired, removing this event handler would do the trick.
The Popup Control Class
internal class PopupControlTimer : IDisposable
{
private readonly UIElement _hiddenUiElement;
private readonly DispatcherTimer _timer = new DispatcherTimer();
public PopupControlTimer(UIElement hiddenUiElement)
{
_hiddenUiElement = hiddenUiElement;
_timer.Interval = new TimeSpan(0, 0, 10);
_timer.Tick += (s, arg) =>
{
FadeOutStoryBoard(_hiddenUiElement);
_timer.Stop();
};
hiddenUiElement.MouseEnter += HiddenUiElementMouseEnter;
hiddenUiElement.MouseLeave += HiddenUiElementMouseLeave;
}
public void Start()
{
_timer.Start();
_hiddenUiElement.Visibility = Visibility.Visible;
}
public void Stop()
{
_timer.Stop();
}
private void HiddenUiElementMouseLeave(object sender, MouseEventArgs e) { Start(); }
private void HiddenUiElementMouseEnter(object sender, MouseEventArgs e) { Stop(); }
public void Dispose()
{
_hiddenUiElement.MouseEnter += HiddenUiElementMouseEnter;
_hiddenUiElement.MouseLeave += HiddenUiElementMouseLeave;
}
private void FadeOutStoryBoard(UIElement hiddenUiElement)
{
if (_fadeOutStoryboard == null)
{
_fadeOutStoryboard = new Storyboard();
_fadeOutStoryboard.Completed += (s, e) =>
{
if (!(hiddenUiElement is Popup))
hiddenUiElement.Visibility = Visibility.Collapsed;
else
((Popup) hiddenUiElement).IsOpen = false;
_fadeOutStoryboard.Stop();
};
var fadeOutAnimation = new DoubleAnimation(1.0F, 0.0F,
new Duration(TimeSpan.FromSeconds(2)));
Storyboard.SetTarget(fadeOutAnimation, (hiddenUiElement is Popup)
? ((Popup)hiddenUiElement).Child : hiddenUiElement);
Storyboard.SetTargetProperty(fadeOutAnimation,
new PropertyPath(UIElement.OpacityProperty));
_fadeOutStoryboard.Children.Add(fadeOutAnimation);
}
hiddenUiElement.Dispatcher.BeginInvoke(new Action(_fadeOutStoryboard.Begin),
DispatcherPriority.Render, null);
}
private Storyboard _fadeOutStoryboard;
}
The PopupControlTimer
is primarily tasked with setting the Visibility
of the UIElement
to Collapsed
when the 10 second time elapses, but it does this only after an animation changes the Opacity
of the UIElement
to Zero over a period of 2 seconds. Once the 2 seconds elapses, the Visibility
is actually set to Collapsed
.
There is also a FadeOutStoryBoard
menthod create the animation Storyboard
. The St
oryboard is created once and then reused, and contains the code for when the animation completes..
The PopupControlTimer
also monitors the MouseEnter and MouseLeave events, and disables the timer on MouseEnter
, and resets and restarts the timer when on the MouseLeave
event.
The Popup Control Class Behavior
The code above handles everything required when using the MouseDownUiElementVisibilityBehavior
, but has been enhance to support the fading of a Popup. Part of what is required is in the code above, which includes recognizing that the attached Control
is a Popup
, and setting the Popup
's IsOpen
property to false
when the fading timer has elapsed and also doing the fade on the Child
of the Popup
since it is not possible to fade a Popup
. There is also a DependencyProperty
that is added which will be set to true
on the Popup
to enable the fade behavior once the Popup
is opened.
When the UseWithPopup
DependencyProperty
is set to true
, an event handler to the Popup
's Opened
event is attached and an instance of the PopupControlTimer
is save in a DepencencyProperty
associated with the Popup. The Popup_Opened
event handler will then get this instance of the timer and execute the Start
method of the timer.
public static readonly DependencyProperty UseWithPopupProperty =
DependencyProperty.RegisterAttached("UseWithPopup",
typeof(bool), typeof(PopupControlTimer),
new PropertyMetadata(false, OnUseWithPopupChanged));
public static bool GetUseWithPopup(DependencyObject uiElement)
{
return (bool)uiElement.GetValue(UseWithPopupProperty);
}
public static void SetUseWithPopup(DependencyObject uiElement, bool value)
{
uiElement.SetValue(UseWithPopupProperty, value);
}
private static void OnUseWithPopupChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var popup = (Popup)d;
if ((bool)e.NewValue)
{
popup.Opened += Popup_Opened;
SetPopupControlTimer(popup, new PopupControlTimer(popup));
}
else
{
popup.Opened += Popup_Opened;
SetPopupControlTimer(popup, null);
}
}
private static void Popup_Opened(object sender, EventArgs e)
{
var timer = GetPopupControlTimer((UIElement)sender);
timer.Start();
}
private static readonly DependencyProperty PopupControlTimerProperty =
DependencyProperty.RegisterAttached("PopupControlTimer",
typeof(PopupControlTimer), typeof(DependencyObject),
new PropertyMetadata(null));
private static PopupControlTimer GetPopupControlTimer(DependencyObject uiElement)
{
return (PopupControlTimer)uiElement.GetValue(PopupControlTimerProperty);
}
private static void SetPopupControlTimer(DependencyObject uiElement, PopupControlTimer value)
{
uiElement.SetValue(PopupControlTimerProperty, value);
}
The Sample
Application at startup
Application after top control (Border) clicked showing hidden Border with contained controls
Application as hidden border is fading after 10 second timer elapses
Application after the ToggleButton
is pushed to set IsChecked
equal to true
, which causes the Popup
control to appear
The sample has a Border
that is clicked to bring up a Border
containing a TextBox
. Clicking the Border
again will close the Border
containing a TextBox
. It is possible to click in the TextBox
and type,
Using the timer
Using the timer with a Popup
To use this PopupControlTimer
on a Popup
Control, just need to set the UseWithPopup
property of the PopupControlTimer
to true
as shown in sample XAML for a Popup
Control
below:
<Popup Name="Popup"
HorizontalAlignment="Center"
VerticalAlignment="Center"
fadingPopupMouseDown:PopupControlTimer.UseWithPopup="True"
IsOpen="{Binding IsChecked,
ElementName=ToggleButton}">
<StackPanel Margin="2"
Background="Aqua"
Orientation="Vertical">
<TextBlock Width="100"
Margin="2"
Text="Pop-up" />
<TextBox Width="200"
Margin="2"
Text="You can change this text" />
</StackPanel>
</Popup>
Using the timer with Generic Controls
To use the PopupControlTimer
on a generic UIElement
, just set the HiddenUiElementproperty
property of the MouseDownUiElementVisibilityBehavior
on the UIElement
that is to be clicked to show the make the hidden UIElement
visible:
<Border Width="100"
Height="100"
VerticalAlignment="Top"
Background="AliceBlue"
fadingPopupMouseDown:MouseDownUiElementVisibilityBehavior.HiddenUiElement
="{Binding ElementName=Panel}">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="Click Here to show stack panel"
TextAlignment="Center"
TextWrapping="Wrap" />
</Border>
It should be noted that in the Binding
for the MouseDownUiElementVisibilityBehanvior
only the ElementName
is specified. This is how Binding
to another UIElement
is done.
History
- 02/16/2016: Initial Version.
- 02/19/2016: Updated code to include using Timer with Popup.
- 02/23/2016: Additional explanation of Popup