I'm implementing an emitted propertychanged handler for my POCO object containing virtual auto-properties, and I've got code that works to the point where propertychanged is being raised whenever I change the underlying property. The reason for doing this is that I am sharing a POCO object with the server (for better or for worse), where I will be sending modified objects to the server. I cannot decorate the POCO object with attributes (since the server would also have these decorators, as we share the common class) and I cannot use third-party tools such as Fody or PostSharp due to policies. I need to track whether the object has been modified, and I'm stuck on this.
Here is the Emit that wraps my virtual auto-properties with change notification:
MethodBuilder setMethodBuilder = typeBuilder.DefineMethod(setMethod.Name, setMethod.Attributes, setMethod.ReturnType, types.ToArray());
typeBuilder.DefineMethodOverride(setMethodBuilder, setMethod);
ILGenerator wrapper = setMethodBuilder.GetILGenerator();
...Emit if property <> value IsModified=true here...
wrapper.Emit(OpCodes.Ldarg_0);
wrapper.Emit(OpCodes.Ldarg_1);
wrapper.EmitCall(OpCodes.Call, setMethod, null);
What I need to do is get the set method of the existing "IsModified" boolean property and set it if the property value <> value.
Here's an example of what I'd like to emit (this is currently defined as a POCO with virtual auto-properties):
public class AnEntity
{
string _myData;
public string MyData
{
get
{
return _myData;
}
set
{
if(_myData <> value)
{
IsModified = true;
_myData = value;
OnPropertyChanged("MyData");
}
}
}
bool _isModified;
public bool IsModified { get; set; }
{
get
{
return _isModified;
}
set
{
_isModified = value;
OnPropertyChanged("IsModified");
}
}
}
I've been stuck on this for a while...I have managed to create a new property called "NewIsModified" in the new proxy class that was created, however, I'd very much like to reuse the existing IsModified property in my original POCO.
I hope I've explained my question properly and is easy to understand. Any help would be greatly appreciated, and I hope it will help someone else, too.
Kind regards.
Here is a working code for do it in Mono.Cecil
C# code before:
public class AnEntityVirtual
{
public virtual string MyData { get; set; }
public virtual bool IsModified { get; set; }
}
IL code of the set_MyData
before:
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: stfld string ClassLibrary1.AnEntityVirtual::'<MyData>k__BackingField'
IL_0007: ret
The rewriting:
// Read the module and get the relevant type
var assemblyPath = $"{Environment.CurrentDirectory}\\ClassLibrary1.dll";
var module = ModuleDefinition.ReadModule(assemblyPath);
var type = module.Types.Single(t => t.Name == "AnEntityVirtual");
// Get the method to rewrite
var myDataProperty = type.Properties.Single(prop => prop.Name == "MyData");
var isModifiedSetMethod = type.Properties.Single(prop => prop.Name == "IsModified").SetMethod;
var setMethodBody = myDataProperty.SetMethod.Body;
// Initilize before rewriting (clear pre instructions, create locals and init them)
setMethodBody.Instructions.Clear();
var localDef = new VariableDefinition(module.TypeSystem.Boolean);
setMethodBody.Variables.Add(localDef);
setMethodBody.InitLocals = true;
// Get fields\methos to use in the new method body
var propBackingField = type.Fields.Single(field => field.Name == $"<{myDataProperty.Name}>k__BackingField");
var equalMethod =
myDataProperty.PropertyType.Resolve().Methods.FirstOrDefault(method => method.Name == "Equals") ??
module.ImportReference(typeof(object)).Resolve().Methods.Single(method => method.Name == "Equales");
var equalMethodReference = module.ImportReference(equalMethod);
// Start the rewriting
var ilProcessor = setMethodBody.GetILProcessor();
// First emit a Ret instruction. This is beacause we want a label to jump if the values are equals
ilProcessor.Emit(OpCodes.Ret);
var ret = setMethodBody.Instructions.First();
ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldarg_0)); // load 'this'
ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldfld, propBackingField)); // load backing field
ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldarg_1)); // load 'value'
ilProcessor.InsertBefore(ret, ilProcessor.Create(equalMethod.IsStatic ? OpCodes.Call : OpCodes.Callvirt, equalMethodReference)); // call equals
ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Stloc_0)); // store result
ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldloc_0)); // load result
ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Brtrue_S, ret)); // check result and jump to Ret if are equals
ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldarg_0)); // load 'this'
ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldc_I4_1)); // load 1 ('true')
ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Call, isModifiedSetMethod)); // set IsModified to 'true'
ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldarg_0)); // load this
ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldarg_1)); // load 'value'
ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Stfld, propBackingField)); // store 'value' in backing field
// here you can call to Notify or whatever you want
module.Write(assemblyPath.Replace(".dll", "_new") + ".dll"); // save the new assembly
C# code after:
public virtual string MyData
{
[CompilerGenerated]
get
{
return this.<MyData>k__BackingField;
}
[CompilerGenerated]
set
{
if (!this.<MyData>k__BackingField.Equals(value))
{
this.IsModified = true;
this.<MyData>k__BackingField = value;
}
}
}
IL code after:
IL_0000: ldarg.0
IL_0001: ldfld string ClassLibrary1.AnEntityVirtual::'<MyData>k__BackingField'
IL_0006: ldarg.1
IL_0007: callvirt instance bool [mscorlib]System.String::Equals(object)
IL_000c: stloc.0
IL_000d: ldloc.0
IL_000e: brtrue.s IL_001e
IL_0010: ldarg.0
IL_0011: ldc.i4.1
IL_0012: call instance void ClassLibrary1.AnEntityVirtual::set_IsModified(bool)
IL_0017: ldarg.0
IL_0018: ldarg.1
IL_0019: stfld string ClassLibrary1.AnEntityVirtual::'<MyData>k__BackingField'
IL_001e: ret
As I wrote, this is an example of how to do it in Cecil. In your real code, you can base on that, but with some changes.
For example, you can create private field for your property and not use the compiler generated backing field.
You may call OptimizeMacros.
Also if you know exactly which property you need to rewrite, you can call to other equal method, e.g. if it's string
, you can call a static method of type string op_Equality
or op?_Inequality
this is the ==
and !=
of string