Browse Source

fix: ICommadSource Implemetation (#15496)

* test: CommandParameter does not change between CanExecute and Execute

* feat: CommandParameter does not change between CanExecute and Execute

* test: update
release/11.1.0-rc1
workgroupengineering 2 years ago
committed by Max Katz
parent
commit
1408253223
  1. 44
      src/Avalonia.Controls/Button.cs
  2. 79
      src/Avalonia.Controls/MenuItem.cs
  3. 29
      src/Avalonia.Controls/SplitButton/SplitButton.cs
  4. 9
      tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_Tab.cs
  5. 212
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  6. 109
      tests/Avalonia.Controls.UnitTests/MenuItemTests.cs
  7. 33
      tests/Avalonia.Controls.UnitTests/SplitButtonTests.cs
  8. 52
      tests/Avalonia.Controls.UnitTests/Utils/TestCommand.cs

44
src/Avalonia.Controls/Button.cs

@ -34,8 +34,10 @@ namespace Avalonia.Controls
[PseudoClasses(pcFlyoutOpen, pcPressed)]
public class Button : ContentControl, ICommandSource, IClickableControl
{
private const string pcPressed = ":pressed";
private const string pcPressed = ":pressed";
private const string pcFlyoutOpen = ":flyout-open";
private EventHandler? _canExecuteChangeHandler = default;
private EventHandler CanExecuteChangedHandler => _canExecuteChangeHandler ??= new(CanExecuteChanged);
/// <summary>
/// Defines the <see cref="ClickMode"/> property.
@ -250,10 +252,11 @@ namespace Avalonia.Controls
base.OnAttachedToLogicalTree(e);
if (Command != null)
(var command, var parameter) = (Command, CommandParameter);
if (command is not null)
{
Command.CanExecuteChanged += CanExecuteChanged;
CanExecuteChanged(this, EventArgs.Empty);
command.CanExecuteChanged += CanExecuteChangedHandler;
CanExecuteChanged(command, parameter);
}
}
@ -269,9 +272,9 @@ namespace Avalonia.Controls
base.OnDetachedFromLogicalTree(e);
if (Command != null)
if (Command is { } command)
{
Command.CanExecuteChanged -= CanExecuteChanged;
command.CanExecuteChanged -= CanExecuteChangedHandler;
}
}
@ -343,9 +346,10 @@ namespace Avalonia.Controls
var e = new RoutedEventArgs(ClickEvent);
RaiseEvent(e);
if (!e.Handled && Command?.CanExecute(CommandParameter) == true)
(var command, var parameter) = (Command, CommandParameter);
if (!e.Handled && command is not null && command.CanExecute(parameter))
{
Command.Execute(CommandParameter);
command.Execute(parameter);
e.Handled = true;
}
}
@ -451,25 +455,24 @@ namespace Avalonia.Controls
if (change.Property == CommandProperty)
{
var (oldValue, newValue) = change.GetOldAndNewValue<ICommand?>();
if (((ILogical)this).IsAttachedToLogicalTree)
{
var (oldValue, newValue) = change.GetOldAndNewValue<ICommand?>();
if (oldValue is ICommand oldCommand)
{
oldCommand.CanExecuteChanged -= CanExecuteChanged;
oldCommand.CanExecuteChanged -= CanExecuteChangedHandler;
}
if (newValue is ICommand newCommand)
{
newCommand.CanExecuteChanged += CanExecuteChanged;
newCommand.CanExecuteChanged += CanExecuteChangedHandler;
}
}
CanExecuteChanged(this, EventArgs.Empty);
CanExecuteChanged(newValue, CommandParameter);
}
else if (change.Property == CommandParameterProperty)
{
CanExecuteChanged(this, EventArgs.Empty);
CanExecuteChanged(Command, change.NewValue);
}
else if (change.Property == IsCancelProperty)
{
@ -557,7 +560,18 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param>
private void CanExecuteChanged(object? sender, EventArgs e)
{
var canExecute = Command == null || Command.CanExecute(CommandParameter);
CanExecuteChanged(Command, CommandParameter);
}
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
private void CanExecuteChanged(ICommand? command, object? parameter)
{
if (!((ILogical)this).IsAttachedToLogicalTree)
{
return;
}
var canExecute = command == null || command.CanExecute(parameter);
if (canExecute != _commandCanExecute)
{

79
src/Avalonia.Controls/MenuItem.cs

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Reactive;
using System.Windows.Input;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
@ -13,7 +12,7 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Layout;
using Avalonia.Reactive;
namespace Avalonia.Controls
{
@ -24,6 +23,9 @@ namespace Avalonia.Controls
[PseudoClasses(":separator", ":radio", ":toggle", ":checked", ":icon", ":open", ":pressed", ":selected")]
public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource, IClickableControl, IRadioButton
{
private EventHandler? _canExecuteChangeHandler = default;
private EventHandler CanExecuteChangedHandler => _canExecuteChangeHandler ??= new(CanExecuteChanged);
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
@ -83,7 +85,7 @@ namespace Avalonia.Controls
/// </summary>
public static readonly StyledProperty<string?> GroupNameProperty =
RadioButton.GroupNameProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="Click"/> event.
/// </summary>
@ -292,7 +294,7 @@ namespace Avalonia.Controls
get => GetValue(StaysOpenOnClickProperty);
set => SetValue(StaysOpenOnClickProperty, value);
}
/// <inheritdoc cref="IMenuItem.ToggleType" />
public MenuItemToggleType ToggleType
{
@ -306,7 +308,7 @@ namespace Avalonia.Controls
get => GetValue(IsCheckedProperty);
set => SetValue(IsCheckedProperty, value);
}
bool IRadioButton.IsChecked
{
get => IsChecked;
@ -319,7 +321,7 @@ namespace Avalonia.Controls
get => GetValue(GroupNameProperty);
set => SetValue(GroupNameProperty, value);
}
/// <summary>
/// Gets or sets a value that indicates whether the <see cref="MenuItem"/> has a submenu.
/// </summary>
@ -413,15 +415,16 @@ namespace Avalonia.Controls
{
SetCurrentValue(HotKeyProperty, _hotkey);
}
base.OnAttachedToLogicalTree(e);
if (Command != null)
(var command, var parameter) = (Command, CommandParameter);
if (command is not null)
{
Command.CanExecuteChanged += CanExecuteChanged;
command.CanExecuteChanged += CanExecuteChangedHandler;
}
TryUpdateCanExecute();
TryUpdateCanExecute(command, parameter);
var parent = Parent;
@ -437,7 +440,7 @@ namespace Avalonia.Controls
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
TryUpdateCanExecute();
}
@ -454,7 +457,7 @@ namespace Avalonia.Controls
if (Command != null)
{
Command.CanExecuteChanged -= CanExecuteChanged;
Command.CanExecuteChanged -= CanExecuteChangedHandler;
}
}
@ -464,9 +467,10 @@ namespace Avalonia.Controls
/// <param name="e">The click event args.</param>
protected virtual void OnClick(RoutedEventArgs e)
{
if (!e.Handled && Command?.CanExecute(CommandParameter) == true)
(var command, var parameter) = (Command, CommandParameter);
if (!e.Handled && command is not null && command.CanExecute(parameter) == true)
{
Command.Execute(CommandParameter);
command.Execute(parameter);
e.Handled = true;
}
}
@ -577,21 +581,25 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param>
private static void CommandChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is MenuItem menuItem &&
((ILogical)menuItem).IsAttachedToLogicalTree)
var newCommand = e.NewValue as ICommand;
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.CanExecuteChangedHandler;
}
if (e.NewValue is ICommand newCommand)
{
newCommand.CanExecuteChanged += menuItem.CanExecuteChanged;
if (newCommand is not null)
{
newCommand.CanExecuteChanged += menuItem.CanExecuteChangedHandler;
}
}
menuItem.TryUpdateCanExecute();
menuItem.TryUpdateCanExecute(newCommand, menuItem.CommandParameter);
}
}
/// <summary>
@ -602,7 +610,8 @@ namespace Avalonia.Controls
{
if (e.Sender is MenuItem menuItem)
{
menuItem.TryUpdateCanExecute();
(var command, var parameter) = (menuItem.Command, e.NewValue);
menuItem.TryUpdateCanExecute(command, parameter);
}
}
@ -621,21 +630,27 @@ namespace Avalonia.Controls
/// </summary>
private void TryUpdateCanExecute()
{
if (Command == null)
TryUpdateCanExecute(Command, CommandParameter);
}
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
private void TryUpdateCanExecute(ICommand? command, object? parameter)
{
if (command == null)
{
_commandCanExecute = !_commandBindingError;
UpdateIsEffectivelyEnabled();
return;
}
//Perf optimization - only raise CanExecute event if the menu is open
if (!((ILogical)this).IsAttachedToLogicalTree ||
Parent is MenuItem { IsSubMenuOpen: false })
{
return;
}
var canExecute = Command.CanExecute(CommandParameter);
var canExecute = command.CanExecute(parameter);
if (canExecute != _commandCanExecute)
{
_commandCanExecute = canExecute;
@ -720,7 +735,7 @@ namespace Avalonia.Controls
(MenuInteractionHandler as DefaultMenuInteractionHandler)?.OnCheckedChanged(this);
}
}
/// <summary>
/// Called when the <see cref="HeaderedSelectingItemsControl.Header"/> property changes.
/// </summary>
@ -834,7 +849,7 @@ namespace Avalonia.Controls
SelectedItem = null;
}
void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e);
void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => CanExecuteChangedHandler(sender, e);
void IClickableControl.RaiseClick()
{

29
src/Avalonia.Controls/SplitButton/SplitButton.cs

@ -136,7 +136,18 @@ namespace Avalonia.Controls
/// <inheritdoc cref="ICommandSource.CanExecuteChanged"/>
private void CanExecuteChanged(object? sender, EventArgs e)
{
var canExecute = Command == null || Command.CanExecute(CommandParameter);
(var command, var parameter) = (Command, CommandParameter);
CanExecuteChanged(command, parameter);
}
private void CanExecuteChanged(ICommand? command, object? parameter)
{
if (!((ILogical)this).IsAttachedToLogicalTree)
{
return;
}
var canExecute = command is null || command.CanExecute(parameter);
if (canExecute != _commandCanExecute)
{
@ -282,10 +293,11 @@ namespace Avalonia.Controls
{
if (e.Property == CommandProperty)
{
// Must unregister events here while a reference to the old command still exists
var (oldValue, newValue) = e.GetOldAndNewValue<ICommand?>();
if (_isAttachedToLogicalTree)
{
// Must unregister events here while a reference to the old command still exists
var (oldValue, newValue) = e.GetOldAndNewValue<ICommand?>();
if (oldValue is ICommand oldCommand)
{
@ -298,11 +310,11 @@ namespace Avalonia.Controls
}
}
CanExecuteChanged(this, EventArgs.Empty);
CanExecuteChanged(newValue, CommandParameter);
}
else if (e.Property == CommandParameterProperty)
else if (e.Property == CommandParameterProperty && IsLoaded)
{
CanExecuteChanged(this, EventArgs.Empty);
CanExecuteChanged(Command, e.NewValue);
}
else if (e.Property == FlyoutProperty)
{
@ -386,15 +398,16 @@ namespace Avalonia.Controls
/// <param name="e">The event args from the internal Click event.</param>
protected virtual void OnClickPrimary(RoutedEventArgs? e)
{
(var command, var parameter) = (Command, CommandParameter);
// Note: It is not currently required to check enabled status; however, this is a failsafe
if (IsEffectivelyEnabled)
{
var eventArgs = new RoutedEventArgs(ClickEvent);
RaiseEvent(eventArgs);
if (!eventArgs.Handled && Command?.CanExecute(CommandParameter) == true)
if (!eventArgs.Handled && command?.CanExecute(parameter) == true)
{
Command.Execute(CommandParameter);
command.Execute(parameter);
eventArgs.Handled = true;
}
}

9
tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_Tab.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Xunit;
@ -1282,6 +1283,8 @@ namespace Avalonia.Base.UnitTests.Input
Button expected;
bool executed = false;
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var top = new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
@ -1304,6 +1307,12 @@ namespace Avalonia.Base.UnitTests.Input
}
};
var testRoot = new TestRoot(top);
top.ApplyTemplate();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next) as Button;
Assert.Equal(expected.Name, result?.Name);

212
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

@ -1,5 +1,7 @@
using System;
using System.Windows.Input;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Controls.UnitTests.Utils;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
@ -9,7 +11,6 @@ using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
using MouseButton = Avalonia.Input.MouseButton;
@ -19,7 +20,7 @@ namespace Avalonia.Controls.UnitTests
public class ButtonTests : ScopedTestBase
{
private MouseTestHelper _helper = new MouseTestHelper();
[Fact]
public void Button_Is_Disabled_When_Command_Is_Disabled()
{
@ -100,6 +101,9 @@ namespace Avalonia.Controls.UnitTests
DataContext = new object(),
[!Button.CommandProperty] = new Binding("Command"),
};
var root = new TestRoot { Child = target };
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
Assert.True(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
@ -141,9 +145,9 @@ namespace Avalonia.Controls.UnitTests
renderer.Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<Visual>(), It.IsAny<Func<Visual, bool>>()))
.Returns<Point, Visual, Func<Visual, bool>>((p, r, f) =>
r.Bounds.Contains(p) ? new Visual[] { r } : new Visual[0]);
using var _ = UnitTestApplication.Start(TestServices.StyledWindow);
var root = new Window() { HitTesterOverride = renderer.Object };
var target = new Button()
{
@ -188,7 +192,7 @@ namespace Avalonia.Controls.UnitTests
target.Click += (s, e) => clicked = true;
RaisePointerEntered(target);
RaisePointerMove(target, new Point(50,50));
RaisePointerMove(target, new Point(50, 50));
RaisePointerPressed(target, 1, MouseButton.Left, new Point(50, 50));
RaisePointerExited(target);
@ -210,9 +214,9 @@ namespace Avalonia.Controls.UnitTests
.Returns<Point, Visual, Func<Visual, bool>>((p, r, f) =>
r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ?
new Visual[] { r } : new Visual[0]);
using var _ = UnitTestApplication.Start(TestServices.StyledWindow);
var root = new Window() { HitTesterOverride = renderer.Object };
var target = new Button()
{
@ -298,16 +302,104 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Raises_Click_When_AccessKey_Raised()
{
var command = new TestCommand(p => p is bool value && value);
var target = new Button { Command = command };
var raised = 0;
var ah = new AccessKeyHandler();
using var app = UnitTestApplication.Start(TestServices.StyledWindow.With(accessKeyHandler: ah));
var impl = CreateMockTopLevelImpl();
var command = new TestCommand(p => p is bool value && value, _ => raised++);
Button target;
var root = new TestTopLevel(impl.Object)
{
Template = CreateTemplate(),
Content = target = new Button
{
Content = "_A",
Command = command,
Template = new FuncControlTemplate<Button>((parent, scope) =>
{
return new ContentPresenter
{
Name = "PART_ContentPresenter",
[~ContentPresenter.ContentProperty] = new TemplateBinding(Button.ContentProperty),
[~ContentPresenter.ContentTemplateProperty] = new TemplateBinding(Button.ContentProperty),
RecognizesAccessKey = true,
}.RegisterInNameScope(scope);
})
},
};
root.ApplyTemplate();
root.Presenter.UpdateChild();
target.ApplyTemplate();
target.Presenter.UpdateChild();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
var accessKey = Key.A;
target.CommandParameter = true;
Assert.True(target.IsEffectivelyEnabled);
RaiseAccessKey(root, accessKey);
Assert.Equal(1, raised);
target.CommandParameter = false;
Assert.False(target.IsEffectivelyEnabled);
RaiseAccessKey(root, accessKey);
Assert.Equal(1, raised);
static FuncControlTemplate<TestTopLevel> CreateTemplate()
{
return new FuncControlTemplate<TestTopLevel>((x, scope) =>
new ContentPresenter
{
Name = "PART_ContentPresenter",
[~ContentPresenter.ContentProperty] = new TemplateBinding(ContentControl.ContentProperty),
[~ContentPresenter.ContentTemplateProperty] = new TemplateBinding(ContentControl.ContentTemplateProperty)
}.RegisterInNameScope(scope));
}
static Mock<ITopLevelImpl> CreateMockTopLevelImpl(bool setupProperties = false)
{
var topLevel = new Mock<ITopLevelImpl>();
if (setupProperties)
topLevel.SetupAllProperties();
topLevel.Setup(x => x.RenderScaling).Returns(1);
topLevel.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
return topLevel;
}
static void RaiseAccessKey(IInputElement target, Key accessKey)
{
KeyDown(target, Key.LeftAlt);
KeyDown(target, accessKey, KeyModifiers.Alt);
KeyUp(target, accessKey, KeyModifiers.Alt);
KeyUp(target, Key.LeftAlt);
}
static void KeyDown(IInputElement target, Key key, KeyModifiers modifiers = KeyModifiers.None)
{
target.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
Key = key,
KeyModifiers = modifiers,
});
}
static void KeyUp(IInputElement target, Key key, KeyModifiers modifiers = KeyModifiers.None)
{
target.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyUpEvent,
Key = key,
KeyModifiers = modifiers,
});
}
}
[Fact]
public void Button_Invokes_Doesnt_Execute_When_Button_Disabled()
{
@ -321,7 +413,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, raised);
}
[Fact]
public void Button_IsDefault_Works()
{
@ -331,13 +423,13 @@ namespace Avalonia.Controls.UnitTests
var target = new Button();
var window = new Window { Content = target };
window.Show();
target.Click += (s, e) => ++raised;
target.IsDefault = false;
window.RaiseEvent(CreateKeyDownEvent(Key.Enter));
Assert.Equal(0, raised);
target.IsDefault = true;
window.RaiseEvent(CreateKeyDownEvent(Key.Enter));
Assert.Equal(1, raised);
@ -345,18 +437,18 @@ namespace Avalonia.Controls.UnitTests
target.IsDefault = false;
window.RaiseEvent(CreateKeyDownEvent(Key.Enter));
Assert.Equal(1, raised);
target.IsDefault = true;
window.RaiseEvent(CreateKeyDownEvent(Key.Enter));
Assert.Equal(2, raised);
window.Content = null;
// To check if handler was raised on the button, when it's detached, we need to pass it as a source manually.
window.RaiseEvent(CreateKeyDownEvent(Key.Enter, target));
Assert.Equal(2, raised);
}
}
[Fact]
public void Button_IsCancel_Works()
{
@ -366,13 +458,13 @@ namespace Avalonia.Controls.UnitTests
var target = new Button();
var window = new Window { Content = target };
window.Show();
target.Click += (s, e) => ++raised;
target.IsCancel = false;
window.RaiseEvent(CreateKeyDownEvent(Key.Escape));
Assert.Equal(0, raised);
target.IsCancel = true;
window.RaiseEvent(CreateKeyDownEvent(Key.Escape));
Assert.Equal(1, raised);
@ -380,17 +472,45 @@ namespace Avalonia.Controls.UnitTests
target.IsCancel = false;
window.RaiseEvent(CreateKeyDownEvent(Key.Escape));
Assert.Equal(1, raised);
target.IsCancel = true;
window.RaiseEvent(CreateKeyDownEvent(Key.Escape));
Assert.Equal(2, raised);
window.Content = null;
window.RaiseEvent(CreateKeyDownEvent(Key.Escape, target));
Assert.Equal(2, raised);
}
}
[Fact]
public void Button_CommandParameter_Does_Not_Change_While_Execution()
{
var target = new Button();
object lastParamenter = "A";
var generator = new Random();
var onlyOnce = false;
var command = new TestCommand(parameter =>
{
if (!onlyOnce)
{
onlyOnce = true;
target.CommandParameter = generator.Next();
}
lastParamenter = parameter;
return true;
},
parameter =>
{
Assert.Equal(lastParamenter, parameter);
});
target.CommandParameter = lastParamenter;
target.Command = command;
var root = new TestRoot { Child = target };
(target as IClickableControl).RaiseClick();
}
private KeyEventArgs CreateKeyDownEvent(Key key, Interactive source = null)
{
return new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = key, Source = source };
@ -421,50 +541,20 @@ namespace Avalonia.Controls.UnitTests
_helper.Move(button, pos);
}
private class TestCommand : ICommand
{
private readonly Func<object, bool> _canExecute;
private readonly Action<object> _execute;
private EventHandler _canExecuteChanged;
private bool _enabled = true;
public TestCommand(bool enabled = true)
{
_enabled = enabled;
_canExecute = _ => _enabled;
_execute = _ => { };
}
public TestCommand(Func<object, bool> canExecute, Action<object> execute = null)
{
_canExecute = canExecute;
_execute = execute ?? (_ => { });
}
public bool IsEnabled
{
get { return _enabled; }
set
{
if (_enabled != value)
{
_enabled = value;
_canExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
}
public int SubscriptionCount { get; private set; }
private class TestTopLevel : TopLevel
{
private readonly ILayoutManager _layoutManager;
public bool IsClosed { get; private set; }
public event EventHandler CanExecuteChanged
public TestTopLevel(ITopLevelImpl impl, ILayoutManager layoutManager = null)
: base(impl)
{
add { _canExecuteChanged += value; ++SubscriptionCount; }
remove { _canExecuteChanged -= value; --SubscriptionCount; }
_layoutManager = layoutManager ?? new LayoutManager(this);
}
public bool CanExecute(object parameter) => _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
private protected override ILayoutManager CreateLayoutManager() => _layoutManager;
}
}
}

109
tests/Avalonia.Controls.UnitTests/MenuItemTests.cs

@ -1,15 +1,14 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Controls.UnitTests.Utils;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Moq;
using Xunit;
@ -100,7 +99,9 @@ namespace Avalonia.Controls.UnitTests
[!MenuItem.CommandProperty] = new Binding("Command"),
};
var root = new TestRoot { Child = target };
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
Assert.True(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
@ -165,7 +166,7 @@ namespace Avalonia.Controls.UnitTests
root.Child = null;
Assert.Equal(0, command.SubscriptionCount);
}
[Fact]
public void MenuItem_Invokes_CanExecute_When_Added_To_Logical_Tree_And_CommandParameter_Changed()
{
@ -173,13 +174,15 @@ namespace Avalonia.Controls.UnitTests
var target = new MenuItem { Command = command };
var root = new TestRoot { Child = target };
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
target.CommandParameter = true;
Assert.True(target.IsEffectivelyEnabled);
target.CommandParameter = false;
Assert.False(target.IsEffectivelyEnabled);
}
[Fact]
public void MenuItem_Does_Not_Invoke_CanExecute_When_ContextMenu_Closed()
{
@ -197,7 +200,8 @@ namespace Avalonia.Controls.UnitTests
window.ApplyStyling();
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
Assert.True(target.IsEffectivelyEnabled);
target.Command = command;
Assert.Equal(0, canExecuteCallCount);
@ -207,8 +211,9 @@ namespace Avalonia.Controls.UnitTests
command.RaiseCanExecuteChanged();
Assert.Equal(0, canExecuteCallCount);
contextMenu.Open();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
Assert.Equal(3, canExecuteCallCount);// 3 because popup is changing logical child and moreover we need to invalidate again after the item is attached to the visual tree
command.RaiseCanExecuteChanged();
@ -218,7 +223,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(5, canExecuteCallCount);
}
}
[Fact]
public void MenuItem_Does_Not_Invoke_CanExecute_When_MenuFlyout_Closed()
{
@ -237,7 +242,7 @@ namespace Avalonia.Controls.UnitTests
window.ApplyStyling();
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
Assert.True(target.IsEffectivelyEnabled);
target.Command = command;
Assert.Equal(0, canExecuteCallCount);
@ -249,6 +254,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, canExecuteCallCount);
flyout.ShowAt(button);
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
Assert.Equal(2, canExecuteCallCount); // 2 because we need to invalidate after the item is attached to the visual tree
command.RaiseCanExecuteChanged();
@ -258,7 +264,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(4, canExecuteCallCount);
}
}
[Fact]
public void MenuItem_Does_Not_Invoke_CanExecute_When_Parent_MenuItem_Closed()
{
@ -278,7 +284,9 @@ namespace Avalonia.Controls.UnitTests
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
contextMenu.Open();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
Assert.True(target.IsEffectivelyEnabled);
target.Command = command;
Assert.Equal(0, canExecuteCallCount);
@ -377,7 +385,7 @@ namespace Avalonia.Controls.UnitTests
var panel = Assert.IsType<StackPanel>(menu.Presenter.Panel);
Assert.Equal(2, panel.Children.Count);
for (var i = 0; i < panel.Children.Count; i++)
for (var i = 0; i < panel.Children.Count; i++)
{
var menuItem = Assert.IsType<MenuItem>(panel.Children[i]);
@ -501,7 +509,7 @@ namespace Avalonia.Controls.UnitTests
var window = new Window { Content = menu };
window.Show();
Assert.False(menuItem1.IsChecked);
Assert.True(menuItem2.IsChecked);
Assert.False(menuItem3.IsChecked);
@ -512,7 +520,7 @@ namespace Avalonia.Controls.UnitTests
Assert.False(menuItem2.IsChecked);
Assert.True(menuItem3.IsChecked);
}
[Fact]
public void Radio_Menu_Group_Can_Be_Changed_In_Runtime()
{
@ -541,7 +549,7 @@ namespace Avalonia.Controls.UnitTests
var window = new Window { Content = menu };
window.Show();
Assert.False(menuItem1.IsChecked);
Assert.True(menuItem2.IsChecked);
Assert.False(menuItem3.IsChecked);
@ -555,12 +563,12 @@ namespace Avalonia.Controls.UnitTests
menuItem3.GroupName = null;
menuItem1.IsChecked = true;
Assert.True(menuItem1.IsChecked);
Assert.False(menuItem2.IsChecked);
Assert.True(menuItem3.IsChecked);
}
[Fact]
public void Radio_MenuItem_In_Same_Group_But_Submenu_Is_Unchecked()
{
@ -600,7 +608,7 @@ namespace Avalonia.Controls.UnitTests
var window = new Window { Content = menu };
window.Show();
Assert.False(menuItem1.IsChecked);
Assert.False(menuItem2.IsChecked);
Assert.True(menuItem3.IsChecked);
@ -768,12 +776,40 @@ namespace Avalonia.Controls.UnitTests
Assert.False(menuItem4.IsChecked);
}
[Fact]
public void MenuItem_CommandParameter_Does_Not_Change_While_Execution()
{
var target = new MenuItem();
object lastParamenter = "A";
var generator = new Random();
var onlyOnce = false;
var command = new TestCommand(parameter =>
{
if (!onlyOnce)
{
onlyOnce = true;
target.CommandParameter = generator.Next();
}
lastParamenter = parameter;
return true;
},
parameter =>
{
Assert.Equal(lastParamenter, parameter);
});
target.CommandParameter = lastParamenter;
target.Command = command;
var root = new TestRoot { Child = target };
(target as IClickableControl).RaiseClick();
}
private IDisposable Application()
{
var screen = new PixelRect(new PixelPoint(), new PixelSize(100, 100));
var screenImpl = new Mock<IScreenImpl>();
screenImpl.Setup(x => x.ScreenCount).Returns(1);
screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(1, screen, screen, true) });
screenImpl.Setup(X => X.AllScreens).Returns(new[] { new Screen(1, screen, screen, true) });
var windowImpl = MockWindowingPlatform.CreateWindowMock();
popupImpl = MockWindowingPlatform.CreatePopupMock(windowImpl.Object);
@ -790,41 +826,10 @@ namespace Avalonia.Controls.UnitTests
return UnitTestApplication.Start(services);
}
private class TestCommand : ICommand
{
private readonly Func<object, bool> _canExecute;
private readonly Action<object> _execute;
private EventHandler _canExecuteChanged;
public TestCommand(bool enabled = true)
: this(_ => enabled, _ => { })
{
}
public TestCommand(Func<object, bool> canExecute, Action<object> execute = null)
{
_canExecute = canExecute;
_execute = execute ?? (_ => { });
}
public int SubscriptionCount { get; private set; }
public event EventHandler CanExecuteChanged
{
add { _canExecuteChanged += value; ++SubscriptionCount; }
remove { _canExecuteChanged -= value; --SubscriptionCount; }
}
public bool CanExecute(object parameter) => _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty);
}
private record MenuViewModel(string Header)
{
public IList<MenuViewModel> Children { get; set;}
public IList<MenuViewModel> Children { get; set; }
}
}
}

33
tests/Avalonia.Controls.UnitTests/SplitButtonTests.cs

@ -0,0 +1,33 @@
using System;
using Avalonia.Controls.UnitTests.Utils;
using Avalonia.Input;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests;
public class SplitButtonTests : ScopedTestBase
{
[Fact]
public void SplitButton_CommandParameter_Does_Not_Change_While_Execution()
{
var target = new SplitButton();
object lastParamenter = "A";
var generator = new Random();
var command = new TestCommand(parameter =>
{
target.CommandParameter = generator.Next();
lastParamenter = parameter;
return true;
},
parameter =>
{
Assert.Equal(lastParamenter, parameter);
});
target.CommandParameter = lastParamenter;
target.Command = command;
var root = new TestRoot { Child = target };
(target as IClickableControl).RaiseClick();
}
}

52
tests/Avalonia.Controls.UnitTests/Utils/TestCommand.cs

@ -0,0 +1,52 @@
using System;
using System.Windows.Input;
namespace Avalonia.Controls.UnitTests.Utils;
internal class TestCommand : ICommand
{
private readonly Func<object, bool> _canExecute;
private readonly Action<object> _execute;
private EventHandler _canExecuteChanged;
private bool _enabled = true;
public TestCommand(bool enabled = true)
{
_enabled = enabled;
_canExecute = _ => _enabled;
_execute = _ => { };
}
public TestCommand(Func<object, bool> canExecute, Action<object> execute = null)
{
_canExecute = canExecute;
_execute = execute ?? (_ => { });
}
public bool IsEnabled
{
get { return _enabled; }
set
{
if (_enabled != value)
{
_enabled = value;
_canExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
}
public int SubscriptionCount { get; private set; }
public event EventHandler CanExecuteChanged
{
add { _canExecuteChanged += value; ++SubscriptionCount; }
remove { _canExecuteChanged -= value; --SubscriptionCount; }
}
public bool CanExecute(object parameter) => _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty);
}
Loading…
Cancel
Save