diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs
index 3c19b47a05..2ad220dddd 100644
--- a/src/Avalonia.Base/Logging/LogArea.cs
+++ b/src/Avalonia.Base/Logging/LogArea.cs
@@ -34,5 +34,10 @@ namespace Avalonia.Logging
/// The log event comes from the control system.
///
public const string Control = "Control";
+
+ ///
+ /// The log event comes from Win32Platform.
+ ///
+ public const string Win32Platform = nameof(Win32Platform);
}
}
diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs
index e94d00b2ff..91eef3947b 100644
--- a/src/Avalonia.Controls/Button.cs
+++ b/src/Avalonia.Controls/Button.cs
@@ -30,7 +30,7 @@ namespace Avalonia.Controls
/// A button control.
///
[PseudoClasses(":pressed")]
- public class Button : ContentControl
+ public class Button : ContentControl, ICommandSource
{
///
/// Defines the property.
@@ -492,5 +492,7 @@ namespace Avalonia.Controls
{
PseudoClasses.Set(":pressed", isPressed);
}
+
+ void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e);
}
}
diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs
index 95752e7875..b76b7fbed5 100644
--- a/src/Avalonia.Controls/HotkeyManager.cs
+++ b/src/Avalonia.Controls/HotkeyManager.cs
@@ -12,18 +12,21 @@ namespace Avalonia.Controls
class HotkeyCommandWrapper : ICommand
{
- public HotkeyCommandWrapper(IControl control)
+ public HotkeyCommandWrapper(ICommandSource control)
{
- Control = control;
+ CommandSource = control;
}
- public readonly IControl Control;
+ public readonly ICommandSource CommandSource;
- private ICommand GetCommand() => Control.GetValue(Button.CommandProperty);
+ private ICommand GetCommand() => CommandSource.Command;
- public bool CanExecute(object parameter) => GetCommand()?.CanExecute(parameter) ?? false;
+ public bool CanExecute(object parameter) =>
+ CommandSource.Command?.CanExecute(CommandSource.CommandParameter) == true
+ && CommandSource.IsEffectivelyEnabled;
- public void Execute(object parameter) => GetCommand()?.Execute(parameter);
+ public void Execute(object parameter) =>
+ GetCommand()?.Execute(CommandSource.CommandParameter);
#pragma warning disable 67 // Event not used
public event EventHandler CanExecuteChanged;
@@ -44,7 +47,7 @@ namespace Avalonia.Controls
public Manager(IControl control)
{
_control = control;
- _wrapper = new HotkeyCommandWrapper(_control);
+ _wrapper = new HotkeyCommandWrapper(_control as ICommandSource);
}
public void Init()
@@ -84,7 +87,7 @@ namespace Avalonia.Controls
{
if (_root != null && _hotkey != null)
{
- _binding = new KeyBinding() {Gesture = _hotkey, Command = _wrapper};
+ _binding = new KeyBinding() { Gesture = _hotkey, Command = _wrapper };
_root.KeyBindings.Add(_binding);
}
}
@@ -102,8 +105,13 @@ namespace Avalonia.Controls
HotKeyProperty.Changed.Subscribe(args =>
{
var control = args.Sender as IControl;
- if (args.OldValue != null|| control == null)
+ if (args.OldValue != null || control == null || !(control is ICommandSource))
+ {
+ Logging.Logger.TryGet(Logging.LogEventLevel.Warning, Logging.LogArea.Control)?.
+ Log(control, $"The element {args.Sender.GetType().Name} does not support binding a HotKey ({args.NewValue}).");
return;
+ }
+
new Manager(control).Init();
});
}
diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs
index 7d4fef009d..d034f54df2 100644
--- a/src/Avalonia.Controls/MenuItem.cs
+++ b/src/Avalonia.Controls/MenuItem.cs
@@ -22,7 +22,7 @@ namespace Avalonia.Controls
/// A menu item control.
///
[PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")]
- public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable
+ public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource
{
///
/// Defines the property.
@@ -609,6 +609,8 @@ namespace Avalonia.Controls
SelectedItem = null;
}
+ void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e);
+
///
/// A dependency resolver which returns a .
///
diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs
index 76197b62ad..2eaf24d2f2 100644
--- a/src/Avalonia.Controls/NativeMenuItem.cs
+++ b/src/Avalonia.Controls/NativeMenuItem.cs
@@ -150,7 +150,7 @@ namespace Avalonia.Controls
void CanExecuteChanged()
{
- IsEnabled = _command?.CanExecute(null) ?? true;
+ IsEnabled = _command?.CanExecute(CommandParameter) ?? true;
}
public bool HasClickHandlers => Click != null;
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
index 3623e95597..7dd4ed0832 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
@@ -12,6 +12,9 @@
+
diff --git a/src/Avalonia.Input/ICommandSource.cs b/src/Avalonia.Input/ICommandSource.cs
new file mode 100644
index 0000000000..ba2e8eed4e
--- /dev/null
+++ b/src/Avalonia.Input/ICommandSource.cs
@@ -0,0 +1,36 @@
+using System.Windows.Input;
+
+namespace Avalonia.Input
+{
+ ///
+ /// An interface for classes that know how to invoke a Command.
+ ///
+ public interface ICommandSource
+ {
+ ///
+ /// The command that will be executed when the class is "invoked."
+ /// Classes that implement this interface should enable or disable based on the command's CanExecute return value.
+ /// The property may be implemented as read-write if desired.
+ ///
+ ICommand Command { get; }
+
+ ///
+ /// The parameter that will be passed to the command when executing the command.
+ /// The property may be implemented as read-write if desired.
+ ///
+ object CommandParameter { get; }
+
+
+ ///
+ /// Bor the bheavior CanExecuteChanged
+ ///
+ ///
+ ///
+ void CanExecuteChanged(object sender, System.EventArgs e);
+
+ ///
+ /// Gets a value indicating whether this control and all its parents are enabled.
+ ///
+ bool IsEffectivelyEnabled { get; }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs
index 7944d8b569..24cded1d22 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs
@@ -72,16 +72,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
v.Property is AvaloniaSyntheticCompiledBindingProperty prop
&& prop.Name == SyntheticCompiledBindingPropertyName.ElementName);
- var sourceProperty = syntheticCompiledBindingProperties
- .FirstOrDefault(v =>
- v.Property is AvaloniaSyntheticCompiledBindingProperty prop
- && prop.Name == SyntheticCompiledBindingPropertyName.Source);
-
var relativeSourceProperty = syntheticCompiledBindingProperties
.FirstOrDefault(v =>
v.Property is AvaloniaSyntheticCompiledBindingProperty prop
&& prop.Name == SyntheticCompiledBindingPropertyName.RelativeSource);
+ var sourceProperty = binding.Children.OfType()
+ .FirstOrDefault(v =>
+ v.Property is XamlAstClrProperty prop
+ && prop.Name == "Source");
+
if (elementNameProperty?.Values[0] is XamlAstTextNode elementName)
{
convertedNode = new BindingExpressionGrammar.NameNode { Name = elementName.Text };
@@ -91,14 +91,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
throw new XamlParseException($"Invalid ElementName '{elementNameProperty.Values[0]}'.", elementNameProperty.Values[0]);
}
- if (sourceProperty?.Values[0] != null)
+ if (sourceProperty != null && convertedNode != null)
{
- if (convertedNode != null)
- {
- throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding);
- }
-
- convertedNode = new RawSourceBindingExpressionNode(sourceProperty?.Values[0]);
+ throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding);
}
if (GetRelativeSourceObjectFromAssignment(
@@ -223,10 +218,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
binding.Children.Remove(elementNameProperty);
}
- if (sourceProperty != null)
- {
- binding.Children.Remove(sourceProperty);
- }
if (relativeSourceProperty != null)
{
binding.Children.Remove(relativeSourceProperty);
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs
index fc32084687..4d8c940bbc 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Text;
-using XamlX;
using XamlX.Ast;
using XamlX.Transform;
using XamlX.TypeSystem;
@@ -15,14 +13,102 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
if (node is XamlAstConstructableObjectNode binding && binding.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension))
{
- IXamlType startType;
- var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault();
- if (parentDataContextNode is null)
+ IXamlType startType = null;
+ var sourceProperty = binding.Children.OfType().FirstOrDefault(c => c.Property.Name == "Source");
+ if ((sourceProperty?.Values.Count ?? 0) == 1)
{
- throw new XamlX.XamlParseException("Cannot parse a compiled binding without an explicit x:DataType directive to give a starting data type for bindings.", binding);
+ var sourceValue = sourceProperty.Values[0];
+ switch (sourceValue)
+ {
+ case XamlAstTextNode textNode:
+ startType = textNode.Type?.GetClrType();
+ break;
+
+ case XamlMarkupExtensionNode extension:
+ startType = extension.Type?.GetClrType();
+
+ //let's try to infer StaticResource type from parent resources in xaml
+ if (extension.Value.Type.GetClrType().FullName == "Avalonia.Markup.Xaml.MarkupExtensions.StaticResourceExtension" &&
+ extension.Value is XamlAstConstructableObjectNode cn &&
+ cn.Arguments.Count == 1 && cn.Arguments[0] is XamlAstTextNode keyNode)
+ {
+ bool matchProperty(IXamlAstNode node, IXamlType styledElementType, string propertyName)
+ {
+ return (node is XamlPropertyAssignmentNode p &&
+ p.Property.DeclaringType == styledElementType && p.Property.Name == propertyName)
+ ||
+ (node is XamlManipulationGroupNode m && m.Children.Count > 0 &&
+ m.Children[0] is XamlPropertyAssignmentNode pm &&
+ pm.Property.DeclaringType == styledElementType && pm.Property.Name == propertyName);
+ }
+
+ string getResourceValue_xKey(XamlPropertyAssignmentNode node)
+ => node.Values.Count == 2 && node.Values[0] is XamlAstTextNode t ? t.Text : "";
+
+ IXamlType getResourceValue_Type(XamlPropertyAssignmentNode node, IXamlType xamlType)
+ => node.Values.Count == 2 ? node.Values[1].Type.GetClrType() : xamlType;
+
+ IEnumerable getResourceValues(IXamlAstNode node)
+ {
+ if (node is XamlPropertyAssignmentNode propertyNode)
+ {
+ if (propertyNode.Values.Count == 1 &&
+ propertyNode.Values[0] is XamlAstConstructableObjectNode obj &&
+ obj.Type.GetClrType().FullName == "Avalonia.Controls.ResourceDictionary")
+ {
+ foreach (var r in obj.Children.SelectMany(c => getResourceValues(c)))
+ {
+ yield return r;
+ }
+ }
+ else
+ {
+ yield return propertyNode;
+ }
+ }
+ else if (node is XamlManipulationGroupNode m)
+ {
+ foreach (var r in m.Children.OfType())
+ {
+ yield return r;
+ }
+ }
+ }
+
+ string key = keyNode.Text;
+
+ var styledElement = context.GetAvaloniaTypes().StyledElement;
+ var resource = context.ParentNodes()
+ .OfType()
+ .Where(o => styledElement.IsAssignableFrom(o.Type.GetClrType()))
+ .Select(o => o.Children.FirstOrDefault(p => matchProperty(p, styledElement, "Resources")))
+ .Where(r => r != null)
+ .SelectMany(r => getResourceValues(r))
+ .FirstOrDefault(r => getResourceValue_xKey(r) == key);
+
+ if (resource != null)
+ {
+ startType = getResourceValue_Type(resource, startType);
+ }
+ }
+ break;
+
+ case XamlStaticExtensionNode staticExtension:
+ startType = staticExtension.Type?.GetClrType();
+ break;
+ }
}
- startType = parentDataContextNode.DataContextType;
+ if (startType == null)
+ {
+ var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault();
+ if (parentDataContextNode is null)
+ {
+ throw new XamlX.XamlParseException("Cannot parse a compiled binding without an explicit x:DataType directive to give a starting data type for bindings.", binding);
+ }
+
+ startType = parentDataContextNode.DataContextType;
+ }
XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startType);
}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs
index 154c6a235c..ba05347a52 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs
@@ -25,11 +25,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
return new AvaloniaSyntheticCompiledBindingProperty(node,
SyntheticCompiledBindingPropertyName.RelativeSource);
}
- else if (prop.Name == "Source")
- {
- return new AvaloniaSyntheticCompiledBindingProperty(node,
- SyntheticCompiledBindingPropertyName.Source);
- }
}
return node;
@@ -39,8 +34,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
enum SyntheticCompiledBindingPropertyName
{
ElementName,
- RelativeSource,
- Source
+ RelativeSource
}
class AvaloniaSyntheticCompiledBindingProperty : XamlAstNode, IXamlAstPropertyReference
diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
index da39920eb3..17d2ea7ae9 100644
--- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
@@ -32,6 +32,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Mode = Mode,
Priority = Priority,
StringFormat = StringFormat,
+ Source = Source,
DefaultAnchor = new WeakReference(GetDefaultAnchor(provider))
};
}
@@ -52,6 +53,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation)
{
+ if (Source != null)
+ {
+ return CreateSourceObserver(
+ Source,
+ Path.BuildExpression(enableDataValidation));
+ }
+
if (Path.RawSource != null)
{
return CreateSourceObserver(
@@ -77,5 +85,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
[ConstructorArgument("path")]
public CompiledBindingPath Path { get; set; }
+
+ public object Source { get; set; }
}
}
diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index d9f9e7a84e..b21741453c 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
@@ -931,8 +931,8 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll", EntryPoint = "MapVirtualKeyW")]
public static extern uint MapVirtualKey(uint uCode, uint uMapType);
- [DllImport("user32.dll", EntryPoint = "GetMessageW")]
- public static extern sbyte GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
+ [DllImport("user32.dll", EntryPoint = "GetMessageW", SetLastError = true)]
+ public static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
[DllImport("user32.dll")]
public static extern int GetMessageTime();
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index 1c519f10e7..97eb0c6119 100644
--- a/src/Windows/Avalonia.Win32/Win32Platform.cs
+++ b/src/Windows/Avalonia.Win32/Win32Platform.cs
@@ -130,21 +130,34 @@ namespace Avalonia.Win32
public void ProcessMessage()
{
- UnmanagedMethods.MSG msg;
- UnmanagedMethods.GetMessage(out msg, IntPtr.Zero, 0, 0);
- UnmanagedMethods.TranslateMessage(ref msg);
- UnmanagedMethods.DispatchMessage(ref msg);
+
+ if (UnmanagedMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0) > -1)
+ {
+ UnmanagedMethods.TranslateMessage(ref msg);
+ UnmanagedMethods.DispatchMessage(ref msg);
+ }
+ else
+ {
+ Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.Win32Platform)
+ ?.Log(this, "Unmanaged error in {0}. Error Code: {1}", nameof(ProcessMessage), Marshal.GetLastWin32Error());
+
+ }
}
public void RunLoop(CancellationToken cancellationToken)
{
- while (!cancellationToken.IsCancellationRequested)
+ var result = 0;
+ while (!cancellationToken.IsCancellationRequested
+ && (result = UnmanagedMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0)) > 0)
{
- UnmanagedMethods.MSG msg;
- UnmanagedMethods.GetMessage(out msg, IntPtr.Zero, 0, 0);
UnmanagedMethods.TranslateMessage(ref msg);
UnmanagedMethods.DispatchMessage(ref msg);
}
+ if (result < 0)
+ {
+ Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.Win32Platform)
+ ?.Log(this, "Unmanaged error in {0}. Error Code: {1}", nameof(RunLoop), Marshal.GetLastWin32Error());
+ }
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action callback)
diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs
index 9f2712c93c..151c3070f4 100644
--- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs
@@ -5,6 +5,9 @@ using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.Styling;
using Xunit;
+using System;
+using Avalonia.Input.Raw;
+using Factory = System.Func, Avalonia.Controls.Window, Avalonia.AvaloniaObject>;
namespace Avalonia.Controls.UnitTests.Utils
{
@@ -54,6 +57,127 @@ namespace Avalonia.Controls.UnitTests.Utils
}
}
+ [Theory]
+ [MemberData(nameof(ElementsFactory))]
+ public void HotKeyManager_Should_Use_CommandParameter(string factoryName, Factory factory)
+ {
+ using (AvaloniaLocator.EnterScope())
+ {
+ var styler = new Mock();
+ var target = new KeyboardDevice();
+ var commandResult = 0;
+ var expectedParameter = 1;
+ AvaloniaLocator.CurrentMutable
+ .Bind().ToConstant(new WindowingPlatformMock())
+ .Bind().ToConstant(styler.Object);
+
+ var gesture = new KeyGesture(Key.A, KeyModifiers.Control);
+
+ var action = new Action