I have coded a WPF UserControl with a DataGrid. I added the ability to edit one column (RecordingName) using "Single Click Editing" (see: my code and http://wpf.codeplex.com/wikipage?title=Single-Click%20Editing). I am also handling the MouseDoubleClick event for the entire DataGrid.
It works... sort of... You can certainly edit the column in question (RecordingName) and when you double click anywhere other than that column, all is well. It's when you double click on that column that you problems. It's not too surprising (to me). You are trying to capture a double click, but you are also looking at the single click (via the PreviewMouseLeftButtonDown event).
I assume this is a common problem. Can someone advice me on the best way to handle this? I absolutely need to support double click, but it would be nice to also be able to edit the RecordingName with single click editing.
I'd also like to support editing the RecordingName by right clicking on it and picking rename, and by selecting it with F2. This is the behavior you see if you go to windows explorer. If you select the file and then left click on it, you are in edit(rename) mode. If you quickly double click on it, the file is launched. If you right click, or select and hit F2 you can rename it.
Thanks for any help or ideas. I've pasted the code below. I did try to truncate it to the bare minimum. It's still quite a bit of code. For better or worse, I used a MVVM model for the control itself.
Here's the control's xaml:
<UserControl x:Class="StackOverFlowExample.RecordingListControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:StackOverFlowExample"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<ResourceDictionary>
<Style TargetType="{x:Type DataGridCell}">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="DataGridCell_PreviewMouseLeftButtonDown"></EventSetter>
</Style>
<Style x:Key="CellViewStyle" TargetType="{x:Type Label}" BasedOn="{StaticResource {x:Type Label}}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="BorderThickness" Value="1" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type DataGrid}" >
<Setter Property="Foreground" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:RecordingListControl}}, Path=Foreground}" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- style to apply to DataGridTextColumn in edit mode -->
<Style x:Key="CellEditStyle" TargetType="{x:Type TextBox}">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="CellNonEditStyle" TargetType="{x:Type TextBlock}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</UserControl.Resources>
<Grid >
<Grid Name="LayoutRoot">
<DataGrid Name="MainDataGrid" IsEnabled="{Binding Path=IsEnabled}" ItemsSource="{Binding Path=Recordings}" Margin="5" SelectionChanged="ListBox_SelectionChanged" MouseDoubleClick="DataGrid_MouseDoubleClick" AutoGenerateColumns="False" >
<DataGrid.Columns>
<DataGridTextColumn Header="#" IsReadOnly="True" Binding="{Binding RecordingNumber}">
<DataGridTextColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="HorizontalAlignment"
Value="Center" />
</Style>
</DataGridTextColumn.HeaderStyle>
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTemplateColumn SortMemberPath="RecordingName" Header="Recording Name" CanUserSort="True">
<DataGridTemplateColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="HorizontalContentAlignment"
Value="Center" />
</Style>
</DataGridTemplateColumn.HeaderStyle>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Label Content ="{Binding RecordingName, ValidatesOnDataErrors=True, UpdateSourceTrigger=LostFocus}" Foreground="Black" Style="{StaticResource CellViewStyle}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding RecordingName, ValidatesOnDataErrors=True, UpdateSourceTrigger=LostFocus}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Time" CanUserSort="False" IsReadOnly="True" Binding="{Binding TotalTime, StringFormat=mm\\:ss}">
<DataGridTextColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="HorizontalContentAlignment"
Value="Center" />
</Style>
</DataGridTextColumn.HeaderStyle>
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="End Time" IsReadOnly="True" SortMemberPath="EndTime" Binding="{Binding EndTime,StringFormat={}\{0:dd/MM/yyyy HH:mm\}}">
<DataGridTextColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="HorizontalContentAlignment"
Value="Center" />
</Style>
</DataGridTextColumn.HeaderStyle>
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
</DataGrid.Columns>
<DataGrid.Resources>
</DataGrid.Resources>
<DataGrid.ContextMenu >
<ContextMenu DataContext="{Binding Path=PlacementTarget, RelativeSource={RelativeSource Self}}">
<MenuItem
Header="Delete Recording"
Command="{Binding Path=DataContext.DeleteRecordingCommand}"
CommandParameter="{Binding Path=SelectedItem}"/>
</ContextMenu>
</DataGrid.ContextMenu>
</DataGrid>
</Grid>
</Grid>
and here's the code behind (need this for dependency properties. I'm not aware of another way)
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace StackOverFlowExample
{
/// <summary>
/// Interaction logic for UserControl1.xaml
/// </summary>
public partial class RecordingListControl : UserControl
{
public delegate void SelectionEventHandler(object sender, RecordingInfo info);
public event SelectionEventHandler DoubleClickEvent;
public RecordingListViewModel vm = new RecordingListViewModel();
public RecordingListControl()
{
InitializeComponent();
LayoutRoot.DataContext = vm;
}
#region Dependency property for SelectedItem
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(RecordingInfo), typeof(RecordingListControl));
public RecordingInfo SelectedItem
{
get { return (RecordingInfo)GetValue(SelectedItemProperty); }
set
{
SetValue(SelectedItemProperty, value);
}
}
#endregion
public static FrameworkPropertyMetadata md = new FrameworkPropertyMetadata(new PropertyChangedCallback(OnSomeCallback));
public static readonly DependencyProperty SomeDependencyProperty =
DependencyProperty.Register("SomeDependency", typeof(bool), typeof(RecordingListControl), md);
public bool SomeDependency
{
get { return (bool)GetValue(SomeDependencyProperty); }
set { SetValue(SomeDependencyProperty, value); }
}
private static void OnSomeCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
RecordingListControl ctrl = (RecordingListControl)d;
ctrl.vm.PopulateGrid();
}
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems != null && e.AddedItems.Count > 0)
SelectedItem = e.AddedItems[0] as RecordingInfo;
else
SelectedItem = null;
}
private void DataGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (MainDataGrid.SelectedItem == null || SelectedItem == null)
return;
if (DoubleClickEvent != null)
{
DoubleClickEvent(sender, SelectedItem);
}
}
// from: http://wpf.codeplex.com/wikipage?title=Single-Click%20Editing
// SINGLE CLICK EDITING
//
private void DataGridCell_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
DataGridCell cell = sender as DataGridCell;
if (cell != null && !cell.IsEditing && !cell.IsReadOnly)
{
if (!cell.IsFocused)
{
cell.Focus();
}
DataGrid dataGrid = FindVisualParent<DataGrid>(cell);
if (dataGrid != null)
{
if (dataGrid.SelectionUnit != DataGridSelectionUnit.FullRow)
{
if (!cell.IsSelected)
cell.IsSelected = true;
}
else
{
DataGridRow row = FindVisualParent<DataGridRow>(cell);
if (row != null && !row.IsSelected)
{
row.IsSelected = true;
}
}
}
}
}
static T FindVisualParent<T>(UIElement element) where T : UIElement
{
UIElement parent = element;
while (parent != null)
{
T correctlyTyped = parent as T;
if (correctlyTyped != null)
{
return correctlyTyped;
}
parent = VisualTreeHelper.GetParent(parent) as UIElement;
}
return null;
}
}
}
The ViewModel:
public class RecordingInfo
{
public string RecordingName { get; set; }
public int RecordingNumber { get; set; }
public TimeSpan? TotalTime { get; set; }
public DateTime? EndTime { get; set; }
}
public class RecordingListViewModel : ViewModelBase
{
private ObservableCollection<RecordingInfo> _recordings = null;
private string _patientId;
private int _sessionNumber;
BackgroundWorker _workerThread = new BackgroundWorker();
public RecordingListViewModel()
{
_workerThread.DoWork += new DoWorkEventHandler(workerThread_DoWork);
_workerThread.RunWorkerCompleted += new
RunWorkerCompletedEventHandler(workerThread_RunWorkerCompleted);
}
public ObservableCollection<RecordingInfo> Recordings
{
get
{
return _recordings;
}
}
bool _isEnabled = false;
public bool IsEnabled
{
get
{
return _isEnabled;
}
private set
{
if (value != _isEnabled)
{
_isEnabled = value;
OnPropertyChanged("IsEnabled");
}
}
}
public void PopulateGrid()
{
_workerThread.RunWorkerAsync(); // this is overkill in this demo project...
}
private void workerThread_DoWork(object sender, DoWorkEventArgs e)
{
_recordings = new ObservableCollection<RecordingInfo>();
RecordingInfo info1 = new RecordingInfo() { TotalTime = new TimeSpan(100), EndTime = DateTime.Now, RecordingName = "recording 1", RecordingNumber = 1 };
_recordings.Add(info1);
RecordingInfo info2= new RecordingInfo() { TotalTime = new TimeSpan(10000), EndTime = new DateTime(1999,2,2), RecordingName = "recording 2", RecordingNumber = 2 };
_recordings.Add(info2);
RecordingInfo info3 = new RecordingInfo() { TotalTime = new TimeSpan(7000), EndTime = new DateTime(2008, 2, 2), RecordingName = "recording 3", RecordingNumber = 3};
_recordings.Add(info3);
}
private void workerThread_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
OnPropertyChanged("Recordings");
IsEnabled = true;
}
public void SaveRecording(RecordingInfo info)
{
}
private RecordingInfo _selectedItem = null;
public RecordingInfo SelectedItem
{
get { return _selectedItem; }
set
{
if (value == _selectedItem)
return;
// verify that selected item is actully in our collection of recordings!
if (!_recordings.Contains(value))
throw new ApplicationException("Selected item not in collection");
_selectedItem = value;
OnPropertyChanged("SelectedItem");
// selection changed - do something special
}
}
private ICommand _deleteRecordingCmd = null;
public ICommand DeleteRecordingCommand
{
get
{
if (_deleteRecordingCmd == null)
{
_deleteRecordingCmd = new RelayCommand(param => DeleteRecordingCommandImplementation(param));
}
return _deleteRecordingCmd;
}
}
/// <summary>
/// I used ideas from this post to get Delete working:
/// http://stackoverflow.com/questions/19447795/command-bind-to-contextmenu-which-on-listboxitem-in-listbox-dont-work
/// </summary>
/// <param name="note"></param>
private void DeleteRecordingCommandImplementation(object recording)
{
if (_recordings != null && _recordings.Count > 0 && recording is RecordingInfo)
{
if (_recordings.Contains(recording as RecordingInfo))
{
_recordings.Remove(recording as RecordingInfo);
}
OnPropertyChanged("Recordings");
}
}
}
And the MainWindow xaml and code behind:
<Window x:Class="StackOverFlowExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StackOverFlowExample"
Title="MainWindow" Height="350" Width="525">
<Grid>
<local:RecordingListControl x:Name="_ctrl"></local:RecordingListControl>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
_ctrl.SomeDependency = true;
_ctrl.DoubleClickEvent += _ctrl_DoubleClickEvent;
}
void _ctrl_DoubleClickEvent(object sender, RecordingInfo info)
{
MessageBox.Show("You double clicked me!");
}
}