Reputation: 8631
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!");
}
}
Upvotes: 3
Views: 3900
Reputation: 1778
I am not sure what you really want to achieve yet. Is the single click only to change the header, while the double click is to do something with a DataGrid-Entry?
If that's the case you can easy distinguish between the two so you don't handle the wrong case anyways (depending on the handler you choose):
MouseDown:
OnMouseDownDown(object sender, MouseButtonEventArgs e)
{
UIElement uiElement = (UIElement) sender;
Point hitPoint = e.GetPosition(uiElement);
HitTestResult hitTestResult = VisualTreeHelper.HitTest(uiElement, hitPoint);
if (hitTestResult == null)
{
return;
}
Visual DataGridHeader= hitTestResult.VisualHit.FindParent<DataGridHeader>();
}
Click: Use the VisualTreeHelper
to get the first parent of [Whatever type you look for.] of the eventArgs.OriginalSource
OnHandleMouseClick(object sender, RoutedEventArgs e)
{
var target = e.OriginalSource.FindVisualParent<DataGridHeader>();
}
If you only want to distinguish between single and double click (maybe combined with the hittesting you can still use MouseDown
and look at the ClickCount
in the EventArgs
. This count handles clicks within a given timeframe and visual rectangle to differentiate between single and double clicks.
Hope this helps.
EDIT------------------------------------------------------>
I built a small sample after you commented. You can download it here: http://1drv.ms/1kgnCeQ
I was looking for a solution levereging a DispatcherTimer, when I found another question at StackOverflow showing exactly that. Although I came up with the right idea, you can find the source for the code used here: WPF: Button single click + double click issue.
That should be what you asked for. If you need help with F2 or the ContextMenu, just comment. :)
All the best Gope
EDIT2----------------------------------------------------->
I've created a new sample handling all your requirements: http://1drv.ms/1kgnCeQ It contains a simple ContextMenu, as well as the F2 editing in addition to the former requirements. Its pretty raw in terms of ExceptionHandling, etc. but it shows how you can handle all the cases. That should do it. I also added a dataGrid.CancelEdit in the DoubleClick part to reset the editmode. :)
Here is the code from the sample to make it easier for other reading this answer:
MainWindow.xaml:
<Window x:Class="IDevign.DataGrids.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dataGrids="clr-namespace:IDevign.DataGrids"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid x:Name="dataGrid" AutoGenerateColumns="False" MouseDoubleClick="Button_MouseDoubleClick" PreviewKeyDown="DataGrid_OnPreviewKeyDown">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridCell}">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="DataGridCell_PreviewMouseLeftButtonDown"></EventSetter>
</Style>
</DataGrid.Resources>
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Header="Edit" Click="MenuItem_OnClick"></MenuItem>
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTextColumn Header="Recording Name" Binding="{Binding Name}"/>
<DataGridTextColumn Header="Duration" Binding="{Binding Duration}" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
MainWindow.xaml.cs:
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private static DispatcherTimer myClickWaitTimer = new DispatcherTimer(
new TimeSpan(0, 0, 0, 1),
DispatcherPriority.Background,
mouseWaitTimer_Tick,
Dispatcher.CurrentDispatcher);
public MainWindow()
{
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
ObservableCollection<Song> songs = new ObservableCollection<Song>
{
new Song {Duration = 3.11, Name = "Best song ever"},
new Song {Duration = 3.33, Name = "2nd best song ever"},
new Song {Duration = 3.02, Name = "3rd best song ever"}
};
dataGrid.ItemsSource = songs;
}
private void Button_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
// Stop the timer from ticking.
myClickWaitTimer.Stop();
dataGrid.CancelEdit();
MessageBox.Show("DoubleClicked");
e.Handled = true;
}
private static void mouseWaitTimer_Tick(object sender, EventArgs e)
{
myClickWaitTimer.Stop();
}
/// <summary>
/// Handles the LeftButton Click
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void DataGridCell_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
myClickWaitTimer.Start();
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;
}
}
}
}
}
/// <summary>
/// KeyDown Handler for F2 Key
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void DataGrid_OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (dataGrid.SelectedItem == null || e.Key != Key.F2)
{
return;
}
DataGridRow selectedRow = dataGrid.ItemContainerGenerator.ContainerFromItem(dataGrid.SelectedItem) as DataGridRow;
if (selectedRow == null)
{
return;
}
DataGridCell cell = GetCell(dataGrid, selectedRow, 0);
if (cell != null)
{
cell.Focus();
dataGrid.BeginEdit();
e.Handled = true;
}
}
/// <summary>
/// ContextMenu
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MenuItem_OnClick(object sender, RoutedEventArgs e)
{
DataGridRow selectedRow = dataGrid.ItemContainerGenerator.ContainerFromItem(dataGrid.SelectedItem) as DataGridRow;
if (selectedRow == null)
{
return;
}
DataGridCell cell = GetCell(dataGrid, selectedRow, 0);
if (cell != null)
{
cell.Focus();
dataGrid.BeginEdit();
e.Handled = true;
}
}
/// <summary>
/// Helper Method to get a cell by columnIndex
/// </summary>
/// <param name="dataGrid"></param>
/// <param name="rowContainer"></param>
/// <param name="column"></param>
/// <returns></returns>
public static DataGridCell GetCell(DataGrid dataGrid, DataGridRow rowContainer, int column)
{
if (rowContainer != null)
{
DataGridCellsPresenter presenter = FindVisualChild<DataGridCellsPresenter>(rowContainer);
if (presenter == null)
{
/* if the row has been virtualized away, call its ApplyTemplate() method
* to build its visual tree in order for the DataGridCellsPresenter
* and the DataGridCells to be created */
rowContainer.ApplyTemplate();
presenter = FindVisualChild<DataGridCellsPresenter>(rowContainer);
}
if (presenter != null)
{
DataGridCell cell = presenter.ItemContainerGenerator.ContainerFromIndex(column) as DataGridCell;
if (cell == null)
{
/* bring the column into view
* in case it has been virtualized away */
dataGrid.ScrollIntoView(rowContainer, dataGrid.Columns[column]);
cell = presenter.ItemContainerGenerator.ContainerFromIndex(column) as DataGridCell;
}
return cell;
}
}
return null;
}
#region VisualTreeHelper Methods
private static T FindVisualChild<T>(DependencyObject obj) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is T)
return (T) child;
else
{
T childOfChild = FindVisualChild<T>(child);
if (childOfChild != null)
return childOfChild;
}
}
return null;
}
private 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;
}
#endregion
}
An last the Song class:
class Song : INotifyPropertyChanged
{
private string name;
private double duration;
public string Name
{
get { return name; }
set
{
name = value;
OnPropertyChanged();
}
}
public double Duration
{
get { return duration; }
set
{
duration = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Upvotes: 8