Browse Source

Implement EventSetter

pull/15373/head
Max Katz 2 years ago
parent
commit
e45b62ae66
  1. 72
      src/Avalonia.Base/Styling/EventSetter.cs
  2. 45
      src/Avalonia.Base/Styling/EventSetterInstance.cs
  3. 1
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  4. 133
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlEventSetterTransformer.cs
  5. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  6. 54
      tests/Avalonia.Markup.Xaml.UnitTests/SetterTests.cs

72
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;
/// <summary>
/// Represents an event setter in a style. Event setters invoke the specified event handlers in response to events.
/// </summary>
public sealed class EventSetter : SetterBase, ISetterInstance
{
/// <summary>
/// Initializes a new instance of the <see cref="EventSetter"/> class.
/// </summary>
public EventSetter()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EventSetter"/> class, using the provided event and handler parameters.
/// </summary>
/// <param name="eventName">The particular routed event that the <see cref="EventSetter"/> responds to.</param>
/// <param name="handler">The handler to assign in this setter.</param>
public EventSetter(RoutedEvent eventName, Delegate handler)
{
Event = eventName;
Handler = handler;
}
/// <summary>
/// Gets or sets the particular routed event that this EventSetter responds to.
/// </summary>
public RoutedEvent? Event { get; set; }
/// <summary>
/// Gets or sets the reference to a handler for a routed event in the setter.
/// </summary>
public Delegate? Handler { get; set; }
/// <summary>
/// The routing strategies to listen to.
/// </summary>
public RoutingStrategies Routes { get; set; } = RoutingStrategies.Bubble | RoutingStrategies.Direct;
/// <summary>
/// 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.
/// </summary>
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);
}
}

45
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<EventSetterInstance> s_eventSetterProperty = AvaloniaProperty
.Register<InputElement, EventSetterInstance>("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);
}
}

1
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -62,6 +62,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlPropertyPathTransformer(), new AvaloniaXamlIlPropertyPathTransformer(),
new AvaloniaXamlIlSetterTargetTypeMetadataTransformer(), new AvaloniaXamlIlSetterTargetTypeMetadataTransformer(),
new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlSetterTransformer(),
new AvaloniaXamlIlEventSetterTransformer(),
new AvaloniaXamlIlStyleValidatorTransformer(), new AvaloniaXamlIlStyleValidatorTransformer(),
new AvaloniaXamlIlConstructorServiceProviderTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(),
new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), new AvaloniaXamlIlTransitionsTypeMetadataTransformer(),

133
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<AvaloniaXamlIlTargetTypeMetadataNode>()
.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<XamlAstXamlPropertyValueNode>()
.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<XamlStaticExtensionNode>().FirstOrDefault() is not { } valueNode)
{
var eventName = routedEventProp.Values.OfType<XamlAstTextNode>().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<IXamlAstValueNode> {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<T> base type from the field, so we can get generic parameter below.
// This helps to ignore any other possible MyRoutedEvent : RoutedEvent<MyArgs> 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<XamlAstXamlPropertyValueNode>()
.FirstOrDefault(x => x.Property.GetClrProperty().Name == "Handler");
if (handlerProp != null)
{
var handlerName = handlerProp.Values.OfType<XamlAstTextNode>().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<IXamlAstValueNode> { 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;
}
}

6
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 AvaloniaProperty { get; }
public IXamlType AvaloniaPropertyT { get; } public IXamlType AvaloniaPropertyT { get; }
public IXamlType StyledPropertyT { get; } public IXamlType StyledPropertyT { get; }
public IXamlType RoutedEvent { get; }
public IXamlType RoutedEventT { get; }
public IXamlType RoutedEventArgs { get; }
public IXamlMethod AvaloniaObjectSetStyledPropertyValue { get; } public IXamlMethod AvaloniaObjectSetStyledPropertyValue { get; }
public IXamlType AvaloniaAttachedPropertyT { get; } public IXamlType AvaloniaAttachedPropertyT { get; }
public IXamlType IBinding { get; } public IXamlType IBinding { get; }
@ -135,6 +138,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
AvaloniaProperty = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty"); AvaloniaProperty = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty");
AvaloniaPropertyT = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty`1"); AvaloniaPropertyT = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty`1");
StyledPropertyT = cfg.TypeSystem.GetType("Avalonia.StyledProperty`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"); AvaloniaAttachedPropertyT = cfg.TypeSystem.GetType("Avalonia.AttachedProperty`1");
BindingPriority = cfg.TypeSystem.GetType("Avalonia.Data.BindingPriority"); BindingPriority = cfg.TypeSystem.GetType("Avalonia.Data.BindingPriority");
AvaloniaObjectSetStyledPropertyValue = AvaloniaObject AvaloniaObjectSetStyledPropertyValue = AvaloniaObject

54
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.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -48,4 +51,53 @@ public class SetterTests : XamlTestBase
Assert.Equal(typeof(ContentControl), setter.Property.OwnerType); 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($"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Class='Avalonia.Markup.Xaml.UnitTests.WindowWithEventHandler'>
<Window.Styles>
<Style Selector='Window'>
<EventSetter Event='{eventName}'
Handler='{handlerName}' />
</Style>
</Window.Styles>
</Window>
""");
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<RoutedEventArgs> 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);
}
} }

Loading…
Cancel
Save