I am creating a behavior for Keypress as I have a specific requirement for triggering a command when a key is pressed in an application. I am not using the WPF KeyBinding as I have several user controls with all having their respective ViewModels. I wrote the below behavior which works fine :-
public class WindowKeyPressBehavior : Behavior<Control>
{
#region Public Properties.
public KeyCommandCollection KeyCommandCollection
{
get => (KeyCommandCollection)GetValue(KeyCommandCollectionProperty);
set => SetValue(KeyCommandCollectionProperty, value);
}
public static readonly DependencyProperty KeyCommandCollectionProperty =
DependencyProperty.Register("KeyCommandCollection",
typeof(KeyCommandCollection),
typeof(WindowKeyPressBehavior),
new PropertyMetadata(null));
#endregion
#region Constructors.
public WindowKeyPressBehavior()
{
KeyCommandCollection = new KeyCommandCollection();
}
#endregion
#region Protected Method Declarations.
protected override void OnAttached()
{
base.OnAttached();
WeakEventManager<Control, RoutedEventArgs>.RemoveHandler(AssociatedObject, nameof(AssociatedObject.Loaded), AssociatedObject_Loaded);
WeakEventManager<Control, RoutedEventArgs>.AddHandler(AssociatedObject, nameof(AssociatedObject.Loaded), AssociatedObject_Loaded);
WeakEventManager<Control, RoutedEventArgs>.RemoveHandler(AssociatedObject, nameof(AssociatedObject.Unloaded), AssociatedObject_Unloaded);
WeakEventManager<Control, RoutedEventArgs>.AddHandler(AssociatedObject, nameof(AssociatedObject.Unloaded), AssociatedObject_Unloaded);
}
#endregion
#region Private Method Declarations.
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
WeakEventManager<Window, KeyEventArgs>.RemoveHandler(Application.Current.MainWindow, nameof(Application.Current.MainWindow.PreviewKeyDown), MainWindow_PreviewKeyDown);
WeakEventManager<Window, KeyEventArgs>.AddHandler(Application.Current.MainWindow, nameof(Application.Current.MainWindow.PreviewKeyDown), MainWindow_PreviewKeyDown);
LoadDataContexts();
}
private void AssociatedObject_Unloaded(object sender, RoutedEventArgs e)
{
UnloadDataContexts();
WeakEventManager<Window, KeyEventArgs>.RemoveHandler(Application.Current.MainWindow, nameof(Application.Current.MainWindow.PreviewKeyDown), MainWindow_PreviewKeyDown);
}
private void MainWindow_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (KeyCommandCollection == null || !KeyCommandCollection.Any(x => x.Key == e.Key))
{
return;
}
try
{
IEnumerable<KeyElement> keyCommands = KeyCommandCollection.Where(x => x.Key == e.Key);
foreach (KeyElement keyCommand in keyCommands)
{
// Run it in an asynchronous way as CanExecute/Execute can be time consuming in certain cases
// preventing the next command to run.
Application.Current?.Dispatcher?.Invoke(() =>
{
KeyElement command = keyCommand;
// If parameterized command is available execute it.
if (command.ParameterizedCommand?.CanExecute(command.CommandParameter) == true)
{
command.ParameterizedCommand.Execute(command.CommandParameter);
}
// If non-parameterized command is available, execute it.
else if (command.Command?.CanExecute() == true)
{
command.Command.Execute();
}
});
}
}
catch (System.Exception)
{
//TODO : Log Any Errors.
}
}
private void LoadDataContexts()
{
foreach (KeyElement keyElement in KeyCommandCollection.Where(x => x?.DataContext == null))
{
keyElement.DataContext = AssociatedObject?.DataContext;
}
}
private void UnloadDataContexts()
{
foreach (KeyElement keyElement in KeyCommandCollection.Where(x => x?.DataContext != null))
{
keyElement.DataContext = null;
}
}
#endregion
}
The KeyElement class inherits from FrameworkElement as below
public class KeyElement : FrameworkElement
{
public Key Key
{
get => (Key)GetValue(KeyProperty);
set => SetValue(KeyProperty, value);
}
public static readonly DependencyProperty KeyProperty =
DependencyProperty.Register(
"Key",
typeof(Key),
typeof(KeyElement),
new PropertyMetadata(System.Windows.Input.Key.None));
public DelegateCommand Command
{
get => (DelegateCommand)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register(
"Command",
typeof(DelegateCommand),
typeof(KeyElement),
new PropertyMetadata(null));
public DelegateCommand<object> ParameterizedCommand
{
get => (DelegateCommand<object>)GetValue(ParameterizedCommandProperty);
set => SetValue(ParameterizedCommandProperty, value);
}
public static readonly DependencyProperty ParameterizedCommandProperty =
DependencyProperty.Register(
"ParameterizedCommand",
typeof(DelegateCommand<object>),
typeof(KeyElement),
new PropertyMetadata(null));
public object CommandParameter
{
get => GetValue(CommandParameterProperty);
set => SetValue(CommandParameterProperty, value);
}
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register(
"CommandParameter",
typeof(object),
typeof(KeyElement),
new PropertyMetadata(null));
public override string ToString()
{
return $"Key : {Key}";
}
}
/// <summary>
/// Collection of Key-Commands.
/// </summary>
public class KeyCommandCollection : ObservableCollection<KeyElement>
{
}
My XAML file is as below :-
<UserControl
x:Class="Test.Views.LanguageSelectionPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:prism="http://prismlibrary.com/"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:behaviors="clr-namespace:Test.Behaviors;assembly=Test.UI.Core"
mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="1024"
d:DesignWidth="1280">
<i:Interaction.Behaviors>
<behaviors:WindowKeyPressBehavior>
<behaviors:WindowKeyPressBehavior.KeyCommandCollection>
<behaviors:KeyElement
Key="{Binding KioskWorkflow.PrmConfiguration.UpKey}"
ParameterizedCommand="{Binding SwitchCommand}"
CommandParameter="{Binding KioskWorkflow.PrmConfiguration.UpKey}" />
<behaviors:KeyElement
Key="{Binding KioskWorkflow.PrmConfiguration.DownKey}"
ParameterizedCommand="{Binding SwitchCommand}"
CommandParameter="{Binding KioskWorkflow.PrmConfiguration.DownKey}" />
<behaviors:KeyElement
Key="{Binding KioskWorkflow.PrmConfiguration.EnterKey}"
Command="{Binding SubmitCommand}" />
</behaviors:WindowKeyPressBehavior.KeyCommandCollection>
</behaviors:WindowKeyPressBehavior>
</i:Interaction.Behaviors>
<Grid></Grid>
</UserControl>
The Problem I have
I am able to run the application properly and everything seems to be working. The only reason I am unhappy is that I don't want the KeyElement to be a FrameworkElement. Instead I want it to be a plain DependencyObject. Problem I am facing with this being a Dependency Object is that I don't get the DataContext property because of which I am not able to set the Binding properly in Code.
Also I don't want to call the LoadDataContexts method in the behavior.
I tried using the BindingProxy approach but I don't like that approach as well as it needs me to declare a static resource in every page.
I have tried making the class KeyElement inherit from Freezable but that is not helping me either, as Binding is not happening, and even though it is freezable, it is not inheriting the DataContext from the parent i.e. the Behavior/UserControl. Tried several options by using RelativeSource but the DataContext is always null.
Also I get the below error in Output if I replace the FrameworkElement with Freezable:-
System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element
I had a look at the KeyBinding class and it does not inherit from FrameworkElement still does not throw any error in output window.
Any way of achieving the behavior without using a FrameworkElement ?
Any suggestions on improving the above code is also welcome.