更新内容:
1. Tab/Enter跳转到下一列;
2. 最后一行最后一列,Tab/Enter自动增加新行;
3. 增加新行后,自动跳转到新增行的第一列;
4. 删除行后,自动选中上一行;
5. Up/Down/Left/Right自动编辑;
实现方式:
1. Tab/Enter跳转到下一列
从google搜到的一篇文章《Datagrid cell navigation with enter key》,文中最开始提到在DataGrid的KeyDown事件中进行处理。但是DataGrid里面做了一些封装,我们在自己注册(通过+=操作符来登记、或者XAML中定义)的KeyDown事件中,截获不到Enter/Tab等键。所以最后原作者有提出一个思路:继承DataGrid,重新OnKeyDown方法。
但我之前的思路,是通过扩展方法来扩展DataGrid,再来重写就有点儿郁闷了。于是找出Reflector,翻了下DataGrid的实现(reflector中copy出来的):
1: // happyhippy.cnblogs.com
2: // 构造函数
3: public DataGrid()
4: {
5: //省略
6: base.KeyDown += new KeyEventHandler(this.DataGrid_KeyDown);
7: base.KeyUp += new KeyEventHandler(this.DataGrid_KeyUp);
8: //省略
9: }
10:
11: private void DataGrid_KeyDown(object sender, KeyEventArgs e)
12: {
13: if (!e.Handled)
14: {
15: e.Handled = this.ProcessDataGridKey(e);
16: }
17: }
18:
19: private bool ProcessDataGridKey(KeyEventArgs e)
20: {
21: bool flag = false;
22: switch (e.Key)
23: {
24: case Key.Tab:
25: return this.ProcessTabKey(e);
26:
27: case Key.Enter:
28: flag = this.ProcessEnterKey();
29: break;
30:
31: case Key.Escape:
32: return this.ProcessEscapeKey();
33:
34: case Key.PageUp:
35: flag = this.ProcessPriorKey();
36: break;
37:
38: case Key.PageDown:
39: flag = this.ProcessNextKey();
40: break;
41:
42: case Key.End:
43: flag = this.ProcessEndKey();
44: break;
45:
46: case Key.Home:
47: flag = this.ProcessHomeKey();
48: break;
49:
50: case Key.Left:
51: flag = (base.FlowDirection == FlowDirection.LeftToRight) ? this.ProcessLeftKey() : this.ProcessRightKey();
52: break;
53:
54: case Key.Up:
55: flag = this.ProcessUpKey();
56: break;
57:
58: case Key.Right:
59: flag = (base.FlowDirection == FlowDirection.LeftToRight) ? this.ProcessRightKey() : this.ProcessLeftKey();
60: break;
61:
62: case Key.Down:
63: flag = this.ProcessDownKey();
64: break;
65:
66: case Key.Insert:
67: return this.ProcessCopyKey();
68:
69: case Key.A:
70: return this.ProcessAKey();
71:
72: case Key.C:
73: return this.ProcessCopyKey();
74:
75: case Key.F2:
76: return this.ProcessF2Key(e);
77: }
78: if (flag && base.IsTabStop)
79: {
80: base.Focus();
81: }
82: return flag;
83: }
我们可以发现,KeyDown中已经定义一部分按键的处理,并更新了KeyEventArgs.Handled属性。MSDN对该属性的解释是:
将事件标记为已处理将会限制路由事件在事件路由过程中对于侦听器的可见性。事件将仍然进行路由过程的其余部分,但只会在响应中调用在 AddHandler(RoutedEvent, Delegate, Boolean) 方法调用中明确添加的其 HandledEventsToo 为 true 的处理程序。 将不会调用实例侦听器上的默认处理程序(比如用可扩展应用程序标记语言 (XAML) 表示的那些处理程序)。处理标记为已处理的事件这种情形并不常见。 如果您是定义您自己的事件的控件作者,则您在类级别所做的有关事件处理的决策将影响您的控件的用户,以及派生控件的任何用户,并可能会影响您的控件所包含或包含您的控件的其他元素。有关更多信息,请参见 将路由事件标记为“已处理”和“类处理”。 在极少数情况下,适合处理其中 Handled 标记为 true 的事件,并通过将 Handled 更改为 false 来修改事件参数。 在控件输入事件的某些区域(比如 KeyDown 与 TextInput 键处理)中,低级别和高级别的输入事件会争用处理,并且每个事件都会尝试使用不同的路由策略,因此,可能必须要这样做。 |
也就是说,在将KeyEventArgs.Handled,我们通过+=来自己注册的处理程序,就无法执行了。一种替代思路就是在不要用+=来注册处理程序,而是调用AddHandler(RoutedEvent, Delegate, Boolean) 方法来注册处理程序:
1: //之前的处理方式,将监听不到已经处理过的Tab/Enter等键
2: //dataGrid.KeyDown += KeyDownHandler<T>; //注册按键事件
3: //只能通过下列方式来注册
4: dataGrid.AddHandler(DataGrid.KeyDownEvent, new KeyEventHandler(KeyDownHandler<T>), true);
但在使用的时候发现,如果当前Cell(TextBlock)处于编辑状态,要按两次Tab/Enter才能跳转到下一列:第一次提交更改,第二次才执行跳转操作。
回过头来看,上面的DataGrid的部分源码,跟进去看了下KeyUp事件的处理,发现KeyUp并没有去更改KeyEventArgs.Handled属性。因此,我们可以考虑在DataGrid上注册自定义KeyUp事件,来做一些额外处理:
1: dataGrid.KeyUp += KeyUpHandler<T>;
2:
3: private static void KeyUpHandler<T>(object sender, KeyEventArgs e) where T : class, new()
4: {
5: DataGrid dataGrid = sender as DataGrid;
6: switch (e.Key)
7: {
8: case Key.Tab:
9: goto case Key.Enter;
10: case Key.Enter:
11: if (dataGrid.CurrentColumn != null)
12: {
13: if (dataGrid.IsLastRow() & dataGrid.IsLastCell()) // Last Row And Last Cell (End of the grid)
14: {
15: dataGrid.AddEntity<T>();
16: }
17: else if (dataGrid.IsLastCell()) // Last Cell
18: {
19: dataGrid.SelectedIndex = dataGrid.SelectedIndex + 1;
20: dataGrid.NextCellToEdit();
21: }
22: else // Normal navigation
23: {
24: dataGrid.NextCellToEdit();
25: }
26: }
27: break;
28: default:
29: break;
30: }
2. 最后一行最后一列,Tab/Enter自动增加新行
能捕捉到Tab/Enter后,处理就比较简单了;只需判断时候在最后一行、最后一列,如果是,则调用上一篇文章中的增加新行方法:
1: if (dataGrid.IsLastRow() & dataGrid.IsLastCell())
2: {
3: dataGrid.AddEntity<T>();
4: }
3. 增加新行后,自动跳转到新增行的第一列
可以尝试在后台代码中,将DataGrid聚焦在新增行的第一列上:
1: dataGrid.SelectedItem = entity;
2: dataGrid.CurrentColumn = dataGrid.Columns[0];
3: dataGrid.NextCellToEdit();
但是在实际使用过程中,可能会遇到如下异常:
这是因为,新增加的行,可能(有时能成功,有时可能失败)还没有来得及在UI显示出来,如果在这时设置SelectedItem/CurrentColumn,并调用ScrollIntoView,就会出现上面的异常。一种代替思路,就是用一个计时器来进行处理,如果执行失败,则下次接着执行,直到成功为止:
1: public static void ScrolToFirstColumn(this DataGrid dataGrid, object entity)
2: {
3: DispatcherTimer timer = new DispatcherTimer();
4: timer.Interval = TimeSpan.FromTicks(100);
5: object sucess = false;
6: timer.Tick += (o, e) =>
7: {
8: if (!(bool)sucess)
9: {
10: dataGrid.SelectedItem = entity;
11: dataGrid.CurrentColumn = dataGrid.Columns[0];
12: dataGrid.NextCellToEdit();
13: sucess = true;
14: timer.Stop(); //执行完毕后,就不再执行。
15: }
16: };
17: timer.Start();
18: }
4. 删除行后,自动选中上一行
默认情况下,行被删除行后,DataGrid没有选中任何行。如果想连续删除、或者用Up/Down/Left/Right跳转的话,就需要每次用鼠标点一下,着实不方便。于是提供了此功能:
1: public static void DeleteCurrentRow(this DataGrid dataGrid)
2: {
3: object entity = dataGrid.SelectedItem;
4: IList dataSource = dataGrid.ItemsSource as IList;
5: if (entity == null || dataSource == null || dataSource.Count == 0)
6: {
7: return;
8: }
9:
10: int selectedIndex = dataGrid.SelectedIndex; //删除前,记录当前行的位置
11: if (ExtenderInfo[dataGrid].Deleteable)
12: {
13: dataSource.Remove(entity);
14: dataGrid.SelectedIndex = selectedIndex - 1;//删除后,重置当前行
15: }
16: dataGrid.BeginEdit();
17: ExtenderInfo[dataGrid].RaiseRowChangedEvent(dataGrid,
18: new EntityChangedEventArgs() { ChangedEntities = new List<object> { entity }, Type = "Delete" });
19: }
5. Up/Down/Left/Right自动编辑
上一篇文章中,通过在DataGrid的KeyDown事件中,见按键记录到DataGrid.Tag中,然后进入编辑状态时再赋值给单元格中的文本框(TextBox),来实现即时编辑功能。但在使用的时候,发现如果使用中文输入法、或者列使用的是自定义模板列(不是TextBox),就会遇到问题:
如果是中文输入法+默认的TextBox:DataGrid会将第一个字符赋给TextBox,这样就多出一个字符,每次得多按一次BackSpace键。
如果是使用自定义模板列,而不是TextBox,就会丢失第一次字符按键。
一种替代的思路,就是在捕捉Up/Down/Left/Right后,让DataGrid立即进入编辑状态。如上面第1点所述,还是要在KeyUp事件中进行处理。