Skip to content

Commit

Permalink
feat: Every RoutedEvent should be usable as Attached Event (#15274)
Browse files Browse the repository at this point in the history
* test: Automatic RoutedEvent Handler Generation

* feat: Every RoutedEvent should be usable as Attached Event

* fix: Namespace

* fix: Address review

* feat: Handle Preview event

* test: Handle Preview event

* fix: Address Review

reverted Preview feture

* fix: Throw On Fatal

* fix: Error Code
  • Loading branch information
workgroupengineering authored Jun 5, 2024
1 parent 2dfd9be commit 6b48721
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 4 deletions.
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)
{
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;
}
}
}

0 comments on commit 6b48721

Please sign in to comment.