Using Compiled Bindings to Properties of a Record Type Doesn't Fail Until Runtime
See original GitHub issueDescribe the bug
If I use a compiled binding to a property of a record
type, the compilation succeeds, only to get an InvalidProgramException
at runtime when Avalonia attempts to set the property value.
To Reproduce
public record TestRecord(string Prop);
public class TestViewModel : ViewModelBase
{
private TestRecord _testObj = new("");
public TestRecord TestObj
{
get => _testObj;
set = this.RaiseAndSetIfChanged(ref _testObj, value, nameof(TestObj));
}
}
...
<TextBox Text="{CompiledBinding TestObj.Prop}" />
...
Expected behavior This would refuse to compile.
Actual behavior
It compiles, and Avalonia generates this IL at CompiledAvaloniaXaml.XamlIlHelpers.{path}.Name!Setter(object, object)
:
.method compilercontrolled static void
'{path}.Name!Setter'(
[in] object obj0,
[in] object obj1
) cil managed
{
.maxstack 2
IL_0000: ldarg.0 // obj0
IL_0001: castclass {object type}
IL_0006: ldarg obj1
IL_000a: castclass [System.Runtime]System.String
IL_000f: callvirt instance void modreq ([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) {path}::set_Name(string)
IL_0014: pop
IL_0015: ret
} // end of method XamlIlHelpers::'{path}.Name!Setter'
After the first two opcodes, there’s an object on the stack. The next two push a string onto the stack. Then the callvirt
pops those two, leaving the stack empty. Then Avalonia threw in a random pop
that would make a stack underflow before returning. Because of that random pop
opcode, the runtime refuses to run that function and dotPeek refuses to decompile it.
Screenshots
Desktop
- OS: Windows 11 and Raspbian Buster (not that it matters)
- Version 0.10.15
Additional context
Roslyn apparently generates public
setters for record
types, which Avalonia was able to bind to. Hence, why the compilation succeeded.
Issue Analytics
- State:
- Created a year ago
- Comments:11 (5 by maintainers)
Top GitHub Comments
I’ve opened a PR to at least fix the
InvalidProgramException
, so compiled bindings don’t crash and work the same way as reflection bindings do currently.Blocking
init
two-way/one-way-to-source bindings is another task, and if done, should be done both for compiled and reflection bindings. It depends on what the project maintainers think about it.The problem appears for
init
properties (which records use). Minimal repro:It currently works with System.Reflection.Emit (loading xaml files at runtime) but not Cecil (build time). Making it work for Cecil seems easy enough:
ReturnType.Name
ismodreq(IsExternalInit) void
instead ofvoid
in this case, taking the underlying type if there’s a modreq/modopt should be enough. I can open a PR for that.But if this runs,
init
setters then can be set several times, and they probably shouldn’t. IMO, an ideal solution would be to disallow two-way bindings (both compiled and reflection ones) toinit
properties, and only allow them in XAML setters when an object is being created, to mimic C# behavior. That can be considered a new feature though.