Browse Source

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

* 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
pull/15929/head
workgroupengineering 2 years ago
committed by GitHub
parent
commit
6b48721391
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  2. 118
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformRoutedEvent.cs
  3. 53
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  4. 10
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  5. 44
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs

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

@ -86,6 +86,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
InsertBeforeMany(new [] { typeof(DeferredContentTransformer), typeof(AvaloniaXamlIlCompiledBindingsMetadataRemover) }, InsertBeforeMany(new [] { typeof(DeferredContentTransformer), typeof(AvaloniaXamlIlCompiledBindingsMetadataRemover) },
new AvaloniaXamlIlDeferredResourceTransformer()); new AvaloniaXamlIlDeferredResourceTransformer());
InsertBefore<AvaloniaXamlIlTransformInstanceAttachedProperties>(new AvaloniaXamlIlTransformRoutedEvent());
Transformers.Add(new AvaloniaXamlIlControlTemplatePriorityTransformer()); Transformers.Add(new AvaloniaXamlIlControlTemplatePriorityTransformer());
Transformers.Add(new AvaloniaXamlIlMetadataRemover()); Transformers.Add(new AvaloniaXamlIlMetadataRemover());
Transformers.Add(new AvaloniaXamlIlEnsureResourceDictionaryCapacityTransformer()); Transformers.Add(new AvaloniaXamlIlEnsureResourceDictionaryCapacityTransformer());

118
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformRoutedEvent.cs

@ -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);
}
}
}

53
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -9,7 +9,8 @@ using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{ {
class AvaloniaXamlIlWellKnownTypes
sealed class AvaloniaXamlIlWellKnownTypes
{ {
public IXamlType RuntimeHelpers { get; } public IXamlType RuntimeHelpers { get; }
public IXamlType AvaloniaObject { get; } public IXamlType AvaloniaObject { get; }
@ -124,6 +125,49 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType WindowTransparencyLevel { get; } public IXamlType WindowTransparencyLevel { get; }
public IXamlType IReadOnlyListOfT { get; } public IXamlType IReadOnlyListOfT { get; }
public IXamlType ControlTemplate { 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) public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
{ {
@ -161,7 +205,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
IBinding, cfg.WellKnownTypes.Object); IBinding, cfg.WellKnownTypes.Object);
UnsetValueType = cfg.TypeSystem.GetType("Avalonia.UnsetValueType"); UnsetValueType = cfg.TypeSystem.GetType("Avalonia.UnsetValueType");
StyledElement = cfg.TypeSystem.GetType("Avalonia.StyledElement"); StyledElement = cfg.TypeSystem.GetType("Avalonia.StyledElement");
StyledElement = cfg.TypeSystem.GetType("Avalonia.StyledElement");
INameScope = cfg.TypeSystem.GetType("Avalonia.Controls.INameScope"); INameScope = cfg.TypeSystem.GetType("Avalonia.Controls.INameScope");
INameScopeRegister = INameScope.GetMethod( INameScopeRegister = INameScope.GetMethod(
new FindMethodMethodSignature("Register", XamlIlTypes.Void, new FindMethodMethodSignature("Register", XamlIlTypes.Void,
@ -242,7 +285,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
StyledElementClassesProperty = StyledElementClassesProperty =
StyledElement.Properties.First(x => x.Name == "Classes" && x.PropertyType.Equals(Classes)); StyledElement.Properties.First(x => x.Name == "Classes" && x.PropertyType.Equals(Classes));
ClassesBindMethod = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions") ClassesBindMethod = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions")
.FindMethod( "BindClass", IDisposable, false, StyledElement, .FindMethod("BindClass", IDisposable, false, StyledElement,
cfg.WellKnownTypes.String, cfg.WellKnownTypes.String,
IBinding, cfg.WellKnownTypes.Object); IBinding, cfg.WellKnownTypes.Object);
@ -271,6 +314,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
ControlTheme = cfg.TypeSystem.GetType("Avalonia.Styling.ControlTheme"); ControlTheme = cfg.TypeSystem.GetType("Avalonia.Styling.ControlTheme");
ControlTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.ControlTemplate"); ControlTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.ControlTemplate");
IReadOnlyListOfT = cfg.TypeSystem.GetType("System.Collections.Generic.IReadOnlyList`1"); IReadOnlyListOfT = cfg.TypeSystem.GetType("System.Collections.Generic.IReadOnlyList`1");
EventHandlerT = cfg.TypeSystem.GetType("System.EventHandler`1");
Interactivity = new InteractivityWellKnownTypes(cfg);
} }
} }
@ -291,7 +336,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration)); ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration));
return rv; return rv;
} }
public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this AstGroupTransformationContext ctx) public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this AstGroupTransformationContext ctx)
{ {
if (ctx.TryGetItem<AvaloniaXamlIlWellKnownTypes>(out var rv)) if (ctx.TryGetItem<AvaloniaXamlIlWellKnownTypes>(out var rv))

10
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@ -36,5 +36,15 @@
<Link>PlatformFactAttribute.cs</Link> <Link>PlatformFactAttribute.cs</Link>
</Compile> </Compile>
</ItemGroup> </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" /> <Import Project="..\..\build\BuildTargets.targets" />
</Project> </Project>

44
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs

@ -39,6 +39,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.True(target.WasTapped); 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] [Fact]
public void Exception_Is_Thrown_If_Event_Not_Found() public void Exception_Is_Thrown_If_Event_Not_Found()
{ {
@ -48,6 +65,25 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
XamlTestHelpers.AssertThrowsXamlException(() => AvaloniaRuntimeXamlLoader.Load(xaml, rootInstance: target)); 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 class MyButton : Button
{ {
public bool WasClicked { get; private set; } public bool WasClicked { get; private set; }
@ -56,5 +92,13 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
public void OnClick(object sender, RoutedEventArgs e) => WasClicked = true; public void OnClick(object sender, RoutedEventArgs e) => WasClicked = true;
public void OnTapped(object sender, RoutedEventArgs e) => WasTapped = 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…
Cancel
Save