Browse Source

Merge pull request #2529 from AvaloniaUI/fixes/2501-command-isenabled

Fix command enabled state
pull/2679/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
ba0860b34a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      samples/ControlCatalog/ViewModels/MenuPageViewModel.cs
  2. 23
      src/Avalonia.Controls/Button.cs
  3. 2
      src/Avalonia.Controls/ComboBox.cs
  4. 36
      src/Avalonia.Controls/MenuItem.cs
  5. 2
      src/Avalonia.Input/FocusManager.cs
  6. 6
      src/Avalonia.Input/IInputElement.cs
  7. 83
      src/Avalonia.Input/InputElement.cs
  8. 2
      src/Avalonia.Input/InputExtensions.cs
  9. 4
      src/Avalonia.Input/Navigation/FocusExtensions.cs
  10. 44
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  11. 107
      tests/Avalonia.Controls.UnitTests/MenuItemTests.cs
  12. 101
      tests/Avalonia.Input.UnitTests/InputElement_Enabled.cs

3
samples/ControlCatalog/ViewModels/MenuPageViewModel.cs

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using ReactiveUI;
@ -11,7 +12,7 @@ namespace ControlCatalog.ViewModels
public MenuPageViewModel()
{
OpenCommand = ReactiveCommand.CreateFromTask(Open);
SaveCommand = ReactiveCommand.Create(Save);
SaveCommand = ReactiveCommand.Create(Save, Observable.Return(false));
OpenRecentCommand = ReactiveCommand.Create<string>(OpenRecent);
MenuItems = new[]

23
src/Avalonia.Controls/Button.cs

@ -33,8 +33,6 @@ namespace Avalonia.Controls
/// </summary>
public class Button : ContentControl
{
private ICommand _command;
/// <summary>
/// Defines the <see cref="ClickMode"/> property.
/// </summary>
@ -75,6 +73,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> IsPressedProperty =
AvaloniaProperty.Register<Button, bool>(nameof(IsPressed));
private ICommand _command;
private bool _commandCanExecute = true;
/// <summary>
/// Initializes static members of the <see cref="Button"/> class.
/// </summary>
@ -147,6 +148,8 @@ namespace Avalonia.Controls
private set { SetValue(IsPressedProperty, value); }
}
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
@ -292,7 +295,11 @@ namespace Avalonia.Controls
{
if (status?.ErrorType == BindingErrorType.Error)
{
IsEnabled = false;
if (_commandCanExecute)
{
_commandCanExecute = false;
UpdateIsEffectivelyEnabled();
}
}
}
}
@ -351,9 +358,13 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param>
private void CanExecuteChanged(object sender, EventArgs e)
{
// HACK: Just set the IsEnabled property for the moment. This needs to be changed to
// use IsEnabledCore etc. but it will do for now.
IsEnabled = Command == null || Command.CanExecute(CommandParameter);
var canExecute = Command == null || Command.CanExecute(CommandParameter);
if (canExecute != _commandCanExecute)
{
_commandCanExecute = canExecute;
UpdateIsEffectivelyEnabled();
}
}
/// <summary>

2
src/Avalonia.Controls/ComboBox.cs

@ -302,7 +302,7 @@ namespace Avalonia.Controls
}
}
private bool CanFocus(IControl control) => control.Focusable && control.IsEnabledCore && control.IsVisible;
private bool CanFocus(IControl control) => control.Focusable && control.IsEffectivelyEnabled && control.IsVisible;
private void UpdateSelectionBoxItem(object item)
{

36
src/Avalonia.Controls/MenuItem.cs

@ -9,6 +9,7 @@ using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
@ -20,8 +21,6 @@ namespace Avalonia.Controls
/// </summary>
public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable
{
private ICommand _command;
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
@ -91,9 +90,8 @@ namespace Avalonia.Controls
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel());
/// <summary>
/// The submenu popup.
/// </summary>
private ICommand _command;
private bool _commandCanExecute = true;
private Popup _popup;
/// <summary>
@ -231,6 +229,8 @@ namespace Avalonia.Controls
/// <inheritdoc/>
IMenuElement IMenuItem.Parent => Parent as IMenuElement;
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
/// <inheritdoc/>
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
@ -394,6 +394,22 @@ namespace Avalonia.Controls
}
}
protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
{
base.UpdateDataValidation(property, status);
if (property == CommandProperty)
{
if (status?.ErrorType == BindingErrorType.Error)
{
if (_commandCanExecute)
{
_commandCanExecute = false;
UpdateIsEffectivelyEnabled();
}
}
}
}
/// <summary>
/// Closes all submenus of the menu item.
/// </summary>
@ -437,9 +453,13 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param>
private void CanExecuteChanged(object sender, EventArgs e)
{
// HACK: Just set the IsEnabled property for the moment. This needs to be changed to
// use IsEnabledCore etc. but it will do for now.
IsEnabled = Command == null || Command.CanExecute(CommandParameter);
var canExecute = Command == null || Command.CanExecute(CommandParameter);
if (canExecute != _commandCanExecute)
{
_commandCanExecute = canExecute;
UpdateIsEffectivelyEnabled();
}
}
/// <summary>

2
src/Avalonia.Input/FocusManager.cs

@ -146,7 +146,7 @@ namespace Avalonia.Input
/// </summary>
/// <param name="e">The element.</param>
/// <returns>True if the element can be focused.</returns>
private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible;
private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEffectivelyEnabled && e.IsVisible;
/// <summary>
/// Gets the focus scope ancestors of the specified control, traversing popups.

6
src/Avalonia.Input/IInputElement.cs

@ -83,14 +83,14 @@ namespace Avalonia.Input
Cursor Cursor { get; }
/// <summary>
/// Gets a value indicating whether the control is effectively enabled for user interaction.
/// Gets a value indicating whether this control and all its parents are enabled.
/// </summary>
/// <remarks>
/// The <see cref="IsEnabled"/> property is used to toggle the enabled state for individual
/// controls. The <see cref="IsEnabledCore"/> property takes into account the
/// controls. The <see cref="IsEffectivelyEnabled"/> property takes into account the
/// <see cref="IsEnabled"/> value of this control and its parent controls.
/// </remarks>
bool IsEnabledCore { get; }
bool IsEffectivelyEnabled { get; }
/// <summary>
/// Gets a value indicating whether the control is focused.

83
src/Avalonia.Input/InputElement.cs

@ -28,10 +28,12 @@ namespace Avalonia.Input
AvaloniaProperty.Register<InputElement, bool>(nameof(IsEnabled), true);
/// <summary>
/// Defines the <see cref="IsEnabledCore"/> property.
/// Defines the <see cref="IsEffectivelyEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsEnabledCoreProperty =
AvaloniaProperty.Register<InputElement, bool>(nameof(IsEnabledCore), true);
public static readonly DirectProperty<InputElement, bool> IsEffectivelyEnabledProperty =
AvaloniaProperty.RegisterDirect<InputElement, bool>(
nameof(IsEffectivelyEnabled),
o => o.IsEffectivelyEnabled);
/// <summary>
/// Gets or sets associated mouse cursor.
@ -155,6 +157,7 @@ namespace Avalonia.Input
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> DoubleTappedEvent = Gestures.DoubleTappedEvent;
private bool _isEffectivelyEnabled = true;
private bool _isFocused;
private bool _isPointerOver;
private GestureRecognizerCollection _gestureRecognizers;
@ -179,7 +182,7 @@ namespace Avalonia.Input
PointerCaptureLostEvent.AddClassHandler<InputElement>(x => x.OnPointerCaptureLost);
PointerWheelChangedEvent.AddClassHandler<InputElement>(x => x.OnPointerWheelChanged);
PseudoClass<InputElement, bool>(IsEnabledCoreProperty, x => !x, ":disabled");
PseudoClass<InputElement, bool>(IsEffectivelyEnabledProperty, x => !x, ":disabled");
PseudoClass<InputElement>(IsFocusedProperty, ":focus");
PseudoClass<InputElement>(IsPointerOverProperty, ":pointerover");
}
@ -365,31 +368,25 @@ namespace Avalonia.Input
internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); }
}
/// <summary>
/// Gets a value indicating whether the control is effectively enabled for user interaction.
/// </summary>
/// <remarks>
/// The <see cref="IsEnabled"/> property is used to toggle the enabled state for individual
/// controls. The <see cref="IsEnabledCore"/> property takes into account the
/// <see cref="IsEnabled"/> value of this control and its parent controls.
/// </remarks>
bool IInputElement.IsEnabledCore => IsEnabledCore;
/// <inheritdoc/>
public bool IsEffectivelyEnabled
{
get => _isEffectivelyEnabled;
private set => SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value);
}
public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>();
/// <summary>
/// Gets a value indicating whether the control is effectively enabled for user interaction.
/// Allows a derived class to override the enabled state of the control.
/// </summary>
/// <remarks>
/// The <see cref="IsEnabled"/> property is used to toggle the enabled state for individual
/// controls. The <see cref="IsEnabledCore"/> property takes into account the
/// <see cref="IsEnabled"/> value of this control and its parent controls.
/// Derived controls may wish to disable the enabled state of the control without overwriting the
/// user-supplied <see cref="IsEnabled"/> setting. This can be done by overriding this property
/// to return the overridden enabled state. If the value returned from <see cref="IsEnabledCore"/>
/// should change, then the derived control should call <see cref="UpdateIsEffectivelyEnabled()"/>.
/// </remarks>
protected bool IsEnabledCore
{
get { return GetValue(IsEnabledCoreProperty); }
set { SetValue(IsEnabledCoreProperty, value); }
}
public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>();
protected virtual bool IsEnabledCore => IsEnabled;
public GestureRecognizerCollection GestureRecognizers
=> _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this));
@ -417,7 +414,7 @@ namespace Avalonia.Input
protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTreeCore(e);
UpdateIsEnabledCore();
UpdateIsEffectivelyEnabled();
}
/// <summary>
@ -525,9 +522,18 @@ namespace Avalonia.Input
{
}
/// <summary>
/// Updates the <see cref="IsEffectivelyEnabled"/> property value according to the parent
/// control's enabled state and <see cref="IsEnabledCore"/>.
/// </summary>
protected void UpdateIsEffectivelyEnabled()
{
UpdateIsEffectivelyEnabled(this.GetVisualParent<InputElement>());
}
private static void IsEnabledChanged(AvaloniaPropertyChangedEventArgs e)
{
((InputElement)e.Sender).UpdateIsEnabledCore();
((InputElement)e.Sender).UpdateIsEffectivelyEnabled();
}
/// <summary>
@ -551,32 +557,17 @@ namespace Avalonia.Input
}
/// <summary>
/// Updates the <see cref="IsEnabledCore"/> property value.
/// </summary>
private void UpdateIsEnabledCore()
{
UpdateIsEnabledCore(this.GetVisualParent<InputElement>());
}
/// <summary>
/// Updates the <see cref="IsEnabledCore"/> property based on the parent's
/// <see cref="IsEnabledCore"/>.
/// Updates the <see cref="IsEffectivelyEnabled"/> property based on the parent's
/// <see cref="IsEffectivelyEnabled"/>.
/// </summary>
/// <param name="parent">The parent control.</param>
private void UpdateIsEnabledCore(InputElement parent)
private void UpdateIsEffectivelyEnabled(InputElement parent)
{
if (parent != null)
{
IsEnabledCore = IsEnabled && parent.IsEnabledCore;
}
else
{
IsEnabledCore = IsEnabled;
}
IsEffectivelyEnabled = IsEnabledCore && (parent?.IsEffectivelyEnabled ?? true);
foreach (var child in this.GetVisualChildren().OfType<InputElement>())
{
child.UpdateIsEnabledCore(this);
child.UpdateIsEffectivelyEnabled(this);
}
}
}

2
src/Avalonia.Input/InputExtensions.cs

@ -45,7 +45,7 @@ namespace Avalonia.Input
return element != null &&
element.IsVisible &&
element.IsHitTestVisible &&
element.IsEnabledCore &&
element.IsEffectivelyEnabled &&
element.IsAttachedToVisualTree;
}
}

4
src/Avalonia.Input/Navigation/FocusExtensions.cs

@ -13,13 +13,13 @@ namespace Avalonia.Input.Navigation
/// </summary>
/// <param name="e">The element.</param>
/// <returns>True if the element can be focused.</returns>
public static bool CanFocus(this IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible;
public static bool CanFocus(this IInputElement e) => e.Focusable && e.IsEffectivelyEnabled && e.IsVisible;
/// <summary>
/// Checks if descendants of the specified element can be focused.
/// </summary>
/// <param name="e">The element.</param>
/// <returns>True if descendants of the element can be focused.</returns>
public static bool CanFocusDescendants(this IInputElement e) => e.IsEnabledCore && e.IsVisible;
public static bool CanFocusDescendants(this IInputElement e) => e.IsEffectivelyEnabled && e.IsVisible;
}
}

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

@ -26,11 +26,26 @@ namespace Avalonia.Controls.UnitTests
};
var root = new TestRoot { Child = target };
Assert.False(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
command.IsEnabled = true;
Assert.True(target.IsEnabled);
Assert.True(target.IsEffectivelyEnabled);
command.IsEnabled = false;
Assert.False(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
}
[Fact]
public void Button_Is_Disabled_When_Command_Is_Enabled_But_IsEnabled_Is_False()
{
var command = new TestCommand(true);
var target = new Button
{
IsEnabled = false,
Command = command,
};
var root = new TestRoot { Child = target };
Assert.False(((IInputElement)target).IsEffectivelyEnabled);
}
[Fact]
@ -41,7 +56,8 @@ namespace Avalonia.Controls.UnitTests
[!Button.CommandProperty] = new Binding("Command"),
};
Assert.False(target.IsEnabled);
Assert.True(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
}
[Fact]
@ -59,8 +75,12 @@ namespace Avalonia.Controls.UnitTests
};
Assert.True(target.IsEnabled);
Assert.True(target.IsEffectivelyEnabled);
target.DataContext = null;
Assert.False(target.IsEnabled);
Assert.True(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
}
[Fact]
@ -77,9 +97,13 @@ namespace Avalonia.Controls.UnitTests
[!Button.CommandProperty] = new Binding("Command"),
};
Assert.False(target.IsEnabled);
Assert.True(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
target.DataContext = viewModel;
Assert.True(target.IsEnabled);
Assert.True(target.IsEffectivelyEnabled);
}
[Fact]
@ -96,9 +120,13 @@ namespace Avalonia.Controls.UnitTests
[!Button.CommandProperty] = new Binding("Command"),
};
Assert.False(target.IsEnabled);
Assert.True(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
target.DataContext = viewModel;
Assert.False(target.IsEnabled);
Assert.True(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
}
[Fact]

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

@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.Text;
using System.Windows.Input;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.UnitTests;
using Xunit;
@ -25,6 +27,103 @@ namespace Avalonia.Controls.UnitTests
Assert.False(target.Focusable);
}
[Fact]
public void MenuItem_Is_Disabled_When_Command_Is_Enabled_But_IsEnabled_Is_False()
{
var command = new TestCommand(true);
var target = new MenuItem
{
IsEnabled = false,
Command = command,
};
var root = new TestRoot { Child = target };
Assert.False(((IInputElement)target).IsEffectivelyEnabled);
}
[Fact]
public void MenuItem_Is_Disabled_When_Bound_Command_Doesnt_Exist()
{
var target = new MenuItem
{
[!MenuItem.CommandProperty] = new Binding("Command"),
};
Assert.True(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
}
[Fact]
public void MenuItem_Is_Disabled_When_Bound_Command_Is_Removed()
{
var viewModel = new
{
Command = new TestCommand(true),
};
var target = new MenuItem
{
DataContext = viewModel,
[!MenuItem.CommandProperty] = new Binding("Command"),
};
Assert.True(target.IsEnabled);
Assert.True(target.IsEffectivelyEnabled);
target.DataContext = null;
Assert.True(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
}
[Fact]
public void MenuItem_Is_Enabled_When_Bound_Command_Is_Added()
{
var viewModel = new
{
Command = new TestCommand(true),
};
var target = new MenuItem
{
DataContext = new object(),
[!MenuItem.CommandProperty] = new Binding("Command"),
};
Assert.True(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
target.DataContext = viewModel;
Assert.True(target.IsEnabled);
Assert.True(target.IsEffectivelyEnabled);
}
[Fact]
public void MenuItem_Is_Disabled_When_Disabled_Bound_Command_Is_Added()
{
var viewModel = new
{
Command = new TestCommand(false),
};
var target = new MenuItem
{
DataContext = new object(),
[!MenuItem.CommandProperty] = new Binding("Command"),
};
Assert.True(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
target.DataContext = viewModel;
Assert.True(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
}
[Fact]
public void MenuItem_Does_Not_Subscribe_To_Command_CanExecuteChanged_Until_Added_To_Logical_Tree()
{
@ -60,8 +159,14 @@ namespace Avalonia.Controls.UnitTests
private class TestCommand : ICommand
{
private bool _enabled;
private EventHandler _canExecuteChanged;
public TestCommand(bool enabled = true)
{
_enabled = enabled;
}
public int SubscriptionCount { get; private set; }
public event EventHandler CanExecuteChanged
@ -70,7 +175,7 @@ namespace Avalonia.Controls.UnitTests
remove { _canExecuteChanged -= value; --SubscriptionCount; }
}
public bool CanExecute(object parameter) => true;
public bool CanExecute(object parameter) => _enabled;
public void Execute(object parameter)
{

101
tests/Avalonia.Input.UnitTests/InputElement_Enabled.cs

@ -0,0 +1,101 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls;
using Xunit;
namespace Avalonia.Input.UnitTests
{
public class InputElement_Enabled
{
[Fact]
public void IsEffectivelyEnabled_Follows_IsEnabled()
{
var target = new Decorator();
Assert.True(target.IsEnabled);
Assert.True(target.IsEffectivelyEnabled);
target.IsEnabled = false;
Assert.False(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
}
[Fact]
public void IsEffectivelyEnabled_Follows_Ancestor_IsEnabled()
{
Decorator child;
Decorator grandchild;
var target = new Decorator
{
Child = child = new Decorator
{
Child = grandchild = new Decorator(),
}
};
Assert.True(target.IsEnabled);
Assert.True(target.IsEffectivelyEnabled);
Assert.True(child.IsEnabled);
Assert.True(child.IsEffectivelyEnabled);
Assert.True(grandchild.IsEnabled);
Assert.True(grandchild.IsEffectivelyEnabled);
target.IsEnabled = false;
Assert.False(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
Assert.True(child.IsEnabled);
Assert.False(child.IsEffectivelyEnabled);
Assert.True(grandchild.IsEnabled);
Assert.False(grandchild.IsEffectivelyEnabled);
}
[Fact]
public void Disabled_Pseudoclass_Follows_IsEffectivelyEnabled()
{
Decorator child;
var target = new Decorator
{
Child = child = new Decorator()
};
Assert.DoesNotContain(":disabled", child.Classes);
target.IsEnabled = false;
Assert.Contains(":disabled", child.Classes);
}
[Fact]
public void IsEffectivelyEnabled_Respects_IsEnabledCore()
{
Decorator child;
var target = new TestControl
{
Child = child = new Decorator()
};
target.ShouldEnable = false;
Assert.True(target.IsEnabled);
Assert.False(target.IsEffectivelyEnabled);
Assert.True(child.IsEnabled);
Assert.False(child.IsEffectivelyEnabled);
}
private class TestControl : Decorator
{
private bool _shouldEnable;
public bool ShouldEnable
{
get => _shouldEnable;
set { _shouldEnable = value; UpdateIsEffectivelyEnabled(); }
}
protected override bool IsEnabledCore => IsEnabled && _shouldEnable;
}
}
}
Loading…
Cancel
Save