Posted by: beatkiener | September 4, 2009

Mouse wheel behavior with native UIElement MouseWheel event

This week I was struggling with routed events from a control surrounded in a popup control.

My intention was, to create an attached behavior to support mouse wheel scrolling for any control like a combo box. The behavior should use the native UIElement.MouseWheel without any javascript code, because javascript is not supported in out of browser scenario.

    public sealed class MouseWheelBehavior : Behavior<FrameworkElement>
    {
        protected override void OnAttached()
        {
            this.AssociatedObject.MouseWheel +=new MouseWheelEventHandler(AssociatedObject_MouseWheel);
        }
    }

Well it sounds easy, but the problem is that the mouse wheel event is not bubbling up the visual tree as expected.

Below is the pure visual tree of the combo box control template:

   <Setter Property="Template">
    <Setter.Value>
     <ControlTemplate TargetType="ComboBox">
      <Grid MouseWheel="Grid_MouseWheel">
       <Popup x:Name="Popup" MouseWheel="Popup_MouseWheel">
        <Border x:Name="PopupBorder" MouseWheel="PopupBorder_MouseWheel" >
         <ScrollViewer MouseWheel="ScrollViewer_MouseWheel">
          <ItemsPresenter MouseWheel="ItemsPresenter_MouseWheel"/>
         </ScrollViewer>
        </Border>
       </Popup>
      </Grid>
     </ControlTemplate>
    </Setter.Value>

 

When the popup of the combo box is open and the mouse wheel get raised, it bubbles up the tree until it reaches the popup control.

The following events get fired:

  • ItemsPresenter_MouseWheel
  • ScrollViewer_MouseWheel
  • PopupBorder_MouseWheel

Popup_MouseWheel and Grid_MouseWheel are missing.

After I checked the visual parent of the PopupBorder in the visual tree I noticed that the visual tree ends in a canvas panel (VisualTreeHelper.GetParent (PopupBorder) is Canvas == true).

In the Silverlight.net forum I found the verification: “If the custom control contains a Popup control, do not rely on walking the visual tree, because Popup content is in a separate visual tree.” (http://silverlight.net/forums/t/36899.aspx).
Because of this fact, the routed event cannot bubble up tree and my behavior never gets the routed event.

Nothing else remains for me, as to find each popup control in the sub tree of the associated object and to attach/detach the MouseWheel event for the Popup.Child instance. Fortunately the Popup.Child property is set whereas VisualTreeHelper.GetChild ( popup ) returns null here.

Below you can find the behavior source I’ve wrote. Please note that the scrolling is done through the UI-Automation-API. This allows quite simple to scroll any element which implements the IScrollProvider interface including controls like DataGrid, ListView, Combobox, ScrollViewer, etc. Earlier I posted a MouseWheelService class which describes this in more details.

    public sealed class MouseWheelBehavior : Behavior<FrameworkElement>
    {
        List<Popup> popups = new List<Popup>();

        protected override void OnAttached()
        {
            // note: do it in first LayoutUpdated event, because earlier the 
            // template is not applied to the control and the sub tree is not fully loaded
            this.AssociatedObject.LayoutUpdated += new EventHandler(AssociatedObject_LayoutUpdated);
        }

        protected override void OnDetaching()
        {
            // dettach event
            foreach (Popup popup in popups)
            {
                if (popup.Child != null)
                    popup.Child.MouseWheel += new MouseWheelEventHandler(popup_MouseWheel);
            }
        }

        void popup_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            Point mousePosition = e.GetPosition(null);

            e.Handled = true;

            // horizontal scrolling?
            bool ctrlKey = (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;

            // go through all element beneath the current mouse position
            IEnumerable<UIElement> elements = VisualTreeHelper.FindElementsInHostCoordinates(mousePosition, this.AssociatedObject);
            foreach (UIElement element in elements)
            {
                // get automation peer (if already created for this control)
                AutomationPeer automationPeer = FrameworkElementAutomationPeer.FromElement(element);
                if (automationPeer == null)
                {
                    // create automation peer for element
                    automationPeer = FrameworkElementAutomationPeer.CreatePeerForElement(element);
                }

                //expect null: some elements doesn't have an automation peer implemented
                if (automationPeer != null)
                {

                    // try to get scroll provider
                    // note: TextBoxAutomationPeer does not implement IScrollProvider
                    IScrollProvider scrollProvider = automationPeer.GetPattern(PatternInterface.Scroll) as IScrollProvider;
                    if (scrollProvider != null)
                    {
                        // set scoll amount
                        ScrollAmount scrollAmount = ScrollAmount.NoAmount;
                        if (e.Delta < 0)
                            scrollAmount = ScrollAmount.SmallIncrement;
                        else if (e.Delta > 0)
                            scrollAmount = ScrollAmount.SmallDecrement;

                        // is scrolling horizontal possible
                        if (scrollProvider.HorizontallyScrollable && ctrlKey)
                        {
                            scrollProvider.Scroll(scrollAmount, System.Windows.Automation.ScrollAmount.NoAmount);

                            // break the further serach in the uielement collection
                            break; // foreach
                        }
                        else if (scrollProvider.VerticallyScrollable)
                        {
                            scrollProvider.Scroll(System.Windows.Automation.ScrollAmount.NoAmount, scrollAmount);

                            // break the further serach in the uielement collection
                            break; // foreach
                        }

                        // don't break here, because of encapsulated scroll viewers such as in the treeview from the sl-toolkit
                        //break;
                    }

                }
            }
        }

        void AssociatedObject_LayoutUpdated(object sender, EventArgs e)
        {
            // no longer need for this event
            this.AssociatedObject.LayoutUpdated -= new EventHandler(AssociatedObject_LayoutUpdated);

            // search popups (note: it's possible to have more than one in a template)
            FindPopups(this.AssociatedObject);

            // attach event
            foreach (Popup popup in popups)
            {
                if (popup.Child != null)
                    popup.Child.MouseWheel += new MouseWheelEventHandler(popup_MouseWheel);
            }
        }

        private void FindPopups(DependencyObject parent)
        {
            if (parent == null)
                return;

            int childnum = VisualTreeHelper.GetChildrenCount(parent);
            for (int i = 0; i < childnum; i++)
            {
                var child = VisualTreeHelper.GetChild(parent, i);
                if (child is Popup)
                {
                    this.popups.Add(child as Popup);
                    // note: do not return here, because of other popup control in the neighbor childs
                }
                else
                {
                    // search in the neighbor childs
                    FindPopups(child);
                }
            }
        }

    }

 

Last but not least, I’ve posted a wish in the Silverlight 4 wishlist to integrate this separate visual tree into the “standard” tree so that the routed event can bubble up the tree as expected.


Responses

  1. Good job.

    I just made (last thursday – Sep, 03, 2009) the same behavior and published at the Expression Gallery. I solved the popup problem in a way similar to yours.

    I liked your idea of using the ctrl key for horizontal scroll. My behavior does horizontal scroll only when there is no vertical scroll to do. Do you mind if I implement this on the behavior I published?

    I’m considering implementing this behavior using javascript as well because the native mouse wheel event is not yet supported on Mac. I’m waiting on feedback for that

  2. I forgot to leave the link for the behavior I implemented so you can take a look at it. Here it is:
    http://gallery.expression.microsoft.com/en-us/MouseWheelScroll

  3. Hi Kelps,

    no, I don’t mind if you add the ctrl behavior to your code ( maybe you can put a link to my blog into your code comments :) )
    I think it is worth to implement the behavior with java script also, because for a better support of Mac.
    I did exactly this with a very generic implementation. I would call it “global behavior to attach to the root element of the application”.

    http://blog.thekieners.com/2009/04/06/how-to-enable-mouse-wheel-scrolling-in-silverlight-without-extending-controls/

    Fortunately it does not struggle with the broken tree of a popup control. And a further advantage with java script is that the SL plugin doesn’t need to have the focus, it works with the first mouse wheel event whereas the native mouse wheel event does not get fired until the SL plugin has its focus.

    Do you have experience with out of browser scenario on a Mac? I think there is no way to use the mouse wheel, right?

    Best regards,
    Beat Kiener

  4. How to use this to add Mouse Wheel scrolling to AutoCompletebox popup? Previous solution from //http://blog.thekieners.com/2009/04/06/how-to-enable-mouse-wheel-scrolling-in-silverlight-without-extending-controls/
    does not make Autocompletebox popup list scrollable.

    Andrus.


Leave a response

Your response:

Categories