Я пытаюсь реализовать динамический класс, который можно привязать к 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?
Если вы хотите предложить ответ, опубликуйте его как ответ. Для того и нужны ответы – чтобы отвечать на вопросы. Вопросы сами по себе не являются ответами.
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
).
@mike-nakis Я не согласен с тем, что вы скрываете опубликованное мной решение. Я думаю, что публикация окончательного рабочего кода может помочь другим найти решение. Если вы удалите этот код, люди с меньшим пониманием этого вопроса, возможно, не смогут найти решение самостоятельно.