如何在C#中使用DynamicObject实现事件访问器

3
我正在尝试使用C#的DynamicObject实现Qt类系统的通用包装器类。然而,我想编写以下代码:
dynamic obj = new SomeWrapperClass(....); // This extends DynamicObject
obj.OnMyEvent += (Action)(() => Console.WriteLine("DO something!"));

根据VS2010的要求,上述代码是有效的(需要显式转换为Action),但我该如何使用DynamicObject的方法来“捕捉”这个语句呢?
我尝试实现TryGetMember()方法,并确实能过捕捉到这条语句,但我不知道该返回什么才能使它正常工作。
有什么提示吗?
4个回答

2

在这种情况下,Reflector是您的好帮手。生成的第二行代码大致如下:

if(Binder.IsEvent("OnMyEvent", typeof(SomeWrapperClass)))
{
    Binder.InvokeMember("add_OnMyEvent", obj, myAction);
}
else
{
    var e = Binder.GetMember("OnMyEvent", obj);
    var ae = Binder.BinaryOperation(ExpressionType.AddAssign, e, myAction);
    Binder.SetMember("OnMyEvent", obj, ae);
}

如果你无法使用真实事件来处理OnMyEvent(在这种情况下,你可以依赖默认的DynamicObject实现),那么你需要返回一个实现AddAssign并返回类似于多路广播委托的东西。如果可能的话,我建议使用前者...
为了好玩,这里有一个hackish的例子,动态绑定OnMyEventOnMyOtherEvent:
public class SomeWrapperClass : DynamicObject
{
    public event Action OnMyOtherEvent;

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        if (binder.Name == "OnMyEvent")
        {
            result = OnMyOtherEvent;
            return true;
        }
        return base.TryGetMember(binder, out result);
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        if (binder.Name == "OnMyEvent" && value is Action)
        {
            OnMyOtherEvent = (Action)value;
            return true;
        }
        return TrySetMember(binder, value);
    }

    public void Test()
    {
        if (OnMyOtherEvent != null)
            OnMyOtherEvent();
    }

    private static void TestEventHandling()
    {
        dynamic obj = new SomeWrapperClass(); // This extends DynamicObject
        obj.OnMyEvent += (Action)(() => Console.WriteLine("DO something!"));
        obj.Test();
    }
}

首先,非常感谢您提供有关只查看生成的IL(或者一个相对较高层次的表示)的提示。我发现它如何处理GetMember的返回值非常有趣。我是否可以只使用静态的“模拟”事件来为所有我的事件从TryGetMember返回,然后在TryInvokeMember中执行实际绑定(针对add_XYZEvent)? - shartte
是的,那是我想尝试的。我不知道它是否能够很好地工作,但如果你弄清楚了,请分享一下 :)。 - dahlbyk

0

我阅读了所有的答案,真的想避免使用"TryGetMember"和"TrySetMember"的解决方案,因为它感觉非常不自然。
(您可以将事件保存在变量中,或编写类似于var x = dynObj.MyEvent + MyAction;的内容,而不会出现编译时或运行时错误。)

在查看dahlbyk的答案时,我发现在反编译代码中有一个检查"IsEvent",我希望它只返回true

我会尽量简短 - 我用一种非常非常非常hackish的方式使其工作(超过了“常规”反射hackish级别!)

对于此检查,没有调用DynamicObject API中的任何方法,除了GetMetaObject。我尝试重写它,但是我无法获得任何积极的结果。也许有更聪明的人可以证明我做得过头了。

在没有其他选择的情况下,我采用了最糟糕的方法来解决这个问题:颠覆.NET的内部工作,使用运行时补丁程序和大量反射。

解决方案是劫持类型为Microsoft.CSharp.RuntimeBinder.RuntimeBinder的程序集Microsoft.CSharp中名为BindIsEvent的函数的返回值。
当提供我的特定目标(SomeWrapperClass)和事件名称("OnMyEvent")时,它将其结果从"false"替换为"true"。
(实际上它并不返回一个bool,而是返回一个丑陋的内部类EXPR,大多数反射代码都试图创建它)。
在此处提供的Microsoft提供的参考源代码

我的解决方案使用了很多反射,所以我编写了一些辅助函数(Steal等...)

对于这个解决方案,你需要从NuGet获取Harmony补丁库

using System;
using System.Linq;
using System.Reflection;
using static DynamicEventsTest.ReflectionTricks;

namespace DynamicEventsTest
{
    public static class ReflectionTricks
    {
        private static BindingFlags ALL = (BindingFlags)0xffff;
        public static T Steal<T>(Type t, object o, string member) =>
                    (T)(t.GetFields(ALL).SingleOrDefault(fld => fld.Name == member)?.GetValue(o) ??
                    t.GetProperties(ALL).SingleOrDefault(prop => prop.Name == member)?.GetValue(o) ??
                    (t.GetMethods(ALL).Where(mth => mth.Name == member).Skip(1).Any() ?
                        (object)(t.GetMethods(ALL).Where(mth => mth.Name == member).ToArray()) : // Several overloads
                        t.GetMethods(ALL).SingleOrDefault(mth => mth.Name == member))); // Just a single overload (or null)
        public static T Steal<T>(object o, string member) => Steal<T>(o.GetType(), o, member);
        public static T Steal<T>(Type o, string member) => Steal<T>(o, null, member);
        public static T DeepSteal<T>(object o, string pathToInnerMember)
        {
            if (pathToInnerMember.Contains("."))
            {
                string rest = pathToInnerMember.Substring(0, pathToInnerMember.LastIndexOf('.'));
                pathToInnerMember = pathToInnerMember.Substring(pathToInnerMember.LastIndexOf('.') + 1);
                o = DeepSteal<object>(o, rest);
            }
            return Steal<T>(o, pathToInnerMember);
        }

        public static MethodInfo Overload(this MethodInfo[] overloads, params Type[] types) =>
            overloads.SingleOrDefault(
                mi => mi.GetParameters().Length == types.Length &&
                        mi.GetParameters().Zip(types, (pi, expectedType) => pi.ParameterType.IsAssignableFrom(expectedType)).All(b => b));
    }

    internal class Program
    {
        private static void PostHook(object __instance, object __0, object __1, object __2, ref object __result)
        {
            string name = Steal<string>(__0, "Name");
            // Debug print. You can delete this.
            Console.WriteLine("Trying to access: " + name);
            Array array = (Array)__1;
            object wrappedTarget = array.GetValue(0);
            object target = Steal<object>(wrappedTarget, "Value");

            // Make sure that target object and event name match what we want to patch
            if (target is SomeWrapperClass && name == "OnMyEvent")
            {
                Type[] types = __instance.GetType().Assembly.GetTypes();

                // Only touch this
                bool THE_ACTUAL_BOOLEAN_RESULT = true;

                // Don't touch this
                //
                Type PredefinedType = types.Single(t => t.Name.EndsWith("PredefinedType"));
                var PT_BOOL = Steal<object>(PredefinedType, "PT_BOOL");
                Type ConstValFactoryType = types.SingleOrDefault(t => t.Name.EndsWith("ConstValFactory"));
                MethodInfo GetBool = Steal<MethodInfo>(ConstValFactoryType, "GetBool");
                var CONSTVAL = GetBool.Invoke(null, new object[1] { THE_ACTUAL_BOOLEAN_RESULT });
                object SymbolLoader = Steal<object>(__instance, "SymbolLoader");
                var boolType = Steal<MethodInfo[]>(SymbolLoader, "GetReqPredefType").Overload(PredefinedType)
                    .Invoke(SymbolLoader, new object[1] { PT_BOOL });
                object m_exprFactory = Steal<object>(__instance, "m_exprFactory");
                MethodInfo CreateConstant = Steal<MethodInfo[]>(m_exprFactory, "CreateConstant").Overload(boolType.GetType(), CONSTVAL.GetType());
                var replacementResults = CreateConstant.Invoke(m_exprFactory, new object[2] { boolType, CONSTVAL });
                //
                // End

                // Debug prints to show we succeeded. You can Remove those.
                Console.WriteLine(" >> Original Results: " + __result);
                bool bRes = DeepSteal<bool>(__result, "val.boolVal");
                Console.WriteLine(" >> Converted to bool: " + bRes);
                Console.WriteLine(" >> Replacement Results: " + replacementResults);
                bRes = DeepSteal<bool>(replacementResults, "val.boolVal");
                Console.WriteLine(" >> Converted to bool: " + bRes);

                // Actually override the results
                __result = replacementResults;
            }
        }

        static void Main(string[] args)
        {
            // Retrieve the method "RuntimeBinder.BindIsEvent"
            AppDomain d = AppDomain.CurrentDomain;
            Assembly microsoftCSharpAssembly = d.GetAssemblies().Where(x => x.FullName.Contains("Microsoft.CSharp")).Single();
            Type[] types = microsoftCSharpAssembly.GetTypes();
            var RuntimeBinder = types.Single(yy => yy.Name.EndsWith("RuntimeBinder"));
            MethodInfo method = Steal<MethodInfo>(RuntimeBinder, "BindIsEvent");

            // Setup Harmony patching enviroment
            HarmonyLib.Harmony harmony = new HarmonyLib.Harmony("some.string");
            MethodInfo postHookMethod = typeof(Program).GetMethod("PostHook", BindingFlags.Static | BindingFlags.NonPublic);

            // Do the hooking
            harmony.Patch(method, postfix: new HarmonyLib.HarmonyMethod(postHookMethod));

            dynamic obj = new SomeWrapperClass();
            obj.OnMyEvent += (Action)MyEventHandler; // This will Trigger "TryInvokeMember" for "add_OnMyEvent" !
        }

        public static void MyEventHandler()
        {
        }
    }
}

已在.NET框架4.8上测试,但实际效果因版本差异而异(微软可以在任何.NET版本中更改这些内部API)。


0

使用反射调用您的Action

dynamic o = new SomeWrapperClass();
o.OnMyEvent += (Action)(() => Console.WriteLine("DO something!"));
var a = typeof(SomeWrapperClass).GetField("OnMyEvent", BindingFlags.Instance | BindingFlags.NonPublic);
(a.GetValue(o) as Action).Invoke();

输出: 做点什么!


0
我认为你混淆了事件和委托。事件实际上是委托,但你不能在委托中使用“add”和“remove”访问器——不过,对于两者,+=和-=的工作方式都是相同的。
obj.OnMyEvent += (Action)(() => Console.WriteLine("DO something!"));

这基本上是将目标添加到委托的调用列表中,因此该委托必须具有类似类型(在本例中为无参数Action委托)。

建议的实现如下:

private Action actiondelegate = (Action)(() => {});

public override bool TryGetMember(GetMemberBinder binder, out object result)
{
    if (binder.Name == "OnMyEvent")
    {
        result = actiondelegate;
        return true;
    }
}

请注意,您的 Action 委托中需要一个空的 Action - 这是因为如果它为空,则 TryGetMember 和 TrySetMember 将无法正常工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接