Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix crash when KeyBindings change while they are being handled #5055

Merged
merged 8 commits into from
Jun 14, 2021
8 changes: 7 additions & 1 deletion src/Avalonia.Input/KeyboardDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,18 @@ public void ProcessRawEvent(RawInputEventArgs e)
{
var bindings = (currentHandler as IInputElement)?.KeyBindings;
if (bindings != null)
foreach (var binding in bindings)
{
// Create a copy of the KeyBindings list.
// If we don't do this the foreach loop will throw an InvalidOperationException when the KeyBindings list is changed.
// This can happen when a new view is loaded which adds its own KeyBindings to the handler.
var cpy = bindings.ToArray();
foreach (var binding in cpy)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this is going to result in an array being copied for every keypress, which I suspect @MarchingCube will have something to say about ;)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would be possible to copy the array only when a matching key binding is found?

Copy link
Contributor Author

@Fusion86 Fusion86 Dec 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if I fully understand what you are saying, but maybe this works?

var bindingsToHandle = bindings
    .Where(x => x.Gesture?.Matches(ev) == true)
    .ToArray();

foreach (var binding in bindingsToHandle)
{
    if (ev.Handled)
        break;
    binding.TryHandle(ev);
}

This will create an array with all the KeyBindings which should be evaluated. If there are no bindings to be evaluated then the array will be empty, and nothing will happen.
A small issue with this is that the x.Gesture?.Matches(ev) is copied from inside binding.TryHandle(ev) and therefore this check will run twice. Once here and once inside the TryHandle method. Changing this would 'break' the API (though no other code within AvaloniaUI seems to call this method).
I also don't know if using LINQ is acceptable in this context.

E: Another issue with this solution is that it will run the x.Gesture?.Matches(ev) check on all bindings, even if the first binding that is to be executed changes ev.Handled = true. The old code didn't do this.
I guess you could 'fix' that by using the below snippet, but I personally don't really like this approach.

if (bindings.Any(x => x.Gesture?.Matches(ev) == true))
{
    var cpy = bindings.ToArray();
    foreach (var binding in cpy)
    {
        if (ev.Handled)
            break;
        binding.TryHandle(ev);
    }
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah something like this should work. Using LINQ here isn't ideal as that itself will allocate but I've pushed a change which does that check manually to your branch, along with a unit test.

Really sorry it took so long to get back to this.

{
if (ev.Handled)
break;
binding.TryHandle(ev);
}
}
currentHandler = currentHandler.VisualParent;
}

Expand Down