Enter-to-Tab in WPF
Like many users of line-of-business applications, my own users just can't understand the Tab key. For them, pressing Enter on the keyboard shouldn't submit a form, it should just move their focus to the next control.
Back in the Windows Forms days there was a neat way to achieve this functionality: You caught the KeyDown event on each of your controls and used the SelectNextControl method to move focus to the next control. In WPF, however, there's no SelectNextControl method! So how do we do something similar?
The first trick to use in WPF is to handle the PreviewKeyDown event on all your data-entry controls in one hit, rather than having to declaratively handle it individually in each control. In my case I have all my controls inside a grid, so I simply declare my grid like this:
<Grid UIElement.PreviewKeyDown="Grid_PreviewKeyDown">
What I'm doing here is telling the grid that any child control that descends from UIElement (and that includes TextBoxes and ComboBoxes) should have their PreviewKeyDown event handled by a common method called Grid_PreviewKeyDown.
So now we have caught key presses on our controls. Now, how do we tell the window to move its focus to the next available control? We don't have SelectNextControl, but we have a new method with a similar function: MoveFocus!
private void Grid_PreviewKeyDown(object sender, KeyEventArgs e)
{
var uie = e.OriginalSource as UIElement;
if (e.Key == Key.Enter)
{
e.Handled = true;
uie.MoveFocus(
new TraversalRequest(
FocusNavigationDirection.Next));
}
}
So here we're getting the control that sent us the PreviewKeyDown event, and calling its MoveFocus method to shift the focus to the next control!
So now you have no excuses! WPF can be used for line-of-business style applications after all!
# Trackback from Enter to Tab as an Attached Property on 18/08/2008 11:57 AM
Comments
# Paul Stovell
25/03/2008 6:07 PM
Good example Matt. I wonder if it could be implemented as an attached dependency property:
<TextBox extended:KeyboardNavigation.EnterActsAsTab="True" />
In the setter method, you could get the control, then wire up the event handler.
Then you could set it via styles on the various editable controls.
# mabster
25/03/2008 6:23 PM
To be honest, Paul, I wondered myself whether it could be an attached property but wasn't sure how to implement it.
I also wonder whether it could be an "attached event". Perhaps catch PreviewKeyDown on ALL UIElements but then check if the attached "EnterActsAsTab" property is true before running the event handler code.
# Kashif
14/05/2008 7:55 PM
I am using Infragistics third party controls. Would these elements added to the UIElement collection as well?
# Emmanuel Huna
26/09/2008 6:42 AM
Private Sub LayoutRoot_PreviewKeyDown(ByVal sender As Object, ByVal e As KeyEventArgs)
Dim oUIElement As UIElement
Dim bIsMultiline As Boolean = False
Dim oTextBox As Controls.TextBox
If e.Key = Key.Enter Then
Try
oUIElement = CType(e.OriginalSource, UIElement)
Try
oTextBox = CType(oUIElement, Controls.TextBox)
If stit(UCase(oTextBox.Tag)) = cEDITORMULTILINE Then bIsMultiline = True
Catch ex As Exception
bIsMultiline = False
End Try
If Not (bIsMultiline) Then
e.Handled = True
oUIElement.MoveFocus(New TraversalRequest(FocusNavigationDirection.Next))
End If
Catch ex As Exception
End Try
End If
End Sub
# Emad
12/08/2009 5:21 AM
Here's a much simpler method stackoverflow.com/.../interpret-enter
# atervan
10/10/2009 7:58 PM
I us this solution and solved my problem.
thank very much ...
# kishore
28/01/2010 7:43 PM
oh dear... thanks alot....
# LanMi
4/03/2011 2:53 AM
...simple as that
Thank you !
I tried this example
private void MyGrid_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
DataGridRow rowContainer = (DataGridRow)dgMyGrid.ItemContainerGenerator.ContainerFromItem(dgMyGrid.CurrentItem);
if (rowContainer != null)
{
DataGridCellsPresenter presenter = GetVisualChild<DataGridCellsPresenter>(rowContainer);
int columnIndex = dgMyGrid.Columns.IndexOf(dgMyGrid.CurrentColumn);
DataGridCell cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
TraversalRequest request = new TraversalRequest(FocusNavigationDirection.Next);
request.Wrapped = true;
cell.MoveFocus(request);
rowContainer = (DataGridRow)dgMyGrid.ItemContainerGenerator.ContainerFromItem(dgMyGrid.CurrentItem);
dgMyGrid.SelectedItem = dgMyGrid.CurrentItem;
e.Handled = true;
dgMyGrid.UpdateLayout();
}
}
}
public static T GetVisualChild<T>(Visual parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
and i could not get that to work
# Ahmed
3/10/2012 5:06 PM
Wow ! I searched alot about that topic
you helped me alot
thanksssssssssssssss :)
# Ali Shahbaz
8/02/2013 4:05 PM
Thanks, I exactly want this. You solve my problem, great article :)
# James
11/07/2013 12:24 AM
This does not act as a tab at all, if you create a page of buttons, it doesnt tab between them, like a tab button WOULD do
# Yun
19/11/2013 12:24 AM
Thanks Matt! You help me a lot! And I have another question: how to get datagridcell into edit mode directly?
# mabster
19/11/2013 7:10 AM
I've never used a DataGrid in WPF, Yun, so I can't really help. Stack Overflow is a great place to post these sorts of questions.
# Lucas
22/01/2014 11:41 AM
Public Shared Function GetKeysAsTab(ByVal element As DependencyObject) As List(Of System.Windows.Input.Key)
If element Is Nothing Then
Throw New ArgumentNullException("element")
End If
Return CType(element.GetValue(KeysAsTabProperty), List(Of Global.System.Windows.Input.Key))
End Function
Public Shared Sub SetKeysAsTab(ByVal element As DependencyObject, ByVal value As List(Of System.Windows.Input.Key))
If element Is Nothing Then
Throw New ArgumentNullException("element")
End If
element.SetValue(KeysAsTabProperty, value)
End Sub
Public Shared ReadOnly KeysAsTabProperty As _
DependencyProperty = DependencyProperty.RegisterAttached("KeysAsTab", _
GetType(List(Of System.Windows.Input.Key)), GetType(AttachedProperties), _
New PropertyMetadata(New List(Of System.Windows.Input.Key), AddressOf EnterAsTabProperty_Changed))
Private Shared Sub EnterAsTabProperty_Changed(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim element As UIElement = TryCast(sender, UIElement)
If element Is Nothing Then
Throw New ArgumentException("sender", "parameter NULL or wrong type")
End If
If e.NewValue IsNot Nothing Then
element.AddHandler(UIElement.KeyDownEvent, New RoutedEventHandler(AddressOf UIElementKeyAsTab_KeyDown))
Else
element.RemoveHandler(UIElement.KeyDownEvent, New RoutedEventHandler(AddressOf UIElementKeyAsTab_KeyDown))
End If
End Sub
Private Shared Sub UIElementKeyAsTab_KeyDown(sender As Object, e As RoutedEventArgs)
Dim element As UIElement = TryCast(sender, UIElement)
If element Is Nothing Then
Throw New ArgumentException("sender", "parameter NULL or wrong type")
End If
Dim args As KeyEventArgs = CType(e, KeyEventArgs)
Dim keys As List(Of Windows.Input.Key) = GetKeysAsTab(element)
If keys.Contains(args.Key) Then
args.Handled = True
' Act like a tab key and move to the next location
element.MoveFocus(New TraversalRequest(FocusNavigationDirection.Next))
End If
End Sub
then do;
<Behaviours:AttachedProperties.KeysAsTab>
<x:Static Member="input:Key.Enter"/>
<x:Static Member="input:Key.Down"/>
</Behaviours:AttachedProperties.KeysAsTab>
boom :)
# Lucas
22/01/2014 11:58 AM
I should really review stuff before posting :)
I had to create an IList(Of Key) collection to make run-time and xaml happy
Namespace Behaviours
Public Class KeyCollection
Inherits List(Of System.Windows.Input.Key)
End Class
Public Class AttachedProperties
Public Shared Function GetKeysAsTab(ByVal element As DependencyObject) As KeyCollection
If element Is Nothing Then
Throw New ArgumentNullException("element")
End If
Return CType(element.GetValue(KeysAsTabProperty), KeyCollection)
End Function
Public Shared Sub SetKeysAsTab(ByVal element As DependencyObject, ByVal value As KeyCollection)
If element Is Nothing Then
Throw New ArgumentNullException("element")
End If
element.SetValue(KeysAsTabProperty, value)
End Sub
Public Shared ReadOnly KeysAsTabProperty As _
DependencyProperty = DependencyProperty.RegisterAttached("KeysAsTab", _
GetType(KeyCollection), GetType(AttachedProperties), _
New FrameworkPropertyMetadata(New KeyCollection, AddressOf EnterAsTabProperty_Changed))
Private Shared Sub EnterAsTabProperty_Changed(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim element As UIElement = TryCast(sender, UIElement)
If element Is Nothing Then
Throw New ArgumentException("sender", "parameter NULL or wrong type")
End If
If e.NewValue IsNot Nothing Then
element.AddHandler(UIElement.PreviewKeyDownEvent, New RoutedEventHandler(AddressOf UIElementKeyAsTab_KeyDown))
Else
element.RemoveHandler(UIElement.PreviewKeyDownEvent, New RoutedEventHandler(AddressOf UIElementKeyAsTab_KeyDown))
End If
End Sub
Private Shared Sub UIElementKeyAsTab_KeyDown(sender As Object, e As RoutedEventArgs)
Dim element As UIElement = TryCast(sender, UIElement)
If element Is Nothing Then
Throw New ArgumentException("sender", "parameter NULL or wrong type")
End If
Dim args As KeyEventArgs = CType(e, KeyEventArgs)
Dim keys As KeyCollection = GetKeysAsTab(element)
If keys.Contains(args.Key) Then
args.Handled = True
' Act like a tab key and move to the next location
element.MoveFocus(New TraversalRequest(FocusNavigationDirection.Next))
End If
End Sub
#End Region
and my XAML looks like
<Behaviours:AttachedProperties.KeysAsTab>
<Behaviours:KeyCollection>
<x:Static Member="input:Key.Enter"/>
<x:Static Member="input:Key.Down"/>
</Behaviours:KeyCollection>
</Behaviours:AttachedProperties.KeysAsTab>