I have designed a reuseable usercontrol. It contains UserControl.InputBindings. It is quite simple as it only contains a label and a button (and new properties etc.)
We extended Adi Lesters attached behavior code with an unsubscribing mechanism on UnLoaded to clean up the transferred bindings. If the control exits the Visual Tree, the InputBindings are removed from the Window to avoid them being active. (We did not explore using WPF-Triggers on the attached property.)
As controls get reused by WPF in our solution, the behavior does not detach: Loaded/UnLoaded get called more than once. This does not lead to leaking, as the behavior doesn't hold a reference to the 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().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);
}
}
}
Somehow in our solution some controls are not throwing the UnLoaded event, although they never get used again and even get garbage collected after a while. We are taking care of this with tracking with HashCode/WeakReferences and taking a copy of the InputBindings.
Full class is:
public class InputBindingBehavior
{
public static readonly DependencyProperty PropagateInputBindingsToWindowProperty =
DependencyProperty.RegisterAttached("PropagateInputBindingsToWindow", typeof(bool), typeof(InputBindingBehavior),
new PropertyMetadata(false, OnPropagateInputBindingsToWindowChanged));
private static readonly Dictionary, List>> trackedFrameWorkElementsToBindings =
new Dictionary, List>>();
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.InputBindings.Cast().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, List>> 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);
}
}
}
}