wpf - InputBindings funciona solo cuando se enfoca
mvvm user-controls (3)
Diseñé un usercontrol reutilizable. Contiene UserControl.InputBindings. Es bastante simple, ya que solo contiene una etiqueta y un botón (y nuevas propiedades, etc.)
Cuando uso el control en mi ventana, funciona bien. Pero la vinculación de teclas solo funciona cuando se enfoca. Cuando un control tiene un enlace a alt + f8, este atajo solo funciona cuando está enfocado. Cuando se enfoca el otro con su propio enlace, ese funciona pero alt + f8 no más. Cuando ninguno de los controles tiene el foco, nada funciona.
¿Cómo puedo lograr que mi usercontrol defina combinaciones de teclas en toda la ventana?
Especialmente siguiendo el patrón de diseño de MVVM (se usa Caliburn.Micro) pero se agradece cualquier ayuda.
El XAML del control de usuario:
<UserControl x:Class="MyApp.UI.Controls.FunctionButton"
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:MyApp.UI.Controls"
xmlns:cm="http://www.caliburnproject.org"
x:Name="Root"
Focusable="True"
mc:Ignorable="d"
d:DesignHeight="60" d:DesignWidth="120">
<UserControl.Resources>
...
</UserControl.Resources>
<UserControl.InputBindings>
<KeyBinding Key="{Binding ElementName=Root, Path=FunctionKey}" Modifiers="{Binding ElementName=Root, Path=KeyModifiers}" Command="{Binding ElementName=Root, Path=ExecuteCommand}" />
</UserControl.InputBindings>
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Top" Text="{Binding ElementName=Root, Path=HotkeyText}" />
<Button DockPanel.Dock="Bottom" Content="{Binding ElementName=Root, Path=Caption}" cm:Message.Attach="[Event Click] = [Action ExecuteButtonCommand($executionContext)]" cm:Action.TargetWithoutContext="{Binding ElementName=Root}" />
</DockPanel>
</UserControl>
Ejemplo de uso:
<Grid>
<c:FunctionButton Width="75" Height="75" Margin="10,10,0,0" VerticalAlignment="Top" HorizontalAlignment="Left" FunctionKey="F1" ShiftModifier="True" cm:Message.Attach="[Event Execute] = [Action Button1Execute]" />
<c:FunctionButton Width="75" Height="75" Margin="10,90,0,0" VerticalAlignment="Top" HorizontalAlignment="Left" FunctionKey="F2" ShiftModifier="True" cm:Message.Attach="[Event Execute] = [Action Button2Execute]" />
</Grid>
Como dije, cada botón funciona (Ejecutar se dispara) al hacer clic con el mouse y cuando estoy enfocado puedo usar el espacio para activar el botón y funciona el enlace de entrada del botón enfocado, pero nunca del des-enfocado.
Sí, UserControl KeyBindings solo funcionará cuando el control tenga foco.
Si desea que KeyBinding funcione en la ventana, debe definirlo en la ventana. Lo haces en Windows XAML usando:
<Window.InputBindings>
<KeyBinding Command="{Binding Path=ExecuteCommand}" Key="F1" />
</Window.InputBindings>
Sin embargo, ha dicho que quiere que UserControl defina KeyBinding. No sé de ninguna manera de hacer esto en XAML, por lo que tendría que configurar esto en el código subyacente de UserControl. Eso significa encontrar la ventana padre del UserControl y crear KeyBinding
{
var window = FindVisualAncestorOfType<Window>(this);
window.InputBindings.Add(new KeyBinding(ViewModel.ExecuteCommand, ViewModel.FunctionKey, ModifierKeys.None));
}
private T FindVisualAncestorOfType<T>(DependencyObject d) where T : DependencyObject
{
for (var parent = VisualTreeHelper.GetParent(d); parent != null; parent = VisualTreeHelper.GetParent(parent)) {
var result = parent as T;
if (result != null)
return result;
}
return null;
}
El ViewModel.FunctionKey necesitaría ser del tipo Key en este caso, o de lo contrario necesitarás convertir de un string a type Key.
Tener que hacer esto en código subyacente en lugar de XAML no rompe el patrón MVVM. Todo lo que se está haciendo es mover la lógica de enlace de XAML a C #. ViewModel aún es independiente de View, y como tal puede ser probado por unidades sin instanciar la vista. Está absolutamente bien poner tal lógica específica de UI en el código subyacente de una vista.
No se ejecutarán InputBindings para un control que no esté enfocado debido a la forma en que funcionan: se busca en el árbol visual un manejador para el enlace de entrada desde el elemento enfocado al árbol visual (la ventana). Cuando un control no está enfocado, él no será parte de esa ruta de búsqueda.
Como @Wayne ha mencionado, la mejor manera de hacerlo sería simplemente mover los enlaces de entrada a la ventana principal. A veces, sin embargo, esto no es posible (por ejemplo, cuando UserControl no está definido en el archivo xaml de la ventana).
Mi sugerencia sería usar un comportamiento adjunto para mover estos enlaces de entrada del UserControl a la ventana. Hacerlo con un comportamiento adjunto también tiene el beneficio de poder trabajar en cualquier FrameworkElement
y no solo en UserControl. Entonces, básicamente, tendrás algo como esto:
public class InputBindingBehavior
{
public static bool GetPropagateInputBindingsToWindow(FrameworkElement obj)
{
return (bool)obj.GetValue(PropagateInputBindingsToWindowProperty);
}
public static void SetPropagateInputBindingsToWindow(FrameworkElement obj, bool value)
{
obj.SetValue(PropagateInputBindingsToWindowProperty, value);
}
public static readonly DependencyProperty PropagateInputBindingsToWindowProperty =
DependencyProperty.RegisterAttached("PropagateInputBindingsToWindow", typeof(bool), typeof(InputBindingBehavior),
new PropertyMetadata(false, OnPropagateInputBindingsToWindowChanged));
private static void OnPropagateInputBindingsToWindowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((FrameworkElement)d).Loaded += frameworkElement_Loaded;
}
private static void frameworkElement_Loaded(object sender, RoutedEventArgs e)
{
var frameworkElement = (FrameworkElement)sender;
frameworkElement.Loaded -= frameworkElement_Loaded;
var window = Window.GetWindow(frameworkElement);
if (window == null)
{
return;
}
// Move input bindings from the FrameworkElement to the window.
for (int i = frameworkElement.InputBindings.Count - 1; i >= 0; i--)
{
var inputBinding = (InputBinding)frameworkElement.InputBindings[i];
window.InputBindings.Add(inputBinding);
frameworkElement.InputBindings.Remove(inputBinding);
}
}
}
Uso:
<c:FunctionButton Content="Click Me" local:InputBindingBehavior.PropagateInputBindingsToWindow="True">
<c:FunctionButton.InputBindings>
<KeyBinding Key="F1" Modifiers="Shift" Command="{Binding FirstCommand}" />
<KeyBinding Key="F2" Modifiers="Shift" Command="{Binding SecondCommand}" />
</c:FunctionButton.InputBindings>
</c:FunctionButton>
<UserControl.Style>
<Style TargetType="UserControl">
<Style.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=keyPressPlaceHoler}" />
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Style>
keyPressPlaceHoler es el nombre del contenedor de su elemento objetivo
recuerde configurar Focusable = "True" en usercontrol