Implementing Double-Click In Silverlight DataGrid

As I was working on one of my presentations, I wanted to go to a detail from from query form by letting user double-click.  No standard Silverlight controls implement double-clicks, and this includes DataGrid.  So, naturally one would need to listen to click events and interpret clicks within a certain time to be a double-click.  In my case I will use 300 milliseconds.  Another fact to consider is that if you try to do hook up double-click to the DataGrid itself, you will be disappointed because LeftMouseButtonUp event only fires when there are no rows to get in the way.  So, to make this work we will have to use each row’s mouse events.  I am going to wrap this up in a behavior first.  I am going to call this behavior  DataGridMouseLeftButtonDownCommandBehavior.  Here is what my class looks like, and it is very simple.  As you see, I am inheriting from CommandBase from Prism (Composite Application Guidance for Silverlight/WPF).

using System;
using Microsoft.Practices.Composite.Presentation.Commands;
using System.Windows.Controls;
using System.Windows.Input;
 
namespace CompanyModule.Converters
{
    /// <summary>
    /// Inherit from command base from Prism
    /// </summary>
    public class DataGridMouseLeftButtonDownCommandBehavior : CommandBehaviorBase<DataGrid>
    {
        /// <summary>
        /// Last time mouse was clicked
        /// </summary>
        private DateTime _lastTime;
 
        /// <summary>
        /// Number of milliseconds to interpret as doible-click
        /// </summary>
        private const long MillisesondsForDoubleClick = 300;
 
        /// <summary>
        /// Create new instance
        /// </summary>
        /// <param name="dataGrid">DataGrid to attach behavior to</param>
        public DataGridMouseLeftButtonDownCommandBehavior(DataGrid dataGrid)
            : base(dataGrid)
        {
            dataGrid.LoadingRow += OnLoadingRow;
            dataGrid.UnloadingRow += OnUnloadingRow;
        }
 
        /// <summary>
        /// Hook up mouse events
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnUnloadingRow(object sender, DataGridRowEventArgs e)
        {
            e.Row.MouseLeftButtonUp -= OnMouseLeftButtonDown;
        }
 
        /// <summary>
        /// Unhook events to avoid memory leaks
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnLoadingRow(object sender, DataGridRowEventArgs e)
        {
            e.Row.MouseLeftButtonUp += OnMouseLeftButtonDown;
        }
 
 
        private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            // if timing is right, fire command
            if ((DateTime.Now.Subtract(_lastTime).TotalMilliseconds) < MillisesondsForDoubleClick)
            {
                ExecuteCommand();
            }
            // reset the time
            _lastTime = DateTime.Now;
        }
    }
}

 

Now that we have behavior of the way, we need to come up with a clean way to hook it up as well as setting a command and command parameter onto a DataGrid.

I am going to use a static class and attached properties.  Attached properties are a feature of Silverlight that would allow us to set properties on one object via properties defined on another object.  Most common example of them is Grid control in Silverlight.  If you add a child control to a Grid, you set its location by using Grid.Column or Grid.Row on child control itself, such as Textbox.  Properties that I am going to need are Command and CommandParameter as well as a property I can attach my behavior to.  I am again using Prism for my project.  Here is what my attached properties look like:

    public static class DataGridDoubleClick
    {
        private static readonly DependencyProperty DataGridDoubleClickCommandBehaviorProperty = 
            DependencyProperty.RegisterAttached(
                "DataGridDoubleClickCommandBehavior",
                typeof(DataGridMouseLeftButtonDownCommandBehavior),
                typeof(DataGridDoubleClick),
                null);
 
        public static readonly DependencyProperty CommandProperty = 
            DependencyProperty.RegisterAttached(
                "Command",
                typeof(ICommand),
                typeof(DataGridDoubleClick),
                new PropertyMetadata(OnSetCommand));
 
        public static readonly DependencyProperty CommandParameterProperty = 
            DependencyProperty.RegisterAttached(
               "CommandParameter",
               typeof(object),
               typeof(DataGridDoubleClick),
               new PropertyMetadata(OnSetCommandParameter));

 

I am defining call backs in order to be able to attach my behavior to the DataGrid in those.  The reason I cannot just put behavior inside XAML is because I need to pass DataGrid to my behavior as a parameter.  I am using attached property for the behavior because this makes it easy to store in static environment.

Here is how I am hooking up behavior in call back methods.


        private static void OnSetCommand
            (DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            DataGrid dataGrid = dependencyObject as DataGrid;
            if (dataGrid != null)
            {
                DataGridMouseLeftButtonDownCommandBehavior behavior = GetBehavior(dataGrid);
                behavior.Command = e.NewValue as ICommand;
            }
        }
 
        private static void OnSetCommandParameter
            (DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            DataGrid dataGrid = dependencyObject as DataGrid;
            if (dataGrid != null)
            {
                DataGridMouseLeftButtonDownCommandBehavior behavior = GetBehavior(dataGrid);
                behavior.CommandParameter = e.NewValue;
            }
        }

 

The final part is how to set the behavior.  Here is the short method for it:

        private static DataGridMouseLeftButtonDownCommandBehavior GetBehavior(DataGrid dataGrid)
        {
            DataGridMouseLeftButtonDownCommandBehavior behavior =
                dataGrid.GetValue(DataGridDoubleClickCommandBehaviorProperty) 
                    as DataGridMouseLeftButtonDownCommandBehavior;
            if (behavior == null)
            {
                behavior = new DataGridMouseLeftButtonDownCommandBehavior(dataGrid);
                dataGrid.SetValue(DataGridDoubleClickCommandBehaviorProperty, behavior);
            }
            return behavior;
        }

 

Now the final step – XAML for my data grid:

<UserControl
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
   xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   xmlns:my="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"  
   xmlns:command="clr-namespace:Microsoft.Practices.Composite.Presentation.Commands;
           assembly=Microsoft.Practices.Composite.Presentation"

   xmlns:converters="clr-namespace:CompanyModule.Converters"
   xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
   x:Class="CompanyModule.Views.CompanyListView"
   mc:Ignorable="d"
   d:DesignHeight="300" d:DesignWidth="400">
    <UserControl.Resources>
        <converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White" HorizontalAlignment="Stretch">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Button Content="Get Companies" command:Click.Command="{Binding GetCompaniesCommand}"/>
        <my:DataGrid 
           Grid.Row="1" 
           ItemsSource="{Binding CompanyList}" 
           HorizontalAlignment="Stretch"
           converters:DataGridDoubleClick.Command="{Binding Path=DoubleClickCommad}"
           converters:DataGridDoubleClick.CommandParameter="{Binding RelativeSource={RelativeSource Self},
             
Path=SelectedItem}"
           SelectionMode="Single">
           
        </my:DataGrid>
        <Rectangle Grid.RowSpan="2" 
                  Fill="LightBlue" 
                  Visibility="{Binding IsBusy, Converter={StaticResource BooleanToVisibilityConverter}}" 
                  Opacity="0.3"/>
    </Grid>
</UserControl>

 

Here is what my command looks like in ViewModel:

public DelegateCommand<object> DoubleClickCommad { get; private set; }

DoubleClickCommad = new DelegateCommand<object>(SelectCompanyForEdit);

 

private void SelectCompanyForEdit(object load)
{
  if (load != null && load is Company)
    {
      MessageBox.Show(((Company)load).CompanyName);
    }
}

 

And that is all there is to it.

Thanks.

9 Comments

  1. Simpler way, does not use prism, and can be attached to any UIElement

    public static class DoubleClickBehaviour
    {
    public static DependencyProperty CommandDoubleClickProperty = DependencyProperty.RegisterAttached(“CommandDoubleClick”,
    typeof(ICommand),
    typeof(DoubleClickBehaviour),
    new PropertyMetadata(null, OnCommandDoubleClickChanged));

    public static ICommand GetCommandDoubleClick(DependencyObject obj)
    {
    return (ICommand)obj.GetValue(CommandDoubleClickProperty);
    }

    public static void SetCommandDoubleClick(DependencyObject obj, ICommand value)
    {
    obj.SetValue(CommandDoubleClickProperty, value);
    }

    private static void OnCommandDoubleClickChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
    UIElement f = d as UIElement;
    f.MouseLeftButtonDown += new MouseButtonEventHandler(f_MouseLeftButtonDown);
    }

    private static void TimerCallback(object state)
    {
    _t.Dispose();
    _t = null;
    }

    static Timer _t;
    static void f_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
    if (_t == null)
    {
    _t = new Timer(TimerCallback);
    _t.Change(300, 0);
    return;
    }

    // Load the command property
    UIElement f = sender as UIElement;
    GetCommandDoubleClick(f).Execute( null);
    }
    }

  2. This looks great but it’s not working for me. Looks like the command can’t be parsed in the xaml. I’m using Silverlight 4 and Prism 4.

    Error that I’m getting:
    =======================================
    The property ‘Command’ does not exist on the type ‘DataGrid’ in the XML namespace ‘clr-namespace:Mullivan.Silverlight.Converters’

  3. To make this work in Silverlight4 just add:

    public static void SetDataGridDoubleClickCommandBehavior(UIElement element, DataGridMouseLeftButtonDownCommandBehavior value)
    {
    element.SetValue(DataGridDoubleClickCommandBehaviorProperty, value);
    }
    public static DataGridMouseLeftButtonDownCommandBehavior GetDataGridDoubleClickCommandBehavior(UIElement element)
    {
    return (DataGridMouseLeftButtonDownCommandBehavior)element.GetValue(DataGridDoubleClickCommandBehaviorProperty);
    }

    public static void SetCommand(UIElement element, ICommand value)
    {
    element.SetValue(CommandProperty, value);
    }
    public static ICommand GetCommand(UIElement element)
    {
    return (ICommand)element.GetValue(CommandProperty);
    }

    public static void SetCommandParameter(UIElement element, object value)
    {
    element.SetValue(CommandParameterProperty, value);
    }
    public static object GetCommandParameter(UIElement element)
    {
    return element.GetValue(CommandParameterProperty);
    }

    to the DataGridDoubleClick class.

Leave a Reply to Ruben Cancel reply

Your email address will not be published. Required fields are marked *