3

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 Answers3

2

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.

  • How can you be so stubborn? IsMouseOver is always false for mouse transparent controls, is never gets set to true. –  Feb 23 '11 at 18:35
  • create a controls with a blank template, create OnPropertyChanged override, check for e.Property == IsMouseOverProperty - it'll never be set to true, not a chance. –  Feb 23 '11 at 18:40
  • Of course it wouldn't, as the second quote from MSDN says there needs to be something in the visual tree (with the exception of a couple of caveats) of the element that is hit for `IsMouseOver` change. One of these caveats is for a contentcontrol with a null background. The null background provides no mouse interaction as you can easily verify. In your example code you can easily verify using `VisualTreeHelper.GetChildrenCount()` that your blank control has no visual children. In this case there is absolutely nothing that can possibly be hit so there is no mouse interaction. – Black Jack Pershing Feb 23 '11 at 18:57
  • Please do not be frustrated. I am trying to help you. If you think that there is something wrong with the evidence that I provided in the above post please state the specific evidence that you do not agree with and why. If you are going to say that something is wrong you need to clearly point out what you think is wrong and preferably refute the evidence given. – Black Jack Pershing Feb 23 '11 at 18:59
  • Slight edit. The caveat isn't that the `ContentControl` has a null background, but that it does not have any content. Testing `IsMouseOver` on a `ContentControl` with and without content shows this. I should not have said that the caveat was the null background since the background makes no difference when there isn't any content. Evidently a `ContentControl` without any content is also a caveat. MSDN says that `HitTestCore` is overridden for `ContentControl` (see the MSDN entry on `ContentControl.HitTestCore`) so the developers of WPF must have made this another caveat for some reason. – Black Jack Pershing Feb 23 '11 at 20:16
  • you're grand, sorry for being angry:) wpf itself is one big caveat, but somehow I felt in love with it. cheers. –  Feb 23 '11 at 21:17
1

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.

  • Hi, I can't picture out how mouse enters elements simultaneosly, it's just impossible, regardless of elements having overlapping regions or not. It's always one element. What's interesting is the event gets fired for the very first element with MouseEnter event attached. Source & OriginalSource are MyContentControl, so it doesn't report MouseEventReceiver at all, even thou it was raised for it. MyContentControl has no background so it's incapable of recieving mouse events. - I think I might know what happned. –  Feb 22 '11 at 15:23
  • MyContentControl must be able to receive mouse events because it is the only class within your code that is registered to a mouse event. The background of MyContentControl is null, however, the gray box is a visual child of MyContentControl that does not have a null background so mouse events will get raised when the mouse interacts with the gray squares. – Black Jack Pershing Feb 22 '11 at 15:40
  • Hi, I need no borders to understand how my control works:) We are shaking the air. Not every thing has a simple explanation, some of them have no explanation at all (hopefully it's not the case). Thanks for suggestions anyway. In order to prove that Content Presenters have got nothing to do with the problem - drive your mouse carefully by the road between the third and the second box, no event will be raised. –  Feb 22 '11 at 16:02
  • Naturally no event will be raised unless you touch a gray box because there is a null background. If you touch the middle gray box then you are directly touching c2's gray box so there is a MouseEnter event and you are indirectly touching c1's content presenter because c1's content presenter is presenting c2's content. So touching c2's gray box is equivalent to touching content that belongs to c1 because c2's gray box is part of c1's content. The content (c2's gray box) happens to be in c1's content presenter so MouseEnter gets fired for c1 and c2 when you touch the middle box. – Black Jack Pershing Feb 22 '11 at 16:30
  • indirectly touch shouldn't count, we are dealing with a Direct routed event here. –  Feb 22 '11 at 17:06
  • Perhaps the use of the term "indirectly" is misleading you, I just used it to try to help explain. For example, suppose that you have a ContentPresenter with a null background and a small, gray square in the middle. The MouseEnter event only fires for the ContentPresenter when the gray square is first touched. The gray square is the content of the ContentPresenter and so by touching it you are touching the ContentPresenter. MouseEnter would be fired for the square and the ContentPresenter at the same time. This is exactly the type of situation that you are seeing in your code. – Black Jack Pershing Feb 22 '11 at 17:25
  • That would be sound misleading to anyone, who's familiar with routed events concept. You are missing the point, MouseEnter should never fire for ContentPresenter bacause of its Background is null. Full stop. –  Feb 22 '11 at 19:46
  • The ContentPresenter isn't just a background though. If the ContentPresenter only consisted of a background and it was set to null then you would be correct and there shouldn't be any mouse interaction. However, the Content presenter has other content that does not have a null background (the gray square). A null background only eliminates mouse interaction with the background, not the content. Since the content is part of the visual tree of the control that contains it this means that you are interacting with the containing control as well as with the content when you touch the square. – Black Jack Pershing Feb 23 '11 at 00:30
0

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.