Невозможно реализовать INotifyPropertyChanged с помощью System.Reflection.Emit

Я пытаюсь реализовать динамический класс, который можно привязать к DataGrid Avalonia. Этот класс должен реализовать INotifyPropertyChanged, чтобы использовать версию DataGrid. После поиска кажется, что лучший вариант — использовать пакет System.Reflection.Emit.

Я могу генерировать динамические свойства в динамическом классе и экземплярах этого класса. Кроме того, я могу создать ObservableCollection и привязать ее к DataGrid, наполнив ее экземплярами динамического класса, и данные будут правильно отображаться в DataGrid.

Моя проблема связана с реализацией INotifyProperty. На самом деле я получаю сообщение об ошибке, когда вызываю метод OnPropertyChanged из метода set динамического свойства.

Это моя фабрика динамических классов:

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

Это класс модели представления, в котором создаются экземпляры:

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)
            {

            }
        }
    }
}

На самом деле я застрял на этом этапе:

            //....

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

            //....

Программа компилируется без ошибок. Но во время выполнения выдает ошибку: System.InvalidProgramException: 'Common Language Runtime detected an invalid program.', при первом назначении: dynamicClass1.ID = 1;. Если я закомментирую эти строки, программа будет работать как положено, очевидно, без уведомлений об изменении свойств.

В чем проблема? Я неправильно определил реализацию INotifyPropertyChanged? Я определил неправильный вызов события уведомления в методе set?

@mike-nakis Я не согласен с тем, что вы скрываете опубликованное мной решение. Я думаю, что публикация окончательного рабочего кода может помочь другим найти решение. Если вы удалите этот код, люди с меньшим пониманием этого вопроса, возможно, не смогут найти решение самостоятельно.

Guillermo Espert 28.08.2024 11:21
Meta.stackoverflow.com/questions/267434/…
Mike Nakis 28.08.2024 11:55

Если вы хотите предложить ответ, опубликуйте его как ответ. Для того и нужны ответы – чтобы отвечать на вопросы. Вопросы сами по себе не являются ответами.

VLAZ 28.08.2024 11:56
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
3
53
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий
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);

Если evtField равен null, то вы переходите к retLabel, но когда вы вернетесь, у вас все еще будет this в стеке. Вам нужно это вытолкнуть.

IL, который здесь генерирует компилятор , немного отличается:

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

Посмотрите, что у него есть два пути выхода из метода, и тот, который используется, если событие имеет значение null, имеет дополнительный pop.

( Debug IL немного отличается: он сохраняет одиночный ret в конце метода, но добавляет дополнительную ветку для вставки этого pop).

Другие вопросы по теме