diff --git a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs index d92a26c91b3e..66040cf86399 100644 --- a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs @@ -1280,9 +1280,9 @@ static IEnumerable ConnectEvent(VariableDefinition parent, string l // IL_0008: ldarg.0 // // IL_0009: ldftn instance void class Microsoft.Maui.Controls.Xaml.XamlcTests.MyPage::OnButtonClicked(object, class [mscorlib]System.EventArgs) - //OR, if the handler is virtual - // IL_000x: ldarg.0 - // IL_0009: ldvirtftn instance void class Microsoft.Maui.Controls.Xaml.XamlcTests.MyPage::OnButtonClicked(object, class [mscorlib]System.EventArgs) + //OR, if the handler is virtual (non-static) + // IL_000x: dup ; copy target already on stack + // IL_000y: ldvirtftn instance void class Microsoft.Maui.Controls.Xaml.XamlcTests.MyPage::OnButtonClicked(object, class [mscorlib]System.EventArgs) // // IL_000f: newobj instance void class [mscorlib]System.EventHandler::'.ctor'(object, native int) // IL_0014: callvirt instance void class [Microsoft.Maui.Controls]Microsoft.Maui.Controls.Button::add_Clicked(class [mscorlib]System.EventHandler) @@ -1340,9 +1340,17 @@ static IEnumerable ConnectEvent(VariableDefinition parent, string l throw new InvalidProgramException(); } - if (methodDef.IsVirtual) + if (methodDef.IsVirtual && !methodDef.IsStatic) { - yield return Create(Ldarg_0); + // ldvirtftn needs the object whose vtable drives virtual dispatch. + // In a DataTemplate context, Ldarg_0 is the anonymous nested class, not the root + // XAML element — using it causes iOS/Mac Full AOT to crash on virtual handlers. + // The delegate target (already on the stack) IS the correct vtable object, so + // dup it: stack goes [parent, target] → [parent, target, target], ldvirtftn pops one, leaving + // [parent, target, ftn] for the delegate ctor. + // Static methods (including C# 11+ interface static-abstract members) are never + // virtual-dispatched; fall through to ldftn below. + yield return Create(Dup); yield return Create(Ldvirtftn, handlerRef); } else diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui18055.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui18055.xaml new file mode 100644 index 000000000000..bac3eb656e61 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui18055.xaml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui18055.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui18055.xaml.cs new file mode 100644 index 000000000000..6dbc92bc0e71 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui18055.xaml.cs @@ -0,0 +1,48 @@ +using System; +using Xunit; + +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +// Regression test for https://github.com/dotnet/maui/issues/18055 +// XamlC must use Dup (not Ldarg_0) before ldvirtftn when wiring virtual event handlers +// inside a DataTemplate, so the correct vtable object is used for virtual dispatch. +public partial class Maui18055 : ContentPage +{ + public Maui18055() => InitializeComponent(); + + public int BaseForVirtualClicked; + protected virtual void HandleVirtualClicked(object sender, EventArgs e) => BaseForVirtualClicked++; + + [Collection("Issue")] + public class Tests + { + [Theory] + [XamlInflatorData] + // Verifies that a virtual handler wired inside a DataTemplate dispatches to the override, + // not the base class. Without the fix, XamlC emitted Ldarg_0 (the anonymous DataTemplate + // class) as the ldvirtftn vtable source — causing wrong dispatch on JIT and a hard crash + // on iOS/macOS Full AOT. + internal void VirtualHandlerInDataTemplateCallsOverride(XamlInflator inflator) + { + var page = new SubMaui18055(inflator); + Assert.Equal(0, page.BaseForVirtualClicked); + Assert.Equal(0, page.OverrideClicked); + + var template = (Microsoft.Maui.Controls.DataTemplate)page.Resources["virtualHandlerTemplate"]; + var element = (ElementWithEvent)template.CreateContent(); + element.SendClicked(); + + // Override must be called; base must NOT be called. + Assert.Equal(1, page.OverrideClicked); + Assert.Equal(0, page.BaseForVirtualClicked); + } + } +} + +class SubMaui18055 : Maui18055 +{ + public SubMaui18055(XamlInflator inflator) : base(inflator) { } + + public int OverrideClicked; + protected override void HandleVirtualClicked(object sender, EventArgs e) => OverrideClicked++; +}