Search code examples
c#wpfxamlc#-4.0attached-properties

Hide the visibility of an Element after n seconds using dependency property


My Goal: I'm having a Buttonand an Image. Image will be Hidden by default and once the user Hover the mouse on the Button, Image should be shown. It should be Visible until the user leave the mouse over the Image or Button. It should hide the image after 6 seconds of user leaves the mouse point(either from button or image). Mouse Hover again before 6 sec and leave should restart the timer.

What I tried I already having a workable solution using AttachedProperty but that's not efficient. I am sensing there will be a memory leak because of the static here.

public class MouseHoverBehavior
{
    public static readonly DependencyProperty ElementProperty = DependencyProperty.RegisterAttached(
        "Element", typeof(UIElement), typeof(MouseHoverBehavior), new UIPropertyMetadata(OnElementChanged));

    private static UIElement target;

    static MouseHoverBehavior()
    {
        timer = new DispatcherTimer();
        timer.Interval = TimeSpan.FromSeconds(6);
        timer.Tick += Timer_Tick;
    }

    private static void OnElementChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {            
        var element = (sender as Button);
        target = (UIElement)e.NewValue;
        target.Visibility = Visibility.Hidden;

        element.MouseEnter += Element_MouseEnter;
        target.MouseEnter += Element_MouseEnter;
        element.MouseLeave += Element_MouseLeave;
        target.MouseLeave += Element_MouseLeave;
    }

    private static DispatcherTimer timer;

    private static void Element_MouseLeave(object sender, MouseEventArgs e)
    { 
        timer.Start();
    }

    private static void Timer_Tick(object sender, EventArgs e)
    {
        target.Visibility = Visibility.Hidden;
    }

    private static void Element_MouseEnter(object sender, MouseEventArgs e)
    {            
        timer.Stop();
        target.Visibility = Visibility.Visible;
    }

    public static void SetElement(DependencyObject element, UIElement value)
    {
        element.SetValue(ElementProperty, value);
    }

    public static string GetElement(DependencyObject element)
    {
        return (string)element.GetValue(ElementProperty);
    }
}

In xaml:

<StackPanel>
    <Image Source="steve.jpg" Width="200" x:Name="image"/>
    <Button Width="200" Height="100" Margin="20" local:MouseHoverBehavior.Element="{Binding ElementName=image}"/>
</StackPanel>

Did anyone have better idea of doing this in effective way.

Thanks.


Solution

  • As Chris W. intimated, the best practice would be to use Storyboards and EventTriggers. This is exactly they kind of scenario they are designed for. Here's how it would work with your example (I changed the image to a Rectangle so I could test it easily):

       <StackPanel>
            <StackPanel.Triggers>
                <EventTrigger SourceName="_button" RoutedEvent="MouseEnter">
                    <BeginStoryboard>
                        <Storyboard>
                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="image" 
                                                           Storyboard.TargetProperty="Visibility">
                                <DiscreteObjectKeyFrame KeyTime="0"
                                                        Value="{x:Static Visibility.Visible}"/>
                            </ObjectAnimationUsingKeyFrames>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
                <EventTrigger SourceName="_button" RoutedEvent="MouseLeave">
                    <BeginStoryboard>
                        <Storyboard>
                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="image"                                                            
                                                           Storyboard.TargetProperty="Visibility">
                                <DiscreteObjectKeyFrame KeyTime="0:00:06" 
                                                        Value="{x:Static Visibility.Collapsed}" />
                            </ObjectAnimationUsingKeyFrames>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
                <EventTrigger SourceName="image" RoutedEvent="MouseEnter">
                    <BeginStoryboard>
                        <Storyboard>
                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="image" 
                                                           Storyboard.TargetProperty="Visibility">
                                <DiscreteObjectKeyFrame KeyTime="0"
                                                        Value="{x:Static Visibility.Visible}"/>
                            </ObjectAnimationUsingKeyFrames>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
                <EventTrigger SourceName="image" RoutedEvent="MouseLeave">
                    <BeginStoryboard>
                        <Storyboard Duration="1">
                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="image"                                                            
                                                           Storyboard.TargetProperty="Visibility">
                                <DiscreteObjectKeyFrame KeyTime="0:00:06" 
                                                        Value="{x:Static Visibility.Collapsed}" />
                            </ObjectAnimationUsingKeyFrames>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </StackPanel.Triggers>
    
            <Button x:Name="_button"
                    Width="200" Height="100" Margin="20" />
            <Rectangle Visibility="Collapsed"
                       Width="200" 
                       Height="200"
                       Fill="Yellow"
                       x:Name="image"/>
        </StackPanel>
    

    You could also have the image fade out beginning after 5 seconds, completely after 6, etc. I know it seems like a lot of markup, but it's very flexible and avoids a lot of the coding logic headaches you would go through if you wanted to do anything more complex. (In fact, if you wanted something more complex, you'd have to use Storyboard's anyway, and doing them in code is not particularly prettier than in XAML).

    But if you don't want to use storyboard animations for some reason, what you're doing seems fine too for your specific scenario. If you're worried about a memory leak (I don't know how many times these XAML elements will be created and destroyed over the life of the app, but unless it's quite a lot I wouldn't worry about this; the most important resources are freed when the FrameworkElement's are unloaded even if the FEs themselves don't get GC'd), you could in your OnElementChanged subscribe to those elements' Unloaded events and when handling those events unsubscribe them from your Element_MouseEnter and Element_MouseLeave handlers.