Search code examples
c#reflectioncil

Add OnpropertyChanged to property setter at runtime


I'm creating classes at runtime using reflection, based on ClassName and a List of Properties, they're subclasses of the parent class "DataObject", which implements INotifyPropertyChanged and OnPropertyChanged, but when I try to set the properties through the following method I get a "Field token out of range" exception:

        private void dataGrid_AddingNewItem(object sender, AddingNewItemEventArgs e)
    {
        object obj = Activator.CreateInstance(currentType);

        PropertyInfo[] properties = obj.GetType().GetProperties();
        try
        {
            foreach (PropertyInfo prop in properties)
            {
                if (prop.PropertyType == typeof(string) && prop.CanWrite)
                { prop.SetValue(obj, "-", null); } 
                //else
                //{ prop.SetValue(obj, 0, null); }
            }
        }
        catch (Exception ex)
        {
            if (ex.InnerException != null)
            {
                throw ex.InnerException;
            }
        }

        e.NewItem = obj;
    }

this is how I would want each property to work (LastChange is a static string from the parent class):

public string Provaa { get { return provaa; } 
set { LastChange = ToString(); provaa = value; OnPropertyChanged("Provaa"); } }

and this is how that is translated to Msil:

.method public hidebysig specialname instance void 
    set_Provaa(string 'value') cil managed
{
// Code size       32 (0x20)
.maxstack  8
IL_0000:  nop
IL_0001:  ldarg.0
IL_0002:  callvirt   instance string [mscorlib]System.Object::ToString()
IL_0007:  stsfld     string EYBDataManager.DataObject::LastChange
IL_000c:  ldarg.0
IL_000d:  ldarg.1
IL_000e:  stfld      string EYBDataManager.Prova::provaa
IL_0013:  ldarg.0
IL_0014:  ldstr      "Provaa"
IL_0019:  call       instance void EYBDataManager.DataObject::OnPropertyChanged(string)
IL_001e:  nop
IL_001f:  ret
} // end of method Prova::set_Provaa

And lastly this is how I'm attempting to recreate that using reflection:

MethodBuilder currSetPropMthdBldr = typeBuilder.DefineMethod("set_value", GetSetAttr, null, new Type[] { prop.ActualType });
ILGenerator currSetIL = currSetPropMthdBldr.GetILGenerator();
currSetIL.Emit(OpCodes.Ldarg_0);
currSetIL.Emit(OpCodes.Callvirt, typeof(Object).GetMethod("ToString"));
currSetIL.Emit(OpCodes.Stsfld, typeof(DataObject).GetField("LastChange", BindingFlags.Static | BindingFlags.Public);
currSetIL.Emit(OpCodes.Ldarg_0);
currSetIL.Emit(OpCodes.Ldarg_1);
currSetIL.Emit(OpCodes.Stfld, field);
currSetIL.Emit(OpCodes.Ldarg_0);
currSetIL.Emit(OpCodes.Ldstr, propertyName);
currSetIL.Emit(OpCodes.Callvirt, typeof(DataObject).GetMethod("OnPropertyChanged", new Type[1] { typeof(string) }));
currSetIL.Emit(OpCodes.Ret);

which is part of the "CreateClass" method:

public static void CreateClass(string className, List<PropertyTemplate> properties)
{
AssemblyName assemblyName = new AssemblyName();
assemblyName.Name = "tmpAssembly";
AssemblyBuilder assemblyBuilder = System.Threading.Thread.GetDomain().DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder module = assemblyBuilder.DefineDynamicModule("tmpModule");
TypeBuilder typeBuilder = module.DefineType(className, TypeAttributes.Public | TypeAttributes.Class, typeof(DataObject));

foreach (PropertyTemplate prop in properties)
{
    string propertyName = prop.Name;
    FieldBuilder field = typeBuilder.DefineField("p_" + propertyName, prop.ActualType, FieldAttributes.Private);
    PropertyBuilder property = typeBuilder.DefineProperty(propertyName, PropertyAttributes.None, prop.ActualType, new Type[] { prop.ActualType });
    MethodAttributes GetSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;

    MethodBuilder currGetPropMthdBldr = typeBuilder.DefineMethod("get_value", GetSetAttr, prop.ActualType, Type.EmptyTypes);
    ILGenerator currGetIL = currGetPropMthdBldr.GetILGenerator();
    currGetIL.Emit(OpCodes.Ldarg_0);
    currGetIL.Emit(OpCodes.Ldfld, field);
    currGetIL.Emit(OpCodes.Ret);


    ILGenerator currSetIL = currSetPropMthdBldr.GetILGenerator();
    currSetIL.Emit(OpCodes.Ldarg_0);
    currSetIL.Emit(OpCodes.Callvirt, typeof(Object).GetMethod("ToString"));
    currSetIL.Emit(OpCodes.Stsfld, DataObject.LastChange);
    currSetIL.Emit(OpCodes.Ldarg_0);
    currSetIL.Emit(OpCodes.Ldarg_1);
    currSetIL.Emit(OpCodes.Stfld, field);
    currSetIL.Emit(OpCodes.Ldarg_0);
    currSetIL.Emit(OpCodes.Ldstr, propertyName);
    currSetIL.Emit(OpCodes.Callvirt, typeof(DataObject).GetMethod("OnPropertyChanged", new Type[1] { typeof(string) }));
    currSetIL.Emit(OpCodes.Ret);

    property.SetGetMethod(currGetPropMthdBldr);
    property.SetSetMethod(currSetPropMthdBldr);
}

Type genType = typeBuilder.CreateType();
if (Templates.ContainsKey(className))
    Templates[className] = genType;
else
    Templates.Add(className, genType);
}

I suspect I'd need to specify the assembly name and the module name of the class when setting the value, but don't know how, though the Activator creates the class instance with all the correct properties, so probably I made some mistake while building the SetMethod, could someone help me?

Edit Parent Class:

    public class DataObject : INotifyPropertyChanged
{
    public static string LastChange = "";

    public void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public override string ToString()
    {
        string tostring = "|";
        PropertyInfo[] properties = this.GetType().GetProperties();
        foreach (PropertyInfo prop in properties)
        {
            tostring += " " + prop.GetValue(this, null) + " |";
        }
        return tostring;
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

Solution

  • I see two errors in your setter IL code

    This:

    currSetIL.EmitCall(OpCodes.Call, typeof(Object).GetMethod("ToString"), new Type[0]);
    

    Need to be:

    currSetIL.Emit(OpCodes.Callvirt, typeof(Object).GetMethod("ToString"), new Type[0]);
    

    And this:

    currSetIL.Emit(OpCodes.Ldstr, propertyName);
    currSetIL.Emit(OpCodes.Call, typeof(DataObject).GetMethod("OnPropertyChanged", new Type[1] { typeof(string) }));
    

    Need to be:

    currSetIL.Emit(OpCodes.ldarg_0);
    currSetIL.Emit(OpCodes.Ldstr, propertyName);
    currSetIL.Emit(OpCodes.Callvirt, typeof(DataObject).GetMethod("OnPropertyChanged", new Type[1] { typeof(string) }));
    

    I saw in the first case that you put the callvirt in comment and you replace it with call why that? Its a call to virtual method.

    Also in the second case its virtual call and you forgot to load this first to call the instance method OnPropertyChanged.

    This is what I see for now, let me know if it fixed, and if not I try to reproduce it in myself.

    Update:

    Replace this:

    currSetIL.Emit(OpCodes.Stsfld, DataObject.LastChange)
    

    With this:

    currSetIL.Emit(OpCodes.Stsfld, typeof(DataObject).GetField("LastChange", BindingFlags.Static | BindingFlags.Public)