6 changed files with 310 additions and 1 deletions
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue