Search code examples
c#.net-corereflection.emitavalonia

Unable to implement INotifyPropertyChanged with System.Reflection.Emit


I'm trying to implement a dynamic class that can be bound to an Avalonia DataGrid. This class should implement INotifyPropertyChanged in order to use DataGrid edition. After search, it seems the best option is to use System.Reflection.Emit package.

I'm able to generate the dynamic properties in the dynamic class and instances of this class. Also, I can create an ObservableCollection and bound it to the DataGrid, filling it with instances of the dyamic class, and the data is showning properly in the DataGrid.

My problem is with the implementation of the INotifyProperty. Actually I'm getting an error when I call the OnPropertyChanged method from the set method of the dynamic property.

This is my dynamic class factory:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;

namespace MyApp.App.Reflection
{
    public class DynamicClassFactory
    {
        AssemblyName _assemblyName;

        public DynamicClassFactory(string className)
        {
            _assemblyName = new AssemblyName(className);
        }

        public dynamic? CreateObject(string[] propertyNames, Type[] types)
        {
            if (propertyNames.Length != types.Length)
            {
                throw new ArgumentException("The number of property names should match their corresponding types number");
            }

            TypeBuilder dynamicClass = CreateClass();
            CreateConstructor(dynamicClass);
            var onPropertyChangedMethod = ImplementINotifyPropertyChanged(dynamicClass);

            for (int i = 0; i < propertyNames.Length; i++)
            {
                CreateProperties(dynamicClass, propertyNames[i], types[i], onPropertyChangedMethod);
            }

            Type type = dynamicClass.CreateType();
            return Activator.CreateInstance(type);
        }

        private TypeBuilder CreateClass()
        {
            AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(_assemblyName, AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule");
            TypeBuilder typeBuilder = moduleBuilder.DefineType(_assemblyName.FullName, 
                                                                TypeAttributes.Public
                                                                | TypeAttributes.Class
                                                                | TypeAttributes.AutoClass
                                                                | TypeAttributes.AnsiClass
                                                                | TypeAttributes.BeforeFieldInit
                                                                | TypeAttributes.AutoLayout, null);

            typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanged));

            return typeBuilder;
        }

        private void CreateConstructor(TypeBuilder typeBuilder)
        {
            typeBuilder.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);
        }

        private MethodBuilder ImplementINotifyPropertyChanged(TypeBuilder typeBuilder)
        {
            var evtField = typeBuilder.DefineField("PropertyChanged", typeof(PropertyChangedEventHandler), FieldAttributes.Private);
            var evtBuilder = typeBuilder.DefineEvent("PropertyChanged", EventAttributes.None, typeof(PropertyChangedEventHandler));
            var addMethod = typeBuilder.DefineMethod("add_PropertyChanged",
                                                    MethodAttributes.Public | MethodAttributes.Virtual,
                                                    typeof(void),
                                                    new Type[] { typeof(PropertyChangedEventHandler) });

            var addIl = addMethod.GetILGenerator();
            addIl.Emit(OpCodes.Ldarg_0);
            addIl.Emit(OpCodes.Ldarg_0);
            addIl.Emit(OpCodes.Ldfld, evtField);
            addIl.Emit(OpCodes.Ldarg_1);
            addIl.Emit(OpCodes.Call, 
                        typeof(Delegate).GetMethod("Combine", new Type[] { typeof(Delegate), typeof(Delegate) }));
            addIl.Emit(OpCodes.Castclass, typeof(PropertyChangedEventHandler));
            addIl.Emit(OpCodes.Stfld, evtField);
            addIl.Emit(OpCodes.Ret);

            var removeMethod = typeBuilder.DefineMethod("remove_PropertyChanged",
                                                        MethodAttributes.Public | MethodAttributes.Virtual,
                                                        typeof(void),
                                                        new Type[] { typeof(PropertyChangedEventHandler) });

            var removeIl = removeMethod.GetILGenerator();
            removeIl.Emit(OpCodes.Ldarg_0);
            removeIl.Emit(OpCodes.Ldarg_0);
            removeIl.Emit(OpCodes.Ldfld, evtField);
            removeIl.Emit(OpCodes.Ldarg_1);
            removeIl.Emit(OpCodes.Call,
                        typeof(Delegate).GetMethod("Remove", new Type[] { typeof(Delegate), typeof(Delegate) }));
            removeIl.Emit(OpCodes.Castclass, typeof(PropertyChangedEventHandler));
            removeIl.Emit(OpCodes.Stfld, evtField);
            removeIl.Emit(OpCodes.Ret);

            evtBuilder.SetAddOnMethod(addMethod);
            evtBuilder.SetRemoveOnMethod(removeMethod);

            var onPropertyChangedMethod = typeBuilder.DefineMethod("OnPropertyChanged",
                                                                MethodAttributes.Family | MethodAttributes.Virtual,
                                                                typeof(void),
                                                                new Type[] { typeof(string) });

            var onPropertyChangedIl = onPropertyChangedMethod.GetILGenerator();
            var retLabel = onPropertyChangedIl.DefineLabel();

            onPropertyChangedIl.Emit(OpCodes.Ldarg_0);
            onPropertyChangedIl.Emit(OpCodes.Ldfld, evtField);
            onPropertyChangedIl.Emit(OpCodes.Dup);
            onPropertyChangedIl.Emit(OpCodes.Brfalse_S, retLabel);
            onPropertyChangedIl.Emit(OpCodes.Ldarg_0);
            onPropertyChangedIl.Emit(OpCodes.Ldarg_1);
            onPropertyChangedIl.Emit(OpCodes.Newobj, typeof(PropertyChangedEventArgs).GetConstructor(new Type[] { typeof(string) }));
            onPropertyChangedIl.Emit(OpCodes.Callvirt, typeof(PropertyChangedEventHandler).GetMethod("Invoke", new Type[] { typeof(object), typeof(PropertyChangedEventArgs) }));
            onPropertyChangedIl.MarkLabel(retLabel);
            onPropertyChangedIl.Emit(OpCodes.Ret);

            
            return onPropertyChangedMethod;
        }

        private void CreateProperties(TypeBuilder typeBuilder, string propertyName, Type type, MethodBuilder onPropertyChangedMethod)
        {
            FieldBuilder fieldBuilder = typeBuilder.DefineField("_" + propertyName, type, FieldAttributes.Private);

            PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, type, null);
            MethodBuilder getPropMthdBldr = typeBuilder.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, type, Type.EmptyTypes);
            ILGenerator getIl = getPropMthdBldr.GetILGenerator();

            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, fieldBuilder);
            getIl.Emit(OpCodes.Ret);

            MethodBuilder setPropMthdBldr = typeBuilder.DefineMethod("set_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null, new Type[] { type });

            ILGenerator setIl = setPropMthdBldr.GetILGenerator();
            setIl.Emit(OpCodes.Ldarg_0);
            setIl.Emit(OpCodes.Ldarg_1);
            setIl.Emit(OpCodes.Stfld, fieldBuilder);
            setIl.Emit(OpCodes.Ldarg_0);
            setIl.Emit(OpCodes.Ldstr, propertyName);
            setIl.Emit(OpCodes.Call, onPropertyChangedMethod);
            setIl.Emit(OpCodes.Ret);

            propertyBuilder.SetGetMethod(getPropMthdBldr);
            propertyBuilder.SetSetMethod(setPropMthdBldr);
        }
    }
}

This is the view model class where instances are created:

using MyApp.App.Reflection;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyApp.App.Components
{
    public class DataViewerControlViewModel
    {
        public ObservableCollection<dynamic> Data { get; } = new ObservableCollection<dynamic>();

        public DataViewerControlViewModel()
        {
            DynamicClassFactory dynamicClassFactory = new DynamicClassFactory("DynamicRecordsClass");
            var dynamicClass1 = dynamicClassFactory.CreateObject(new string[] { "ID", "Name", "Age", "IsAdmin" }, new Type[] { typeof(int), typeof(string), typeof(int), typeof(bool) });

            dynamicClass1.ID = 1;
            dynamicClass1.Name = "John Doe";
            dynamicClass1.Age = 30;
            dynamicClass1.IsAdmin = true;

            Data.Add(dynamicClass1);

            var dynamicClass2 = dynamicClassFactory.CreateObject(new string[] { "ID", "Name", "Age", "IsAdmin" }, new Type[] { typeof(int), typeof(string), typeof(int), typeof(bool) });

            dynamicClass2.ID = 2;
            dynamicClass2.Name = "Jane Doe";
            dynamicClass2.Age = 25;
            dynamicClass2.IsAdmin = false;

            Data.Add(dynamicClass2);

            Data.CollectionChanged += Data_CollectionChanged;
        }

        private void Data_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Replace)
            {

            }
        }
    }
}

Actually I'm stuck at this point:

            //....

            setIl.Emit(OpCodes.Ldarg_0);
            setIl.Emit(OpCodes.Ldstr, propertyName);
            setIl.Emit(OpCodes.Call, onPropertyChangedMethod);

            //....

Program compiles without any error. But in runtime, it produces an error: System.InvalidProgramException: 'Common Language Runtime detected an invalid program.', over the first assignation: dynamicClass1.ID = 1;. If I comment out these lines program works as expected, obviously without property change notifications.

Where is the problem? I defined wrong the INotifyPropertyChanged implementation? I defined a wrong call to the notify event in the set method?


Solution

  • onPropertyChangedIl.Emit(OpCodes.Ldarg_0);
    onPropertyChangedIl.Emit(OpCodes.Ldfld, evtField);
    onPropertyChangedIl.Emit(OpCodes.Dup);
    onPropertyChangedIl.Emit(OpCodes.Brfalse_S, retLabel);
    onPropertyChangedIl.Emit(OpCodes.Ldarg_0);
    onPropertyChangedIl.Emit(OpCodes.Ldarg_1);
    onPropertyChangedIl.Emit(OpCodes.Newobj, typeof(PropertyChangedEventArgs).GetConstructor(new Type[] { typeof(string) }));
    onPropertyChangedIl.Emit(OpCodes.Callvirt, typeof(PropertyChangedEventHandler).GetMethod("Invoke", new Type[] { typeof(object), typeof(PropertyChangedEventArgs) }));
    onPropertyChangedIl.MarkLabel(retLabel);
    onPropertyChangedIl.Emit(OpCodes.Ret);
    

    If the evtField is null, then you branch to retLabel, but you've still got the this on the stack when you return. You need to pop that.

    The IL that the compiler generates here is slightly different:

    IL_0000: ldarg.0
    IL_0001: ldfld class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler C::PropertyChanged
    IL_0006: dup
    IL_0007: brtrue.s IL_000b
    
    IL_0009: pop
    IL_000a: ret
    
    IL_000b: ldarg.0
    IL_000c: ldarg.1
    IL_000d: newobj instance void [System.ObjectModel]System.ComponentModel.PropertyChangedEventArgs::.ctor(string)
    IL_0012: callvirt instance void [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler::Invoke(object, class [System.ObjectModel]System.ComponentModel.PropertyChangedEventArgs)
    IL_0017: ret
    

    See how it has two exit paths from the method, and the one taken if the event is null has an extra pop.

    (The Debug IL is slightly different: this keeps the single ret at the end of the method, but adds an extra branch to insert that pop).