diff --git a/src/Avalonia.Base/Styling/EventSetter.cs b/src/Avalonia.Base/Styling/EventSetter.cs
new file mode 100644
index 0000000000..6650357a0c
--- /dev/null
+++ b/src/Avalonia.Base/Styling/EventSetter.cs
@@ -0,0 +1,72 @@
+using System;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.PropertyStore;
+using Avalonia.Threading;
+
+namespace Avalonia.Styling;
+
+///
+/// Represents an event setter in a style. Event setters invoke the specified event handlers in response to events.
+///
+public sealed class EventSetter : SetterBase, ISetterInstance
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public EventSetter()
+ {
+
+ }
+
+ ///
+ /// Initializes a new instance of the class, using the provided event and handler parameters.
+ ///
+ /// The particular routed event that the responds to.
+ /// The handler to assign in this setter.
+ public EventSetter(RoutedEvent eventName, Delegate handler)
+ {
+ Event = eventName;
+ Handler = handler;
+ }
+
+ ///
+ /// Gets or sets the particular routed event that this EventSetter responds to.
+ ///
+ public RoutedEvent? Event { get; set; }
+
+ ///
+ /// Gets or sets the reference to a handler for a routed event in the setter.
+ ///
+ public Delegate? Handler { get; set; }
+
+ ///
+ /// The routing strategies to listen to.
+ ///
+ public RoutingStrategies Routes { get; set; } = RoutingStrategies.Bubble | RoutingStrategies.Direct;
+
+ ///
+ /// Gets or sets a value that determines whether the handler assigned to the setter should still be invoked, even if the event is marked handled in its event data.
+ ///
+ public bool HandledEventsToo { get; set; }
+
+ internal override ISetterInstance Instance(IStyleInstance styleInstance, StyledElement target)
+ {
+ if (target is null)
+ throw new ArgumentNullException(nameof(target));
+
+ if (Event is null)
+ throw new InvalidOperationException($"{nameof(EventSetter)}.{nameof(Event)} must be set.");
+
+ if (Handler is null)
+ throw new InvalidOperationException($"{nameof(EventSetter)}.{nameof(Handler)} must be set.");
+
+ if (target is not InputElement inputElement)
+ throw new ArgumentException($"{nameof(EventSetter)} target must be a {nameof(InputElement)}", nameof(target));
+
+ if (styleInstance.HasActivator)
+ throw new InvalidOperationException("EventSetter cannot be used in styles with activators, i.e. styles with complex selectors.");
+
+ return new EventSetterInstance(inputElement, Event, Handler, Routes, HandledEventsToo);
+ }
+}
diff --git a/src/Avalonia.Base/Styling/EventSetterInstance.cs b/src/Avalonia.Base/Styling/EventSetterInstance.cs
new file mode 100644
index 0000000000..df1268fecf
--- /dev/null
+++ b/src/Avalonia.Base/Styling/EventSetterInstance.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Reflection;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.PropertyStore;
+
+namespace Avalonia.Styling;
+
+internal class EventSetterInstance : ISetterInstance, IValueEntry
+{
+ private readonly InputElement _inputElement;
+ private readonly RoutedEvent _routedEvent;
+ private readonly Delegate _handler;
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1010")]
+ private static readonly StyledProperty s_eventSetterProperty = AvaloniaProperty
+ .Register("EventSetter");
+
+ public EventSetterInstance(InputElement inputElement, RoutedEvent routedEvent, Delegate handler,
+ RoutingStrategies routes, bool handledEventsToo)
+ {
+ _inputElement = inputElement;
+ _routedEvent = routedEvent;
+ _handler = handler;
+
+ inputElement.AddHandler(routedEvent, handler, routes, handledEventsToo);
+ }
+
+ public AvaloniaProperty Property => s_eventSetterProperty;
+ public bool HasValue() => false;
+ public object GetValue() => BindingOperations.DoNothing;
+
+ public bool GetDataValidationState(out BindingValueType state, out Exception? error)
+ {
+ state = BindingValueType.DoNothing;
+ error = null;
+ return false;
+ }
+
+ public void Unsubscribe()
+ {
+ _inputElement.RemoveHandler(_routedEvent, _handler);
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
index 9d1f439bab..396296b034 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
@@ -62,6 +62,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlPropertyPathTransformer(),
new AvaloniaXamlIlSetterTargetTypeMetadataTransformer(),
new AvaloniaXamlIlSetterTransformer(),
+ new AvaloniaXamlIlEventSetterTransformer(),
new AvaloniaXamlIlStyleValidatorTransformer(),
new AvaloniaXamlIlConstructorServiceProviderTransformer(),
new AvaloniaXamlIlTransitionsTypeMetadataTransformer(),
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlEventSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlEventSetterTransformer.cs
new file mode 100644
index 0000000000..ed5d78a457
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlEventSetterTransformer.cs
@@ -0,0 +1,133 @@
+using System.Collections.Generic;
+using System.Linq;
+using XamlX.Ast;
+using XamlX.Transform;
+using XamlX.TypeSystem;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
+
+internal class AvaloniaXamlIlEventSetterTransformer : IXamlAstTransformer
+{
+ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+ {
+ if (!(node is XamlAstObjectNode on
+ && on.Type.GetClrType().FullName == "Avalonia.Styling.EventSetter"))
+ return node;
+
+ IXamlType targetType = null;
+
+ var styleParent = context.ParentNodes()
+ .OfType()
+ .FirstOrDefault(x => x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style);
+
+ if (styleParent != null)
+ {
+ targetType = styleParent.TargetType.GetClrType()
+ ?? throw new XamlStyleTransformException("Can not find parent Style Selector or ControlTemplate TargetType. If setter is not part of the style, you can set x:SetterTargetType directive on its parent.", node);
+ }
+
+ if (targetType == null)
+ {
+ throw new XamlStyleTransformException("Could not determine target type of Setter", node);
+ }
+
+ var routedEventProp = on.Children.OfType()
+ .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Event");
+ var routedEventType = context.GetAvaloniaTypes().RoutedEvent;
+ var routedEventTType = context.GetAvaloniaTypes().RoutedEventT;
+ IXamlType actualRoutedEventType;
+ if (routedEventProp != null)
+ {
+ if (routedEventProp.Values.OfType().FirstOrDefault() is not { } valueNode)
+ {
+ var eventName = routedEventProp.Values.OfType().FirstOrDefault()?.Text;
+ if (eventName == null)
+ throw new XamlStyleTransformException("EventSetter.Event must be an event name or x:Static expression to an event.", node);
+
+ // Ideally, compiler should do one of these lookups:
+ // - Runtime lookup in RoutedEventRegistry. But can't do that in compiler. Could be the best approach, if we inject RoutedEventRegistry extra code, still no compile time alidation.
+ // - Lookup for CLR events with this name and routed args. But there might not be a CLR event at all. And we can't reliably get RoutedEvent instance from there.
+ // - Lookup for AddEventNameHandler methods. But the same - there might not be one.
+ // Instead this transformer searches for routed event definition in one of base classes. Not ideal too, it won't handle attached events. Still better than others.
+ // Combining with ability to use x:Static for uncommon use-cases this approach should work well.
+ if (!eventName.EndsWith("Event"))
+ {
+ eventName += "Event";
+ }
+
+ IXamlType type = targetType, nextType = targetType;
+ IXamlMember member = null;
+ while (nextType is not null && member is null)
+ {
+ type = nextType;
+ member = type?.Fields.FirstOrDefault(f => f.IsPublic && f.IsStatic && f.Name == eventName) ??
+ (IXamlMember)type?.GetAllProperties().FirstOrDefault(p =>
+ p.Name == eventName && p.Getter is { IsPublic: true, IsStatic: true });
+ nextType = type.BaseType;
+ }
+
+ if (member is null)
+ throw new XamlStyleTransformException($"EventSetter.Event with name \"{eventName}\" wasn't found.", node);
+
+ valueNode = new XamlStaticExtensionNode(on,
+ new XamlAstClrTypeReference(routedEventProp, type, false), member.Name);
+
+ routedEventProp.Values = new List {valueNode};
+ }
+
+ actualRoutedEventType = valueNode.Type.GetClrType();
+ if (!routedEventType.IsAssignableFrom(actualRoutedEventType))
+ throw new XamlStyleTransformException("EventSetter.Event must be assignable to RoutedEvent type.", node);
+
+ // Get RoutedEvent or RoutedEvent base type from the field, so we can get generic parameter below.
+ // This helps to ignore any other possible MyRoutedEvent : RoutedEvent type definitions.
+ while (!(actualRoutedEventType.FullName.StartsWith(routedEventType.FullName)
+ || actualRoutedEventType.FullName.StartsWith(routedEventTType.FullName)))
+ {
+ actualRoutedEventType = actualRoutedEventType.BaseType;
+ }
+ }
+ else
+ {
+ throw new XamlStyleTransformException($"EventSetter.Event must be set.", node);
+ }
+
+ var handlerProp = on.Children.OfType()
+ .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Handler");
+ if (handlerProp != null)
+ {
+ var handlerName = handlerProp.Values.OfType().FirstOrDefault()?.Text;
+ if (handlerName == null)
+ throw new XamlStyleTransformException("EventSetter.Handler must be a method name.", node);
+
+ var rootType = context.RootObject?.Type.GetClrType();
+ var argsType = actualRoutedEventType.GenericArguments.Any() ?
+ actualRoutedEventType.GenericArguments[0] :
+ context.GetAvaloniaTypes().RoutedEventArgs;
+
+ var handler = rootType?.FindMethod(
+ handlerName,
+ context.Configuration.WellKnownTypes.Void,
+ true,
+ new[] { context.Configuration.WellKnownTypes.Object, argsType });
+ if (handler != null)
+ {
+ var delegateType = context.Configuration.TypeSystem.GetType("System.EventHandler`1").MakeGenericType(argsType);
+ var handlerInvoker = new XamlLoadMethodDelegateNode(handlerProp, context.RootObject, delegateType, handler);
+ handlerProp.Values = new List { handlerInvoker };
+ }
+ else
+ {
+ throw new XamlStyleTransformException(
+ $"EventSetter.Handler with name \"{handlerName}\" wasn't found on type \"{rootType?.Name ?? "(null)"}\"." +
+ $"EventSetter is supported only on XAML files with x:Class.", node);
+ }
+ }
+ else
+ {
+ throw new XamlStyleTransformException($"EventSetter.Handler must be set.", node);
+ }
+
+ return on;
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
index 89d2518e2d..0e83c77aab 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
@@ -18,6 +18,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType AvaloniaProperty { get; }
public IXamlType AvaloniaPropertyT { get; }
public IXamlType StyledPropertyT { get; }
+ public IXamlType RoutedEvent { get; }
+ public IXamlType RoutedEventT { get; }
+ public IXamlType RoutedEventArgs { get; }
public IXamlMethod AvaloniaObjectSetStyledPropertyValue { get; }
public IXamlType AvaloniaAttachedPropertyT { get; }
public IXamlType IBinding { get; }
@@ -135,6 +138,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
AvaloniaProperty = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty");
AvaloniaPropertyT = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty`1");
StyledPropertyT = cfg.TypeSystem.GetType("Avalonia.StyledProperty`1");
+ RoutedEvent = cfg.TypeSystem.GetType("Avalonia.Interactivity.RoutedEvent");
+ RoutedEventT = cfg.TypeSystem.GetType("Avalonia.Interactivity.RoutedEvent`1");
+ RoutedEventArgs = cfg.TypeSystem.GetType("Avalonia.Interactivity.RoutedEventArgs");
AvaloniaAttachedPropertyT = cfg.TypeSystem.GetType("Avalonia.AttachedProperty`1");
BindingPriority = cfg.TypeSystem.GetType("Avalonia.Data.BindingPriority");
AvaloniaObjectSetStyledPropertyValue = AvaloniaObject
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/SetterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/SetterTests.cs
index 6fc0f2d91c..ae172f738e 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/SetterTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/SetterTests.cs
@@ -1,4 +1,7 @@
-using Avalonia.Controls;
+using System;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Xunit;
@@ -48,4 +51,53 @@ public class SetterTests : XamlTestBase
Assert.Equal(typeof(ContentControl), setter.Property.OwnerType);
}
}
+
+ [Theory]
+ [InlineData("{x:Static InputElement.KeyDownEvent}","OnKeyDown")]
+ [InlineData("KeyDown","OnKeyDown")]
+ [InlineData("KeyDown", "OnKeyDownUnspecific")]
+ [InlineData("KeyDown","OnKeyDownStatic")]
+ public void EventSetter_Should_Be_Registered(string eventName, string handlerName)
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var window = (WindowWithEventHandler)AvaloniaRuntimeXamlLoader.Load($"""
+
+
+
+
+
+""");
+ var callbackCalled = false;
+ window.Callback += _ => callbackCalled = true;
+
+ window.Show();
+
+ window.RaiseEvent(new KeyEventArgs() { RoutedEvent = InputElement.KeyDownEvent });
+
+ Assert.True(callbackCalled);
+ }
+ }
+}
+
+public class WindowWithEventHandler : Window
+{
+ public Action Callback;
+ public void OnKeyDown(object sender, KeyEventArgs e)
+ {
+ Callback?.Invoke(e);
+ }
+ public void OnKeyDownUnspecific(object sender, RoutedEventArgs e)
+ {
+ Callback?.Invoke(e);
+ }
+ public static void OnKeyDownStatic(object sender, RoutedEventArgs e)
+ {
+ ((WindowWithEventHandler)sender).Callback?.Invoke(e);
+ }
}