再利用可能なユーザーコントロールを設計しました。 UserControl.InputBindingsが含まれています。ラベルとボタン(および新しいプロパティなど)のみが含まれているため、非常にシンプルです。
ウィンドウでコントロールを使用すると、うまく機能します。ただし、キーバインディングは、フォーカスされている場合にのみ機能します。 1つのコントロールにalt + f8へのバインドがある場合、このショートカットはフォーカスされている場合にのみ機能します。独自のバインディングを持つもう一方にフォーカスがある場合、その一方は機能しますが、alt + f8は機能しなくなります。どのコントロールにもフォーカスがない場合、何も機能しません。
ユーザーコントロールがウィンドウ全体のキーバインドを定義するようにするにはどうすればよいですか?
特にMVVMデザインパターン(Caliburn.Microを使用)に従いますが、助けていただければ幸いです。
ユーザーコントロールのXAML:
<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>
使用例:
<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>
前述のように、各ボタンはマウスクリックで機能し(実行が起動されます)、フォーカスされている場合はスペースを使用してボタンをアクティブにでき、フォーカスされたボタンの入力バインディングは機能しますが、フォーカスされていないボタンは機能しません。
InputBindingsは、その動作方法が原因でフォーカスされていないコントロールに対しては実行されません。入力バインディングのハンドラーは、フォーカスされた要素からビジュアルツリーのルート(ウィンドウ)までビジュアルツリーで検索されます。コントロールがフォーカスされていない場合、彼はその検索パスの一部にはなりません。
@Wayneが述べたように、最善の方法は、入力バインディングを親ウィンドウに移動することです。ただし、これが不可能な場合もあります(たとえば、ウィンドウのxamlファイルでUserControlが定義されていない場合)。
私の提案は、添付の動作を使用して、これらの入力バインディングをUserControlからウィンドウに移動することです。アタッチされた動作を使用してこれを行うと、UserControlだけでなく、任意のFrameworkElement
で作業できるという利点もあります。つまり、基本的には次のようなものになります。
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);
}
}
}
使用法:
<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 KeyBindingsは、コントロールにフォーカスがある場合にのみ機能します。
KeyBindingをウィンドウで機能させる場合は、ウィンドウ自体でKeyBindingを定義する必要があります。これは、WindowsXAMLで次を使用して行います。
<Window.InputBindings>
<KeyBinding Command="{Binding Path=ExecuteCommand}" Key="F1" />
</Window.InputBindings>
ただし、UserControlでKeyBindingを定義する必要があるとのことです。 XAMLでこれを行う方法がわからないため、UserControlのコードビハインドでこれを設定する必要があります。つまり、UserControlの親ウィンドウを見つけて、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;
}
この場合、ViewModel.FunctionKeyはKey型である必要があります。そうでない場合は、文字列からKey型に変換する必要があります。
XAMLではなくコードビハインドでこれを行う必要があるからといって、MVVMパターンが壊れることはありません。行われているのは、バインディングロジックをXAMLからC#に移動することだけです。 ViewModelは引き続きビューから独立しているため、ビューをインスタンス化せずに単体テストを行うことができます。このようなI固有ロジックをコードビハインドビューに配置することは絶対に問題ありません。
Adi LestersUnLoadedにサブスクライブ解除メカニズムを備えた添付の動作コードを拡張して、転送されたバインディングをクリーンアップしました。コントロールがビジュアルツリーを終了すると、InputBindingsがアクティブにならないように、ウィンドウから削除されます。 (添付プロパティでWPFトリガーを使用することは検討していません。)
このソリューションでは、コントロールがWPFによって再利用されるため、動作は切り離されません:Loaded/UnLoaded複数回呼び出されます。ビヘイビアーはFrameWorkElementへの参照を保持していないため、これによってリークが発生することはありません。
private static void OnPropagateInputBindingsToWindowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((FrameworkElement)d).Loaded += OnFrameworkElementLoaded;
((FrameworkElement)d).Unloaded += OnFrameworkElementUnLoaded;
}
private static void OnFrameworkElementLoaded(object sender, RoutedEventArgs e)
{
var frameworkElement = (FrameworkElement)sender;
var window = Window.GetWindow(frameworkElement);
if (window != null)
{
// transfer InputBindings into our control
if (!trackedFrameWorkElementsToBindings.TryGetValue(frameworkElement, out var bindingList))
{
bindingList = frameworkElement.InputBindings.Cast<InputBinding>().ToList();
trackedFrameWorkElementsToBindings.Add(
frameworkElement, bindingList);
}
// apply Bindings to Window
foreach (var inputBinding in bindingList)
{
window.InputBindings.Add(inputBinding);
}
frameworkElement.InputBindings.Clear();
}
}
private static void OnFrameworkElementUnLoaded(object sender, RoutedEventArgs e)
{
var frameworkElement = (FrameworkElement)sender;
var window = Window.GetWindow(frameworkElement);
// remove Bindings from Window
if (window != null)
{
if (trackedFrameWorkElementsToBindings.TryGetValue(frameworkElement, out var bindingList))
{
foreach (var binding in bindingList)
{
window.InputBindings.Remove(binding);
frameworkElement.InputBindings.Add(binding);
}
trackedFrameWorkElementsToBindings.Remove(frameworkElement);
}
}
}
どういうわけか、私たちのソリューションでは、一部のコントロールはUnLoadedイベントをスローしていませんが、二度と使用されることはなく、しばらくするとガベージコレクションも行われます。 HashCode/WeakReferencesを使用して追跡し、InputBindingsのコピーを取得することで、これを処理しています。
フルクラスは次のとおりです。
public class InputBindingBehavior
{
public static readonly DependencyProperty PropagateInputBindingsToWindowProperty =
DependencyProperty.RegisterAttached("PropagateInputBindingsToWindow", typeof(bool), typeof(InputBindingBehavior),
new PropertyMetadata(false, OnPropagateInputBindingsToWindowChanged));
private static readonly Dictionary<int, Tuple<WeakReference<FrameworkElement>, List<InputBinding>>> trackedFrameWorkElementsToBindings =
new Dictionary<int, Tuple<WeakReference<FrameworkElement>, List<InputBinding>>>();
public static bool GetPropagateInputBindingsToWindow(FrameworkElement obj)
{
return (bool)obj.GetValue(PropagateInputBindingsToWindowProperty);
}
public static void SetPropagateInputBindingsToWindow(FrameworkElement obj, bool value)
{
obj.SetValue(PropagateInputBindingsToWindowProperty, value);
}
private static void OnPropagateInputBindingsToWindowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((FrameworkElement)d).Loaded += OnFrameworkElementLoaded;
((FrameworkElement)d).Unloaded += OnFrameworkElementUnLoaded;
}
private static void OnFrameworkElementLoaded(object sender, RoutedEventArgs e)
{
var frameworkElement = (FrameworkElement)sender;
var window = Window.GetWindow(frameworkElement);
if (window != null)
{
// transfer InputBindings into our control
if (!trackedFrameWorkElementsToBindings.TryGetValue(frameworkElement.GetHashCode(), out var trackingData))
{
trackingData = Tuple.Create(
new WeakReference<FrameworkElement>(frameworkElement),
frameworkElement.InputBindings.Cast<InputBinding>().ToList());
trackedFrameWorkElementsToBindings.Add(
frameworkElement.GetHashCode(), trackingData);
}
// apply Bindings to Window
foreach (var inputBinding in trackingData.Item2)
{
window.InputBindings.Add(inputBinding);
}
frameworkElement.InputBindings.Clear();
}
}
private static void OnFrameworkElementUnLoaded(object sender, RoutedEventArgs e)
{
var frameworkElement = (FrameworkElement)sender;
var window = Window.GetWindow(frameworkElement);
var hashCode = frameworkElement.GetHashCode();
// remove Bindings from Window
if (window != null)
{
if (trackedFrameWorkElementsToBindings.TryGetValue(hashCode, out var trackedData))
{
foreach (var binding in trackedData.Item2)
{
frameworkElement.InputBindings.Add(binding);
window.InputBindings.Remove(binding);
}
trackedData.Item2.Clear();
trackedFrameWorkElementsToBindings.Remove(hashCode);
// catch removed and orphaned entries
CleanupBindingsDictionary(window, trackedFrameWorkElementsToBindings);
}
}
}
private static void CleanupBindingsDictionary(Window window, Dictionary<int, Tuple<WeakReference<FrameworkElement>, List<InputBinding>>> bindingsDictionary)
{
foreach (var hashCode in bindingsDictionary.Keys.ToList())
{
if (bindingsDictionary.TryGetValue(hashCode, out var trackedData) &&
!trackedData.Item1.TryGetTarget(out _))
{
Debug.WriteLine($"InputBindingBehavior: FrameWorkElement {hashCode} did never unload but was GCed, cleaning up leftover KeyBindings");
foreach (var binding in trackedData.Item2)
{
window.InputBindings.Remove(binding);
}
trackedData.Item2.Clear();
bindingsDictionary.Remove(hashCode);
}
}
}
}
しかし、少し遅れて、おそらく100%MVVMに準拠していない場合は、次のonloaded-eventを使用して、すべてのInputbindingsをウィンドウに伝播できます。
void UserControl1_Loaded(object sender, RoutedEventArgs e)
{
Window window = Window.GetWindow(this);
foreach (InputBinding ib in this.InputBindings)
{
window.InputBindings.Add(ib);
}
}
これはView-Layerにのみ影響するため、MVVMの観点からこのソリューションで問題ありません。このビットが見つかりました ここ
<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は、ターゲットuielementのコンテナーの名前です。
usercontrolでFocusable = "True"を設定することを忘れないでください