【问题标题】:IL Emit - set an existing property with a boolean value before notifypropertychangedIL Emit - 在 notifypropertychanged 之前使用布尔值设置现有属性
【发布时间】:2016-11-15 03:34:30
【问题描述】:

我正在为包含虚拟自动属性的 POCO 对象实现一个发出的 propertychanged 处理程序,并且我的代码可以工作到每当我更改基础属性时都会引发 propertychanged。这样做的原因是我正在与服务器共享一个 POCO 对象(无论好坏),我会将修改后的对象发送到服务器。我不能用属性装饰 POCO 对象(因为服务器也会有这些装饰器,因为我们共享公共类),并且由于政策的原因,我不能使用 Fody 或 PostSharp 等第三方工具。我需要跟踪该对象是否已被修改,而我被困在了这一点上。

这是用更改通知包装我的虚拟自动属性的 Emit:

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

我需要做的是获取现有“IsModified”布尔属性的set方法,如果属性值值则设置它。

这是我想要发出的示例(目前定义为具有虚拟自动属性的 POCO):

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

我已经被这个问题困扰了一段时间......我已经设法在创建的新代理类中创建了一个名为“NewIsModified”的新属性,但是,我非常希望重用现有的 IsModified我原来的 POCO 中的财产。

我希望我已经正确解释了我的问题并且易于理解。任何帮助将不胜感激,我希望它也能帮助其他人。

亲切的问候。

【问题讨论】:

  • mono cecil 是您可以接受的解决方案吗?

标签: c# poco inotifypropertychanged cil reflection.emit


【解决方案1】:

这是在 Mono.Cecil 中执行的工作代码

之前的C#代码:

public class AnEntityVirtual
{
    public virtual string MyData { get; set; }
    public virtual bool IsModified { get; set; }
}

set_MyData之前的IL代码:

IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: stfld string ClassLibrary1.AnEntityVirtual::'<MyData>k__BackingField'
IL_0007: ret

重写:

// 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#代码:

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代码:

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

正如我所写,这是一个如何在 Cecil 中执行此操作的示例。 在您的真实代码中,您可以以此为基础,但需要进行一些更改。

例如,您可以为您的属性创建私有字段,而不使用编译器生成的支持字段。

您可以调用 OptimizeMacros。

此外,如果您确切知道需要重写哪个属性,则可以调用其他相等方法,例如如果是string,可以调用字符串类型的静态方法op_Equalityop?_Inequality这是==string!=

【讨论】:

  • 非常感谢你,杜迪。不幸的是,我不能使用 Mono.Cecil,因为我们的内部政策限制非常有限。我也不知道类型的任何属性的名称,除了 isModifed 直到运行时,因为新代理是基于类型 创建的,尽管创建的代理类型将始终包含 isModified。我将采用您的方法并尝试使其与 IL Emit 一起使用。
  • @option 欢迎您。对于未知属性名称相同。只需枚举 type.Properties。使用 Reflection.Emit 实现它并不难,原理相同。如果你需要我这样做,也许我明天早上可以。
  • 非常感谢您提供 - 我已经完成了所有工作,我正在循环访问属性并向它们添加 notifypropertychanged 事件......这只是剩下要做的 isModified 部分:公共字符串MyData { ... set { if(_myData value) { IsModified = true; _myData = 价值; OnPropertyChanged("我的数据"); } } }
  • 感谢您的回答 - 我已经设法通过您的方法设置了内部 IsModified 字段: var isModifiedSetMethod = type.GetProperty("IsModified").SetMethod; ...然后将其传递给包装属性设置器的方法: setMethodWrapperIl.Emit(OpCodes.Ldarg_0); setMethodWrapperIl.Emit(OpCodes.Ldc_I4_1); setMethodWrapperIl.Emit(OpCodes.Call, prop); // 存储到 IsModified
  • @Option 欢迎您。如果它有效,您可以添加您的解决方案作为答案。如果我的回答足够好,你可以接受我的回答:)
猜你喜欢
  • 2011-05-04
  • 2017-10-10
  • 2023-03-08
  • 1970-01-01
  • 2023-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-05
相关资源
最近更新 更多