From 1479f52e62c33dbf5c793493b4f005c99e9a250e Mon Sep 17 00:00:00 2001 From: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:21:20 +1000 Subject: [PATCH] Prevent toggle controls from changing state when commands cannot execute (#21126) * Add failing tests for toggle command gating * Prevent disabled toggle controls from changing state * Rework toggle command gating tests around pointer input * Add touch regression for toggle command gating * Remove synthetic toggle command gating tests --- .../Primitives/ToggleButton.cs | 5 ++ .../Input/TouchDeviceTests.cs | 81 +++++++++++++++++++ .../Primitives/ToggleButtonTests.cs | 4 +- 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 2a5c1c3cf3..666892b382 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -72,6 +72,11 @@ namespace Avalonia.Controls.Primitives protected override void OnClick() { + if (!IsEffectivelyEnabled) + { + return; + } + Toggle(); base.OnClick(); } diff --git a/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs index 600856c643..bbdb24e1dd 100644 --- a/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs @@ -1,6 +1,8 @@ using System; +using System.Windows.Input; using Avalonia.Base.UnitTests.Input; using Avalonia.Controls; +using Avalonia.Controls.Primitives; using Avalonia.Input.Raw; using Avalonia.Platform; using Avalonia.Rendering; @@ -242,6 +244,47 @@ namespace Avalonia.Input.UnitTests Assert.Equal(3, doubleTappedExecutedTimes); } + [Fact] + public void ToggleButton_Does_Not_Toggle_When_Command_Becomes_Disabled_Between_TouchBegin_And_TouchEnd() + { + using var app = UnitTestApp(new TimeSpan(200)); + + var renderer = new Mock(); + var impl = CreateTopLevelImplMock(); + var command = new TestCommand(true); + var target = new ToggleButton + { + Width = 100, + Height = 100, + Command = command, + }; + var root = CreateInputRoot(impl.Object, target, renderer.Object); + var device = new TouchDevice(); + var touchBegin = new RawPointerEventArgs(device, 0, root.PresentationSource, RawPointerEventType.TouchBegin, new Point(50, 50), RawInputModifiers.None) + { + RawPointerId = 1 + }; + var touchEnd = new RawPointerEventArgs(device, 1, root.PresentationSource, RawPointerEventType.TouchEnd, new Point(50, 50), RawInputModifiers.None) + { + RawPointerId = 1 + }; + + SetHit(renderer, target); + + impl.Object.Input!(touchBegin); + + Assert.True(target.IsPressed); + Assert.False(target.IsChecked ?? false); + + command.IsEnabled = false; + + Assert.False(target.IsEffectivelyEnabled); + + impl.Object.Input!(touchEnd); + + Assert.False(target.IsChecked ?? false); + } + private IDisposable UnitTestApp(TimeSpan doubleClickTime = new TimeSpan()) { var unitTestApp = UnitTestApplication.Start( @@ -301,6 +344,44 @@ namespace Avalonia.Input.UnitTests }); } + private sealed class TestCommand : ICommand + { + private bool _enabled; + private EventHandler? _canExecuteChanged; + + public TestCommand(bool enabled) + { + _enabled = enabled; + } + + public bool IsEnabled + { + get => _enabled; + set + { + if (_enabled == value) + { + return; + } + + _enabled = value; + _canExecuteChanged?.Invoke(this, EventArgs.Empty); + } + } + + public event EventHandler? CanExecuteChanged + { + add => _canExecuteChanged += value; + remove => _canExecuteChanged -= value; + } + + public bool CanExecute(object? parameter) => _enabled; + + public void Execute(object? parameter) + { + } + } + private class TestTopLevel(ITopLevelImpl impl) : TopLevel(impl) { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs index ac35c4b33f..c33c4d1447 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs @@ -1,4 +1,6 @@ -using Avalonia.Data; +using System; +using Avalonia.Controls.UnitTests.Utils; +using Avalonia.Data; using Avalonia.UnitTests; using Xunit;