Ary Borenszweig

Ary Borenszweig

Adding double click support in Silverlight

12 min
Jun 24 2008
coding
12 min
Jun 24 2008

Silverlight Beta 2 doesn't support double click, but since I needed it, I implemented it. I've created a class named Mouse. You create a Mouse instance that wraps an UIElement. The instance attaches itself to the MouseLeftDownButton and, using a timer, it allows you to recieve events when double click is performed.

Actually, you can detect as many clicks as you wish: the event arguments include the number of clicks performed by the user.

But I don't want to attach to mouse events in code. I want to do it in XAML, like with MouseLeftButtonDown or Click. But, unfortunately, you can't use attached properties whose type is an event handler. If you do that, you get an ugly exception.

"What a pity", I thought, "now I need to remove some of my MouseLeftButtonDown events and replace them by code that creates Mouse instances and then attaching to Mouse events programatically". But before doing that, I decided to give it another shot. What if I created an attached property whose type is string, and then at runtime I searched the method and invoke it?

The problem with that approach is that the method won't be declared in the object where you are attaching the event, but probably in some parent control. For example:

<UserControl>
    <Rectangle manas:Mouse.Click="SomeMethod" />
</UserControl>

SomeMethod is probably declared in our custom UserControl, not in Rectangle. But that poses no problem at all: just go up in the visual hierarchy throught the Parent property until we find a class that declares SomeMethod with the signature of our interest.

But, alas, at the time attached properties are processed by the XAML processor, the object in question is not yet in the visual hierarchy: it doesn't have a parent. That's no problem at all: we attach to the Loaded event and do the lookup in that moment, and we are guaranted that we can reach the parent we are interested in.

Once I did that I was really amazed that it worked! :-)

Then, I refactored the code so I could use that trick to support other custom event handlers in XAML as well. Finally, changing the old event handlers to start firing at double clicks instead of single clicks was a matter of seconds.

Here's the full code in case someone finds it useful.

using System;
using System.Reflection;
using System.Threading;
using System.Windows;
using System.Windows.Input;

namespace Manas.Silverlight {

    /// <summary>
    /// Allows attaching onself to multiple click events in a component, instead 
    /// of sticking with Silverlight single click support only.
    /// 
    /// Usage:
    /// ^^^^^^
    /// MultipleClicks clicks = new MultipleClicks(someUiElmenet);
    /// clicks.MouseLeftButtonDown += (sender, e) => {
    ///   Console.WriteLine("Number of clicks: " + e.Clicks);
    /// };
    /// 
    /// You can limit the number of clicks allowed over an element:
    /// 
    /// MultipleClicks clicks = new MultipleClicks(someUiElmenet, 2);
    /// clicks.MouseLeftButtonDown += (sender, e) => {
    ///   Console.WriteLine("Number of clicks: " + e.Clicks); // allways 1 or 2, no more
    /// };
    /// 
    /// If you are only planning on supporting double click, this is faster 
    /// since if you don't specify a limit, a little time will be wait to see 
    /// if there's a third click before firing the event. If you do specify a 
    /// limit, as soon as the limit is reached, the event will be fired.
    /// 
    /// XAML
    /// ^^^^
    /// You can attach event handlers to a XAML by using the attached 
    /// property Clicks:
    /// 
    /// manas:Mouse.Click="SomeMethod"
    /// 
    /// or
    /// 
    /// manas:Mouse.Click="SomeMethod, limit" 
    /// 
    /// where limit must be an integer, and, of course, SomeMethod must have 
    /// the following signature:
    /// 
    /// public void SomeMethod(object sender, MouseButtonExtendedEventArgs e)
    /// </summary>
    public class Mouse
    {

        #region XAML support
        public static readonly DependencyProperty Click = DependencyProperty.RegisterAttached("Click", typeof(string), typeof(Mouse), null);

        public static void SetClick(DependencyObject obj,
            string handler)
        {
            obj.SetValue(Click, handler);

            // Check if a limit was specified
            int limit = -1;
            int indexOfComma = handler.IndexOf(',');
            if (indexOfComma != -1)
            {
                if (!int.TryParse(handler.Substring(indexOfComma + 1)
                    .Trim(), out limit))
                {
                    throw new ArgumentException("Syntax for Clicks is 'handler[, limit]', but the specified limit isn't a number. It was: '" + limit + "'");
                }

                handler = handler.Substring(0, indexOfComma).Trim();
            }

            EventSupport.AttachEvent(obj, handler, new Type[] { typeof(object), typeof(MouseButtonExtendedEventArgs) }, (sender, target, info) =>
                {
                    Mouse clicks =
                        new Mouse(sender as UIElement, limit);
                    clicks.MouseLeftButtonDown += (sender2, e2) =>
                    {
                        info.Invoke(target,
                            new object[] { sender2, e2 });
                    };
                });
        }

        public static string GetClick(DependencyObject obj)
        {
            return (string)obj.GetValue(Click);
        }
        #endregion

        // How much time to wait before firing the event
        private const int Threshold = 400;

        public event EventHandler<MouseButtonExtendedEventArgs> MouseLeftButtonDown;

        private Timer timer;
        private int clickCount;
        private int limit;

        /// Allows attaching event fired on multiple clicks over the given 
        /// element without limiting the maximum number of clicks.
        /// </summary>
        public Mouse(UIElement elem) : this(elem, -1)
        {
        }

        /// <summary>
        /// Allows attaching event fired on multiple clicks over the 
        /// given element limiting the maximum number of clicks.
        /// </summary>
        public Mouse(UIElement elem, int limit)
        {
            this.limit = limit;
            elem.MouseLeftButtonDown += new MouseButtonEventHandler(elem_MouseLeftButtonDown);
        }

        private void elem_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            clickCount++;

            if (clickCount == limit)
            {
                Fire(sender, e, clickCount);
                return;
            }

            if (timer == null)
            {
                timer = new Timer((state) =>
                {
                    // If there's a limit set and it was reached, then this 
                    // method will be invoked anyway, so only fire it if there's
                    // no limit or the limit was not yet reached
                    if (limit == -1 || clickCount < limit)
                    {
                        Fire(sender, e, clickCount);
                    }

                    // Reset everything for the next click group
                    clickCount = 0;
                    timer.Dispose();
                    timer = null;
                }, null, Threshold, Timeout.Infinite);
            }
            else
            {
                timer.Change(Threshold, Timeout.Infinite);
            }
        }

        // Fires the MouseLeftButtonDown event, *without* setting the 
        // timer to null
        private void Fire(object sender, MouseButtonEventArgs e, int clicks)
        {
            if (MouseLeftButtonDown != null)
            {
                (sender as UIElement).Dispatcher.BeginInvoke(() =>
                {
                    MouseLeftButtonDown(sender, new MouseButtonExtendedEventArgs()
                    {
                        MouseButtonEventArgs = e,
                        Clicks = clicks
                    });
                });
            }
        }
    }

    public class MouseButtonExtendedEventArgs : EventArgs
    {

        /// <summary>
        /// The original MouseButtonEventArgs.
        /// </summary>
        public MouseButtonEventArgs MouseButtonEventArgs { get; set; }

        /// <summary>
        /// The number of clicks. This is allways >= 1.
        /// </summary>
        public int Clicks { get; set; }

    }

    /// <summary>
    /// Indicates how to attach an event to the given target.
    /// </summary>
    /// <param name="sender">the object that fired the event</param>
    /// <param name="target">the object to which an event must be attached</param>
    /// <param name="method">the method to invoke in the event handler</param>
    public delegate void EventAttacher(object sender,
        DependencyObject target, MethodInfo method);

    /// <summary>
    /// Support for attaching to an event based on the name of a method.
    /// </summary>
    public static class EventSupport
    {

        /// <summary>
        /// Attaches an event to the first Parent of obj (which must be a 
        /// FrameworkElement) which declares a public method with the name 
        /// "handler" and arguments of types "types".
        /// 
        /// When that method is found, attacher is invoked with:
        /// - sender is "obj"
        /// - target is the Parent that declares the method
        /// - method is the method of Parent
        /// </summary>
        /// <param name="obj">the object to which to attach an event</param>
        /// <param name="handler">the name of the method to be found in some Parent of obj</param>
        /// <param name="attacher">indicates how to attach an event to the target</param>
        public static void AttachEvent(DependencyObject obj, string handler, Type[] types, EventAttacher attacher)
        {
            FrameworkElement fe = obj as FrameworkElement;
            if (fe == null)
            {
                throw new ArgumentException("Can only attach events to FrameworkElement instances, not to '" + obj.GetType() + "'");
            }

            fe.Loaded += (sender, e) =>
            {
                DependencyObject parent = sender as DependencyObject;

                MethodInfo info = null;
                while (info == null)
                {
                    info = parent.GetType().GetMethod(handler, types);
                    if (info != null)
                    {
                        attacher(sender, parent, info);
                        return;
                    }

                    if (parent is FrameworkElement)
                    {
                        parent = (parent as FrameworkElement).Parent;
                    }
                    else
                    {
                        parent = null;
                    }

                    if (parent == null)
                    {
                        throw new ArgumentException("Can't find handler '" + handler + "' (maybe it's not public?)");
                    }
                }
            };
        }

    }

}