diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 5ed0abf25d..cc9e6b7444 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -7,6 +7,7 @@ using System.Windows.Input; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.LogicalTree; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -160,6 +161,40 @@ namespace Avalonia.Controls } } + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + if (IsDefault) + { + if (e.Root is IInputElement inputElement) + { + StopListeningForDefault(inputElement); + } + } + } + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + + if (Command != null) + { + Command.CanExecuteChanged += CanExecuteChanged; + } + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnDetachedFromLogicalTree(e); + + if (Command != null) + { + Command.CanExecuteChanged -= CanExecuteChanged; + } + } + /// protected override void OnKeyDown(KeyEventArgs e) { @@ -195,20 +230,6 @@ namespace Avalonia.Controls } } - /// - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - - if (IsDefault) - { - if (e.Root is IInputElement inputElement) - { - StopListeningForDefault(inputElement); - } - } - } - /// /// Invokes the event. /// @@ -281,17 +302,17 @@ namespace Avalonia.Controls { if (e.Sender is Button button) { - var oldCommand = e.OldValue as ICommand; - var newCommand = e.NewValue as ICommand; - - if (oldCommand != null) - { - oldCommand.CanExecuteChanged -= button.CanExecuteChanged; - } - - if (newCommand != null) + if (((ILogical)button).IsAttachedToLogicalTree) { - newCommand.CanExecuteChanged += button.CanExecuteChanged; + if (e.OldValue is ICommand oldCommand) + { + oldCommand.CanExecuteChanged -= button.CanExecuteChanged; + } + + if (e.NewValue is ICommand newCommand) + { + newCommand.CanExecuteChanged += button.CanExecuteChanged; + } } button.CanExecuteChanged(button, EventArgs.Empty); diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 64daa133a3..d8473dc613 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -286,6 +286,26 @@ namespace Avalonia.Controls return new MenuItemContainerGenerator(this); } + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + + if (Command != null) + { + Command.CanExecuteChanged += CanExecuteChanged; + } + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnDetachedFromLogicalTree(e); + + if (Command != null) + { + Command.CanExecuteChanged -= CanExecuteChanged; + } + } + /// /// Called when the is clicked. /// @@ -399,14 +419,17 @@ namespace Avalonia.Controls { if (e.Sender is MenuItem menuItem) { - if (e.OldValue is ICommand oldCommand) + if (((ILogical)menuItem).IsAttachedToLogicalTree) { - oldCommand.CanExecuteChanged -= menuItem.CanExecuteChanged; - } + if (e.OldValue is ICommand oldCommand) + { + oldCommand.CanExecuteChanged -= menuItem.CanExecuteChanged; + } - if (e.NewValue is ICommand newCommand) - { - newCommand.CanExecuteChanged += menuItem.CanExecuteChanged; + if (e.NewValue is ICommand newCommand) + { + newCommand.CanExecuteChanged += menuItem.CanExecuteChanged; + } } menuItem.CanExecuteChanged(menuItem, EventArgs.Empty); diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 91b37596b7..9a751d4953 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -5,6 +5,7 @@ using Avalonia.Input; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.UnitTests; using Avalonia.VisualTree; using Moq; using Xunit; @@ -21,6 +22,7 @@ namespace Avalonia.Controls.UnitTests { Command = command, }; + var root = new TestRoot { Child = target }; Assert.False(target.IsEnabled); command.IsEnabled = true; @@ -215,6 +217,39 @@ namespace Avalonia.Controls.UnitTests Assert.True(clicked); } + [Fact] + public void Button_Does_Not_Subscribe_To_Command_CanExecuteChanged_Until_Added_To_Logical_Tree() + { + var command = new TestCommand(true); + var target = new Button + { + Command = command, + }; + + Assert.Equal(0, command.SubscriptionCount); + } + + [Fact] + public void Button_Subscribes_To_Command_CanExecuteChanged_When_Added_To_Logical_Tree() + { + var command = new TestCommand(true); + var target = new Button { Command = command }; + var root = new TestRoot { Child = target }; + + Assert.Equal(1, command.SubscriptionCount); + } + + [Fact] + public void Button_Unsubscribes_From_Command_CanExecuteChanged_When_Removed_From_Logical_Tree() + { + var command = new TestCommand(true); + var target = new Button { Command = command }; + var root = new TestRoot { Child = target }; + + root.Child = null; + Assert.Equal(0, command.SubscriptionCount); + } + private class TestButton : Button, IRenderRoot { public TestButton() @@ -298,6 +333,7 @@ namespace Avalonia.Controls.UnitTests private class TestCommand : ICommand { + private EventHandler _canExecuteChanged; private bool _enabled; public TestCommand(bool enabled) @@ -313,12 +349,18 @@ namespace Avalonia.Controls.UnitTests if (_enabled != value) { _enabled = value; - CanExecuteChanged?.Invoke(this, EventArgs.Empty); + _canExecuteChanged?.Invoke(this, EventArgs.Empty); } } } - public event EventHandler CanExecuteChanged; + public int SubscriptionCount { get; private set; } + + public event EventHandler CanExecuteChanged + { + add { _canExecuteChanged += value; ++SubscriptionCount; } + remove { _canExecuteChanged -= value; --SubscriptionCount; } + } public bool CanExecute(object parameter) => _enabled; diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index e7352af23e..32d154249c 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Text; +using System.Windows.Input; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests @@ -22,5 +24,57 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.Focusable); } + + [Fact] + public void MenuItem_Does_Not_Subscribe_To_Command_CanExecuteChanged_Until_Added_To_Logical_Tree() + { + var command = new TestCommand(); + var target = new MenuItem + { + Command = command, + }; + + Assert.Equal(0, command.SubscriptionCount); + } + + [Fact] + public void MenuItem_Subscribes_To_Command_CanExecuteChanged_When_Added_To_Logical_Tree() + { + var command = new TestCommand(); + var target = new MenuItem { Command = command }; + var root = new TestRoot { Child = target }; + + Assert.Equal(1, command.SubscriptionCount); + } + + [Fact] + public void MenuItem_Unsubscribes_From_Command_CanExecuteChanged_When_Removed_From_Logical_Tree() + { + var command = new TestCommand(); + var target = new MenuItem { Command = command }; + var root = new TestRoot { Child = target }; + + root.Child = null; + Assert.Equal(0, command.SubscriptionCount); + } + + private class TestCommand : ICommand + { + private EventHandler _canExecuteChanged; + + public int SubscriptionCount { get; private set; } + + public event EventHandler CanExecuteChanged + { + add { _canExecuteChanged += value; ++SubscriptionCount; } + remove { _canExecuteChanged -= value; --SubscriptionCount; } + } + + public bool CanExecute(object parameter) => true; + + public void Execute(object parameter) + { + } + } } }