Can/Should MouseEnter event bubble?

3.7k views Asked by At

Is it possible under any circumstances to get MouseEnter event bubbling?

MSDN says it's an attached event with Direct routing strategy, which technically excludes such possibility. I've a fairly complex control (in essence a hierarchy consisting of grids, stackpanels and content controls). I seem to get MouseEnter event propagated from bottom up, here's the debug dump taken from OnMouseEnter handler (I've the same custom control included at different levels of the hierarchy, which handles MouseEnter, so I have a central place for listening that event):

In: parent:s7b, timestamp:37989609

In: parent:s2, timestamp:37989609

In: parent:Root, timestamp:37989609

s7b, s2 and Root are FrameworkElement names and timestamp is e.Timestamp from MosueEnter event.

Provided that the Routing Strategy is Direct, how does WPF decide on event originator? Does it traverse the visual tree until the first FrameworkElement with attached MouseEnter event is found?

While I'm working on a minimalistic repro set for the problem, could anyone suggest what could cause the behaviour?


And here's the repro:

  1. Create two custom controls, one is a contant control, another is event receiver.

1.1. MyContentControl

Code:

    public class MyContentControl : ContentControl
    {
        static MyContentControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MyContentControl), 
                new FrameworkPropertyMetadata(typeof(MyContentControl)));
        }

        protected override void OnMouseEnter(MouseEventArgs e)
        {
            if (e.Source == e.OriginalSource
                && e.Source is MyContentControl)
            {
                Debug.Write(string.Format("mouseenter:{0}, timestamp:{1}\n",
                    (e.Source as MyContentControl).Name,
                    e.Timestamp));
            }

            base.OnMouseEnter(e);
        }
    }

XAML:

<Style TargetType="{x:Type local:MyContentControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MyContentControl}">
                    <StackPanel Orientation="Horizontal">
                        <local:MouseEventReceiver />
                        <ContentPresenter />
                    </StackPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

1.2 MouseEventReceiver

Code:

public class MouseEventReceiver : Control
{
    static MouseEventReceiver()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(MouseEventReceiver), 
            new FrameworkPropertyMetadata(typeof(MouseEventReceiver)));
    }
}

XAML:

<Style TargetType="{x:Type local:MouseEventReceiver}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate>
                    <Grid Background="LightGray" Width="20" Height="20" Margin="5"></Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
  1. Finally the markup of my test harness:

XAML:

<Window x:Class="MouseTricks.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MouseTricks"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <local:MyContentControl x:Name="c1">
            <local:MyContentControl x:Name="c2">
                <local:MyContentControl x:Name="c3" />
            </local:MyContentControl>
        </local:MyContentControl>
    </Grid>
</Window>

In order to reproduce the problem just hover over the right most gray square and watch the Debug Output window, you'll see three entries in there, while I'm expecting just one.

Cheers.

3

There are 3 answers

8
Black Jack Pershing On

Since this is a complex control it seems likely that when you are entering the Root element with the mouse you are also entering s7b and s2 at the same time. Since all three elements are registered for the MouseEnter event they should all respond at exactly the same time if it is possible for the mouse to enter all three elements simultaneously.

It probably appears that the event is bubbling up the visual tree because you happen to have MouseEnter registered for a line of visual parents of similar size. If I define a Button in a StackPanel with the button stretching to fill the StackPanel and register both for the MouseEnter event then whenever the mouse enters the Button it will by default also be entering the parent (the StackPanel). In this case it may look like the event is bubbling up the visual tree when in fact it is just a direct event for two separate elements that is occurring simultaneously.

If you are creating a complex control then usually you would want one MouseEnter callback for the entire control or specific MouseEnter callbacks for specific pieces of the control. Are you sure that you need callbacks for the entire control as well as individual parts of the control?

-Edit

Just saw your new post. I tried your code and I noticed that the content MyContentControl instances are all nested. Since the MyContentControl class derives from content control the controls are being stretched to fit the available space. You can see this by adding a border property to the MyContentControl class. Since the background of MyContentControl is null by default MouseEnter only gets fired when one of the gray boxes is touched.

The first MyContentControl creates a horizontal Stackpanel and adds the gray box and then a content presenter. Anything to the right of the grid with the first gray box will automatically be in c2 and/or c3 because the content presenter from c1 will be stretched to fit the size of the window which has a fixed height and width. This is why when you hover over c2 you get the MouseEnter for c1 and c2 because when the gray box is touched the mouse has entered the content presenter of c1 and the mouse has also entered the gray box of c2. Similar logic can be used to understand the case for c3.

0
AudioBubble On

Mouse transparent controls (MTC) (I'd tend to call them layout controls) having mouse opaque children (MOC) can't handle mouse events correctly.

I could be wrong, but it looks like a bug to me. I can guess that the culprit is the fact that MTCs are incapable of handling mouse input but pretend to do so rather inconsistantly.

Due to the virtue of attached events, MTCs become Source & OriginalSource of mouse events, also their IsMouseOver gets set to true, which doesn't get on well with other parts of the system.

The workaround is - do subscribe only mouse opaque parts of your controls to mouse events. Sounds horrible at first glance, but at you shouldn't lose much of flexibility provided you use commands.

Any suggestions are highly appreciated.

6
Black Jack Pershing On

Perhaps a more detailed description will help. In the MSDN article on Mouse.MouseEnter the following quote is made:

Although this event is used to track when the mouse enters an element, it is also reporting the IsMouseOver property has changed from false to true on this element

MSDN says that Mouse.MouseEnter fires when IsMouseOver changes from false to true. Looking at the MSDN article for IsMouseOver the following quote is made:

Gets a value that indicates whether the mouse pointer is located over this element (including visual children elements that are inside its bounds)

As we both agree, the null background does not support interaction. There are a lot of caveats to the null background issue with regards to IsMouseOver, but it is obvious from practical application that this value does not get switched for a null background. However, the definition does say that if the mouse is "located over" any visual child within the bounds of the element then IsMouseOver will change barring several strange caveats. However, the null background is not one of these caveats.

A quick look at the visual tree of your control using the snoop utility or VisualTreeHelper shows that all three gray grids are the visual children of c1, the two rightmost grids are visual children of c2, and the rightmost grid is a visual child of c3. This is as would be expected since all of your content controls are nested within each other.

By monitoring the IsMouseOver for c1 property you can easily see that when the mouse touches a gray square the property value changes to true. You can verify this by adding a callback to the main window's mouse move event. I used the following callback:

    private void MouseMove_Callback(Object sender, MouseEventArgs e)
    {
        if (c1.IsMouseOver)
            MessageBox.Show("Mouse is Over c1!");
    }

You will notice that no matter which of the three gray squares that you are over the IsMouseOver for c1 is set to true. This indicates that IsMouseOver changes to true for c1 when it is over any of the three squares and so the claims that MSDN makes are true. MouseEnter should and does get fired for c1 no matter which gray square you touch since all three gray squares are in c1s visual tree and are not eliminated from mouse hit testing by a caveat (such as the null background caveat).

The MouseEnter event is responding as a direct event in your application just as MSDN claims.