Binding Convention for DataGridRow

Topics: Actions & Coroutines, Conventions, Feature Requests
Apr 11, 2012 at 7:32 PM

I can't figure out how to attach a Caliburn.Micro Action to the MouseDoubleClick event.

This is a very common UI technique:

You have a list of items in a datagrid and you want to do something with the Item in the row when the user double-clicks the row.

I've found a few examples to get this to work using custom behaviors, but it seems silly to have to write that much code when the following works:

In view XAML:

 

<DataGrid Name="Surveys"
                AutoGenerateColumns="False"
                HeadersVisibility="Column"
                IsReadOnly="True"
                SelectionMode="Single">
    <DataGrid.Columns>
        <DataGridTextColumn Width="*"
                            Binding="{Binding Path=SurveyQuestion}"
                            Header="Survey Question" />
    </DataGrid.Columns>
    <DataGrid.RowStyle>
        <Style TargetType="{x:Type DataGridRow}">
            <EventSetter Event="MouseDoubleClick"
                         Handler="SurveyRowDoubleClick" />
        </Style>
    </DataGrid.RowStyle>
</DataGrid>

 

In code behind:

 

private void SurveyRowDoubleClick(object sender, RoutedEventArgs e)
{
    IMyViewModel vm = (IMyViewModel)DataContext;
    vm.ActivateCurrentSurvey();
}

 

In IMyViewModel interface:

 

public interface IMyViewModel
{
    ICollection<Survey> Surveys { get; set; }

    Survey CurrentSurvey { get; set; }

    void AddSurvey();

    void DeleteCurrentSurvey();
    bool CanDeleteCurrentSurvey { get; }

    void ActivateCurrentSurvey();
    bool CanActivateCurrentSurvey { get; }

    void RefreshSurveys();
}

 

Caliburn,.Micro automatically binds to the Surveys and CurrentSurvey properties (how cool!), but I can't find a way without using code behind or a custom Behavior to fire the EditCurrentSurvey method.

How about a datagrid binding convention that binds the double-click event to an  ActivateCurrentXXX method?

Apr 13, 2012 at 7:57 AM
Edited Apr 13, 2012 at 8:05 AM

In the view with your DataGrid you add 

xmlns:Events="clr-namespace:Common.Controls.Events;assembly=Common"

and in your datagrid xaml you add

<DataGrid Name="Survey" ...
   Events:DoubleClickEvent.AttachAction="YourMethodInViewModel()"

and the DoubleClickEvent class looks like

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interactivity;
using Caliburn.Micro;

namespace Common.Controls.Events
{
   public static class DoubleClickEvent
   {
      public static readonly DependencyProperty AttachActionProperty =
      DependencyProperty.RegisterAttached(
      "AttachAction",
      typeof( string ),
      typeof( DoubleClickEvent ),
      new PropertyMetadata( OnAttachActionChanged ) );

      public static void SetAttachAction( DependencyObject d, string attachText )
      {
         d.SetValue( AttachActionProperty, attachText );
      }

      public static string GetAttachAction( DependencyObject d )
      {
         return d.GetValue( AttachActionProperty ) as string;
      }

      private static void OnAttachActionChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
      {
         if ( e.NewValue == e.OldValue )
            return;

         var text = e.NewValue as string;
         if ( string.IsNullOrEmpty( text ) )
            return;

         AttachActionToTarget( text, d );
      }

      private static void AttachActionToTarget( string text, DependencyObject d )
      {
         var actionMessage = Parser.CreateMessage( d, text );

         var trigger = new ConditionalEventTrigger
         {
            EventName = "MouseLeftButtonUp",
            Condition = e => DoubleClickCatcher.IsDoubleClick( d, e )
         };
         trigger.Actions.Add( actionMessage );

         Interaction.GetTriggers( d ).Add( trigger );
      }

      public class ConditionalEventTrigger : System.Windows.Interactivity.EventTrigger
      {
         public Func<EventArgs, bool> Condition { get; set; }

         protected override void OnEvent( EventArgs eventArgs )
         {
            if ( Condition( eventArgs ) )
               base.OnEvent( eventArgs );
         }
      }

      static class DoubleClickCatcher
      {
         private const int DoubleClickSpeed = 400;
         private const int AllowedPositionDelta = 6;

         static Point clickPosition;
         private static DateTime lastClick = DateTime.Now;
         private static bool firstClickDone;

         internal static bool IsDoubleClick( object sender, EventArgs args )
         {
            var element = sender as UIElement;
            var clickTime = DateTime.Now;

            var e = args as MouseEventArgs;
            if ( e == null )
               throw new ArgumentException( "MouseEventArgs expected" );

            var span = clickTime - lastClick;

            if ( span.TotalMilliseconds > DoubleClickSpeed || firstClickDone == false )
            {
               clickPosition = e.GetPosition( element );
               firstClickDone = true;
               lastClick = DateTime.Now;
               return false;
            }

            firstClickDone = false;
            var position = e.GetPosition( element );
            if ( Math.Abs( clickPosition.X - position.X ) < AllowedPositionDelta &&
            Math.Abs( clickPosition.Y - position.Y ) < AllowedPositionDelta )
               return true;
            return false;
         }
      }
   }
}

This is not exactly what you want but it is not really much code and works very well. Hope this will help you.

Apr 13, 2012 at 7:17 PM

Thanks a BUNCH madben!

This works if I force my CanActivateCurrentSurvey guard method to always return true.

The only exception seems to be if you double-click on the datagrid's scroll bar (not the scroll up-down buttons), the event is fired with no CurrentSurvey selected.  I can live with checking for CurrentSurvey != null in my ActivateCurrentSurvey method if I have to.

If I leave my guard method as...

public bool CanActivateCurrentSurvey
{
    get { return CurrentSurvey != null; }
}

... the datadrid is disabled (can't tab to or click on it).

I know I could just add a separate ActivateCurrentSurveyFromGrid method to my view model, but that kind puts knowledge of my view implemetation into the view model.

I have an Edit Survey button on the save view as the datagrid which I want disabled until they select an item in the grid.

I'd really like to use the same 'Command' for both the datadrid double-click and the button click events.

It is beginning to look like my best bet is to create my own subclass of DataGrid that exposes an ItemActivate event like the old WinForms ListView control does.  Then I could use a normal Caliburn Message.Attach to get my view and view model hooked up.

Apr 17, 2012 at 8:27 AM

In that case I do it with conventional binding. Just bind a property to the datagrid's SelectedItem and within this property's setter set the property which is bound to your Edit Survey button's enabled state.