Enter to Tab as an Attached Property
Updated! As suggested by Eric Burke in the comments, I'm now handling the "Unloaded" event and unhooking the event handlers there, so the elements are properly cleaned up by the garbage collector. Thanks Eric!
A while ago I posted about a trick to make your WPF applications treat the enter key as a tab, and shift focus to the next available control. Paul commented that it should be possible to do that using an attached property, and I agreed, but didn't know how.
Well, now I do.
I'll post the full code for the property itself below, but first, its usage. Firstly you'll need to add an xmlns declaration to the top of your form pointing to the class' namespace. Usually this is just the namespace of your application, like WpfApplication1. I like to call this XML namespace "my". So your window's declaration ends up looking like:
<Window x:Class="WpfApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:my="clr-namespace:WpfApplication1" Title="Window1">
Next you want to find the container control (usually a Grid or StackPanel) in which all your data-entry controls live, and add the attached property to it, like this:
<StackPanel my:EnterKeyTraversal.IsEnabled="True">
Now all the controls within that StackPanel will treat the Enter key as a tab! Once the focus shifts out of the StackPanel the Enter key reverts to its normal behaviour, which is nice because the very next control will probably be an "OK" button, and the user will want to be able to press Enter to click it.
Here's the complete class declaration to give you the EnterKeyTraversal.IsEnabled property. Have fun!
public class EnterKeyTraversal { public static bool GetIsEnabled(DependencyObject obj) { return (bool)obj.GetValue(IsEnabledProperty); } public static void SetIsEnabled(DependencyObject obj, bool value) { obj.SetValue(IsEnabledProperty, value); } static void ue_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) { var ue = e.OriginalSource as FrameworkElement; if (e.Key == Key.Enter) { e.Handled = true; ue.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); } } private static void ue_Unloaded(object sender, RoutedEventArgs e) { var ue = sender as FrameworkElement; if (ue == null) return; ue.Unloaded -= ue_Unloaded; ue.PreviewKeyDown -= ue_PreviewKeyDown; } public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached("IsEnabled", typeof(bool),
typeof(EnterKeyTraversal), new UIPropertyMetadata(false, IsEnabledChanged)); static void IsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var ue = d as FrameworkElement; if (ue == null) return; if ((bool)e.NewValue) { ue.Unloaded += ue_Unloaded; ue.PreviewKeyDown += ue_PreviewKeyDown; } else { ue.PreviewKeyDown -= ue_PreviewKeyDown; } } }
Comments
# eric burke
18/08/2008 4:11 AM
don't forget to unhook that event handler when the UE is unloaded also, and not just when IsEnabled is set to false (if it's a FrameworkElement), otherwise you will leak and keep your whole window around. ;)
# mabster
18/08/2008 8:52 AM
Hi Eric,
Now you've got me worried! What's the best approach to do that? Keep a list of controls I've handled and then catch the Application.Exit event to unhook the handlers?
# eric burke
19/08/2008 12:27 AM
there are 3 approaches that i've seen. i'm sure others exist but these are the most common.
1. have the owner of the FrameworkElement you are working with explicitly set EnterKeyTraversal.IsEnabled="false" when the element is unloaded. this is ok, but it sort of defeats the purpose of the attached property.
2. have the helper class listen for Unloaded. your code is really close, just needs a small refactor.
+ write a function called HookHandlers() which listens for Unloaded as well as any other events you care about
+ write a function called UnhookHandlers() which removes all those event handlers, including Unloaded
+ when IsEnabled is set, call HookHandlers()
+ when IsEnabled is cleared, call UnhookHandlers()
+ when your Unloaded handler fires, call UnhookHandlers()
3. use the WeakEvent pattern. this works well, but requires you to have an instance of a class that can implement IWeakEventListener. it's more code than it's worth IMO.
attached properties are a double-edged sword. they make it super-easy to extend the functionality of built-in components, but they often aren't aware of object lifetimes.
for FrameworkElements, you can watch Unloaded, but for data objects, you either have to hold WeakReferences or somehow know when you can/should release your strong references.
as you might have guessed, i've spent waaay too much time tracking down why my Window (and all its data/visuals/etc) were leaking, only to find that i was holding a list of objects in a helper class, one of which was something in the Window. :(
# mabster
19/08/2008 8:52 AM
Thanks for the reply, Eric.
Which "Unloaded" event are you talking about here? UIElement doesn't seem to have one.
# mabster
19/08/2008 9:53 AM
Ah! Got it! I have changed the property to work with FrameworkElement instead of UIElement and used the Unloaded event to disconnect the handlers. Thanks Eric!
# HEV
16/07/2009 7:08 PM
1. Handling of "Unloaded" event may cause unexpected behaviour for this attached property. Especially if corresponding element is inside TabControl.
2. What "leak" are you trying to prevent? Where is it? Element (source of events) will be collected normally by GC without any handlers disconnection.
# HEV
16/07/2009 7:10 PM
P.S. Good stuff about Enter handling.
# Pete
10/04/2010 12:10 AM
Thanks!!!
# woodyiii
30/04/2010 7:17 AM
Hi,
I can't seem to get the IsEnabled property to change in the XAML once I've set it to "True".
If I set it back to "False" on a child element of say a <DockPanel>, then that child element doesn't register my changed value. I can see it register the DependencyProperty on load, but the value never changes. Should I not be handling this purely in the XAML?
Any thoughts? Thanks.
# mabster
30/04/2010 8:06 AM
Hi wodoyiii,
I haven't really tried setting IsEnabled back to False on a child element. I don't think it would work with my code, because all I do is handle the key events on the container element itself. It doesn't work with individual child elements.
If you need special handling for the Enter key on just one child element, you might have more success handling the PreviewKeyDown on that element and setting Handled to true.
# Bjarke
28/05/2010 1:24 AM
This was really useful. Thanks.
Here's a small update that allows you to do key traversal using a custom key gesture, and also allows you to navigate in both directions. Use it like:
<StackPanel
ns:CustomKeyTraversal.IsEnabled="True"
ns:CustomKeyTraversal.MoveNextKeyGesture="Ctrl+Down"
ns:CustomKeyTraversal.MovePreviousKeyGesture="Ctrl+Up"
>
...
</StackPanel>
public static class CustomKeyTraversal
{
#region IsEnabled Attached DP
public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached(
"IsEnabled",
typeof(bool),
typeof(CustomKeyTraversal),
new UIPropertyMetadata(false, IsEnabledChanged)
);
public static bool GetIsEnabled(DependencyObject obj)
{
return (bool)obj.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject obj, bool value)
{
obj.SetValue(IsEnabledProperty, value);
}
static void IsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ue = d as FrameworkElement;
if (ue == null) return;
if ((bool)e.NewValue)
{
ue.Unloaded += ue_Unloaded;
ue.PreviewKeyDown += ue_PreviewKeyDown;
}
else
{
ue.PreviewKeyDown -= ue_PreviewKeyDown;
}
}
#endregion
#region MovePreviousKeyGesture Attached DP
public static readonly DependencyProperty MovePreviousKeyGestureProperty = DependencyProperty.RegisterAttached(
"MovePreviousKeyGesture",
typeof(KeyGesture),
typeof(CustomKeyTraversal),
new UIPropertyMetadata(null, null)
);
public static KeyGesture GetMovePreviousKeyGesture(DependencyObject obj)
{
return (KeyGesture)obj.GetValue(MovePreviousKeyGestureProperty);
}
public static void SetMovePreviousKeyGesture(DependencyObject obj, KeyGesture value)
{
obj.SetValue(MovePreviousKeyGestureProperty, value);
}
#endregion
#region MoveNextKeyGesture Attached DP
public static readonly DependencyProperty MoveNextKeyGestureProperty = DependencyProperty.RegisterAttached(
"MoveNextKeyGesture",
typeof(KeyGesture),
typeof(CustomKeyTraversal),
new UIPropertyMetadata(null, null)
);
public static KeyGesture GetMoveNextKeyGesture(DependencyObject obj)
{
return (KeyGesture)obj.GetValue(MoveNextKeyGestureProperty);
}
public static void SetMoveNextKeyGesture(DependencyObject obj, KeyGesture value)
{
obj.SetValue(MoveNextKeyGestureProperty, value);
}
#endregion
static void ue_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
var ue = e.OriginalSource as FrameworkElement;
KeyGesture mpgesture = GetMovePreviousKeyGesture(sender as DependencyObject);
KeyGesture mngesture = GetMoveNextKeyGesture(sender as DependencyObject);
if (Keyboard.Modifiers.Equals(mpgesture.Modifiers) && e.Key == mpgesture.Key)
{
# Peter
21/09/2010 1:38 AM
Hi Matt,
very usefull class. Thank you!
Is there a solution to do the same in a datagrid?
# shakro
9/12/2011 3:12 AM
If you just need to make enter synonomous with tab then, traversal in both directions as the following.
static void ue_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
var ue = e.OriginalSource as FrameworkElement;
if (e.Key == Key.Enter)
{
if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
{
e.Handled = true;
ue.MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous));
}
else
{
e.Handled = true;
ue.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}
}
}
# bkstill
8/06/2012 7:45 AM
Thanks for sharing Matt. Here's the code translated to VB.
Imports System.Windows
Imports System.Windows.Input
Public Class EnterKeyTraversal
Public Shared Function GetIsEnabled(obj As DependencyObject) As Boolean
Return CBool(obj.GetValue(IsEnabledProperty))
End Function
Public Shared Sub SetIsEnabled(obj As DependencyObject, value As Boolean)
obj.SetValue(IsEnabledProperty, value)
End Sub
Private Shared Sub uePreviewKeyDown(sender As Object, e As System.Windows.Input.KeyEventArgs)
Dim ue = TryCast(e.OriginalSource, FrameworkElement)
If e.Key = Key.Enter Then
e.Handled = True
ue.MoveFocus(New TraversalRequest(FocusNavigationDirection.[Next]))
End If
End Sub
Private Shared Sub ueUnloaded(sender As Object, e As RoutedEventArgs)
Dim ue = TryCast(sender, FrameworkElement)
If ue Is Nothing Then
Return
End If
RemoveHandler ue.Unloaded, AddressOf ueUnloaded
RemoveHandler ue.PreviewKeyDown, AddressOf uePreviewKeyDown
End Sub
Public Shared ReadOnly IsEnabledProperty As DependencyProperty = DependencyProperty.RegisterAttached("IsEnabled", GetType(Boolean), GetType(EnterKeyTraversal), New UIPropertyMetadata(False, AddressOf IsEnabledChanged))
Private Shared Sub IsEnabledChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim ue = TryCast(d, FrameworkElement)
If ue Is Nothing Then
Return
End If
If CBool(e.NewValue) Then
AddHandler ue.Unloaded, AddressOf ueUnloaded
AddHandler ue.PreviewKeyDown, AddressOf uePreviewKeyDown
Else
RemoveHandler ue.PreviewKeyDown, AddressOf uePreviewKeyDown
End If
End Sub
End Class
# Raghavendra
4/07/2012 8:45 PM
Hi Mat,
I'm using the EnterKeyTraversal class for one text box in a view. it works for the first load. When I go from view1 to view2 and come back to view1 , Enter button wont work as tab in view1 and no method's are getting fired in EnterKeyTraversal class. Any suggestion?
# mabster
5/07/2012 8:22 AM
Hi Raghavendra,
That sounds a bit strange. Is "view1" always in memory, or are you creating a new instance of it when you return to it? Not that it should make any difference. :\
Matt
# Raghavendra
5/07/2012 6:41 PM
Hi Mat,
Yes View1 was always in memory. I resolved this issue by handling loaded event. Any how thanks for your help.
Regards,
Raghavendra SK
# John
13/07/2012 4:29 AM
Hi Mat/Raghavendra,
Please post the loaded event code that resolved your issue. I'm having the same problem.
Thanks,
John
# Csaba
28/08/2012 4:38 PM
Hello,
Very nice class, thanks for sharing.
I'm also facing the view change problem, so that Loaded event handler would be greatly appreciated.
Thanks,
Csaba
# Lauren
28/11/2012 12:42 AM
"
1. Handling of "Unloaded" event may cause unexpected behaviour for this attached property. Especially if corresponding element is inside TabControl.
2. What "leak" are you trying to prevent? Where is it? Element (source of events) will be collected normally by GC without any handlers disconnection."
I think this is what the above posters are running into with their View1 and View2. I have this same problem with a control inside a TabControl - when I handle the Unloaded, it removes the handlers, but doesn't re-attach them even when the tab becomes visible again.
Obviously I can't attach a loaded listener in the helper as it would be static too and cause the same leak I'm trying to prevent.
Has anyone figured a way to resolve this or do I have to use weak event references?
# Afzal
1/03/2013 11:14 AM
Hi Woodyiii
I changed it a bit I don't want that to handle Enter Key on Buttons to act like Tab Key So I changed a bit
static void ue_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.OriginalSource is Button && e.Key == Key.Enter)
{
return;
}
var ue = e.OriginalSource as FrameworkElement;
if (e.Key == Key.Enter)
{
e.Handled = true;
ue.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
}
}
# Mesta
25/04/2014 1:15 AM
This method is bugged:
private static void ue_Unloaded(object sender, RoutedEventArgs e)
{
var ue = sender as FrameworkElement;
if (ue == null) return;
ue.Unloaded -= ue_Unloaded;
ue.PreviewKeyDown -= ue_PreviewKeyDown;
}
You can see the wrong behaviour if you put a texbox in a tabcontrol.
Create 2 tabs, the first one don't contain any textobx, the 2nd one contains a textbox.
The result is that the attached property will work only the first time that you go in the 2nd tabitem, but for the next ones, it do now work anymore.
# Mesta
28/04/2014 11:54 PM
Well as i read posts before mine, the problem is still there after 5 years, because to fix it you should remove the unloaded event but in this way you are leaking resources.
In the end, the code in this page has bugs and should not be used, until the author will update it.
# Gerardo
24/05/2014 7:37 AM
#Mesta Attached only on MainGrid and it's works
<Grid my:EnterKeyTraversal.IsEnabled="True">
<TabControl>
<TabItem>
<Grid>
.....
</Grid>
</TabItem>
</TabControl>