Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Every RoutedEvent should be usable as Attached Event #15274

Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ void InsertBeforeMany(Type[] types, params IXamlAstTransformer[] t)
InsertBeforeMany(new [] { typeof(DeferredContentTransformer), typeof(AvaloniaXamlIlCompiledBindingsMetadataRemover) },
new AvaloniaXamlIlDeferredResourceTransformer());

InsertBefore<AvaloniaXamlIlTransformInstanceAttachedProperties>(new AvaloniaXamlIlTransformRoutedEvent());

Transformers.Add(new AvaloniaXamlIlControlTemplatePriorityTransformer());
Transformers.Add(new AvaloniaXamlIlMetadataRemover());
Transformers.Add(new AvaloniaXamlIlEnsureResourceDictionaryCapacityTransformer());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using System.Collections.Generic;
using System.Linq;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.Transform;
using XamlX.TypeSystem;

namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;

internal class AvaloniaXamlIlTransformRoutedEvent : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (node is XamlAstNamePropertyReference prop
&& prop.TargetType is XamlAstClrTypeReference targetRef
&& prop.DeclaringType is XamlAstClrTypeReference declaringRef)
{
var xkt = context.GetAvaloniaTypes();
var interactiveType = xkt.Interactivity.Interactive;
var routedEventType = xkt.Interactivity.RoutedEvent;
var AddHandlerT = xkt.Interactivity.AddHandlerT;

if (interactiveType.IsAssignableFrom(targetRef.Type))
{
var eventName = $"{prop.Name}Event";
if (declaringRef.Type.GetAllFields().FirstOrDefault(f => f.IsStatic && f.Name == eventName) is { } eventField)
Comment on lines +26 to +27
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has the same problem, as in my EventSetter PR, since event name might not necessary match field name+"event".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should give ourselves a metric. In the transaction period, an analyzer could be created that signals the lack of convection as happens for avalonia properties.

{
if (routedEventType.IsAssignableFrom(eventField.FieldType))
{
var instance = new XamlAstClrProperty(prop
, prop.Name
, targetRef.Type
, null
);
instance.Setters.Add(new XamlDirectCallAddHandler(eventField,
targetRef.Type,
xkt.Interactivity.AddHandler,
xkt.Interactivity.RoutedEventHandler
)
);
if (eventField.FieldType.GenericArguments?.Count == 1)
{
var agrument = eventField.FieldType.GenericArguments[0];
if (!agrument.Equals(xkt.Interactivity.RoutedEventArgs))
{
instance.Setters.Add(new XamlDirectCallAddHandler(eventField,
targetRef.Type,
xkt.Interactivity.AddHandlerT.MakeGenericMethod([agrument]),
xkt.EventHandlerT.MakeGenericType(agrument)
)
);
}
}
return instance;
}
else
{
context.ReportDiagnostic(new XamlX.XamlDiagnostic(
AvaloniaXamlDiagnosticCodes.TransformError,
XamlX.XamlDiagnosticSeverity.Error,
$"Event definition {prop.Name} found, but its type {eventField.FieldType.GetFqn()} is not compatible with RoutedEvent.",
node));
}
}
}
}
return node;
}

private sealed class XamlDirectCallAddHandler : IXamlILOptimizedEmitablePropertySetter
{
private readonly IXamlField _eventField;
private readonly IXamlType _declaringType;
private readonly IXamlMethod _addMethod;

public XamlDirectCallAddHandler(IXamlField eventField,
IXamlType declaringType,
IXamlMethod addMethod,
IXamlType routedEventHandler
)
{
Parameters = [routedEventHandler];
_eventField = eventField;
_declaringType = declaringType;
_addMethod = addMethod;
}

public IXamlType TargetType => _declaringType;
public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters();
public IReadOnlyList<IXamlType> Parameters { get; }

public IReadOnlyList<IXamlCustomAttribute> CustomAttributes => [];

public void Emit(IXamlILEmitter emitter)
=> emitter.EmitCall(_addMethod, true);

public void EmitWithArguments(XamlEmitContextWithLocals<IXamlILEmitter, XamlILNodeEmitResult> context,
IXamlILEmitter emitter,
IReadOnlyList<IXamlAstValueNode> arguments)
{

using (var loc = emitter.LocalsPool.GetLocal(_declaringType))
emitter
.Ldloc(loc.Local);

emitter.Ldfld(_eventField);

for (var i = 0; i < arguments.Count; ++i)
context.Emit(arguments[i], emitter, Parameters[i]);

emitter.Ldc_I4(5);
emitter.Ldc_I4(0);

emitter.EmitCall(_addMethod, true);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
class AvaloniaXamlIlWellKnownTypes

sealed class AvaloniaXamlIlWellKnownTypes
{
public IXamlType RuntimeHelpers { get; }
public IXamlType AvaloniaObject { get; }
Expand Down Expand Up @@ -124,6 +125,49 @@ class AvaloniaXamlIlWellKnownTypes
public IXamlType WindowTransparencyLevel { get; }
public IXamlType IReadOnlyListOfT { get; }
public IXamlType ControlTemplate { get; }
public IXamlType EventHandlerT { get; }

sealed internal class InteractivityWellKnownTypes
{
public IXamlType Interactive { get; }
public IXamlType RoutedEvent { get; }
public IXamlType RoutedEventArgs { get; }
public IXamlType RoutedEventHandler { get; }
public IXamlMethod AddHandler { get; }
public IXamlMethod AddHandlerT { get; }

internal InteractivityWellKnownTypes(TransformerConfiguration cfg)
{
var ts = cfg.TypeSystem;
Interactive = ts.FindType("Avalonia.Interactivity.Interactive");
RoutedEvent = ts.FindType("Avalonia.Interactivity.RoutedEvent");
RoutedEventArgs = ts.FindType("Avalonia.Interactivity.RoutedEventArgs");
var eventHanlderT = ts.FindType("System.EventHandler`1");
RoutedEventHandler = eventHanlderT.MakeGenericType(RoutedEventArgs);
AddHandler = Interactive.FindMethod(m => m.IsPublic
&& !m.IsStatic
&& m.Name == "AddHandler"
&& m.Parameters.Count == 4
&& m.Parameters[0].Equals(RoutedEvent)
&& m.Parameters[1].Equals(cfg.WellKnownTypes.Delegate)
&& m.Parameters[2].IsEnum
&& m.Parameters[3].Equals(cfg.WellKnownTypes.Boolean)
);
AddHandlerT = Interactive.FindMethod(m => m.IsPublic
&& !m.IsStatic
&& m.Name == "AddHandler"
&& m.Parameters.Count == 4
&& RoutedEvent.IsAssignableFrom(m.Parameters[0])
&& m.Parameters[0].GenericArguments?.Count == 1 // This is specific this case workaround to check is generic method
&& (cfg.WellKnownTypes.Delegate).IsAssignableFrom(m.Parameters[1])
&& m.Parameters[2].IsEnum
&& m.Parameters[3].Equals(cfg.WellKnownTypes.Boolean) == true
);

}
}

public InteractivityWellKnownTypes Interactivity { get; }

public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
{
Expand Down Expand Up @@ -161,7 +205,6 @@ public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
IBinding, cfg.WellKnownTypes.Object);
UnsetValueType = cfg.TypeSystem.GetType("Avalonia.UnsetValueType");
StyledElement = cfg.TypeSystem.GetType("Avalonia.StyledElement");
StyledElement = cfg.TypeSystem.GetType("Avalonia.StyledElement");
INameScope = cfg.TypeSystem.GetType("Avalonia.Controls.INameScope");
INameScopeRegister = INameScope.GetMethod(
new FindMethodMethodSignature("Register", XamlIlTypes.Void,
Expand Down Expand Up @@ -242,7 +285,7 @@ public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
StyledElementClassesProperty =
StyledElement.Properties.First(x => x.Name == "Classes" && x.PropertyType.Equals(Classes));
ClassesBindMethod = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions")
.FindMethod( "BindClass", IDisposable, false, StyledElement,
.FindMethod("BindClass", IDisposable, false, StyledElement,
cfg.WellKnownTypes.String,
IBinding, cfg.WellKnownTypes.Object);

Expand Down Expand Up @@ -271,6 +314,8 @@ public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
ControlTheme = cfg.TypeSystem.GetType("Avalonia.Styling.ControlTheme");
ControlTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.ControlTemplate");
IReadOnlyListOfT = cfg.TypeSystem.GetType("System.Collections.Generic.IReadOnlyList`1");
EventHandlerT = cfg.TypeSystem.GetType("System.EventHandler`1");
Interactivity = new InteractivityWellKnownTypes(cfg);
}
}

Expand All @@ -291,7 +336,7 @@ public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this XamlEmitContext
ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration));
return rv;
}

public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this AstGroupTransformationContext ctx)
{
if (ctx.TryGetItem<AvaloniaXamlIlWellKnownTypes>(out var rv))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,15 @@
<Link>PlatformFactAttribute.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<PackageReference Update="xunit.runner.console" Version="2.7.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Update="xunit.runner.visualstudio" Version="2.5.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
</Project>
44 changes: 44 additions & 0 deletions tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ public void Attached_Event_Is_Assigned()
Assert.True(target.WasTapped);
}

[Fact]
public void Attached_Event_Is_Assigned_Generic()
{
var xaml = @"<Panel xmlns='https://github.com/avaloniaui'><Grid DoubleTapped='OnTapped'><Button Name='target'/></Grid></Panel>";
var host = new MyPanel();

AvaloniaRuntimeXamlLoader.Load(xaml, rootInstance: host);

var target = host.FindControl<Button>("target");

Assert.NotNull(target);

target.RaiseEvent(new TappedEventArgs(Gestures.DoubleTappedEvent, default));

Assert.True(host.WasTapped);
}

[Fact]
public void Exception_Is_Thrown_If_Event_Not_Found()
{
Expand All @@ -48,6 +65,25 @@ public void Exception_Is_Thrown_If_Event_Not_Found()
XamlTestHelpers.AssertThrowsXamlException(() => AvaloniaRuntimeXamlLoader.Load(xaml, rootInstance: target));
}



[Fact]
public void Attached_Event_Routed_Event_Handler()
{
var xaml = @"<Panel xmlns='https://github.com/avaloniaui' Button.Click='OnClick'><Button Name='target'/></Panel>";
var host = new MyPanel();

AvaloniaRuntimeXamlLoader.Load(xaml, rootInstance: host);

var target = host.FindControl<Button>("target");
target.RaiseEvent(new RoutedEventArgs
{
RoutedEvent = Button.ClickEvent,
});

Assert.True(host.WasClicked);
}

public class MyButton : Button
{
public bool WasClicked { get; private set; }
Expand All @@ -56,5 +92,13 @@ public class MyButton : Button
public void OnClick(object sender, RoutedEventArgs e) => WasClicked = true;
public void OnTapped(object sender, RoutedEventArgs e) => WasTapped = true;
}

public class MyPanel : Panel
{
public bool WasClicked { get; private set; }
public bool WasTapped { get; private set; }
public void OnClick(object sender, RoutedEventArgs e) => WasClicked = true;
public void OnTapped(object sender, RoutedEventArgs e) => WasTapped = true;
}
}
}
Loading