开发者

How can I implement event acessors with DynamicObject in C#

开发者 https://www.devze.com 2023-01-27 05:12 出处:网络
I am trying to implement a generic Wrapper-Class for Qt\'s class system using C#\'s DynamicObject. However, I want to write 开发者_运维百科the following code:

I am trying to implement a generic Wrapper-Class for Qt's class system using C#'s DynamicObject. However, I want to write 开发者_运维百科the following code:

dynamic obj = new SomeWrapperClass(....); // This extends DynamicObject
obj.OnMyEvent += (Action)(() => Console.WriteLine("DO something!"));

The above is valid code according to VS2010 (the explicit cast to Action is required), but how exactly do i "catch" that statement using DynamicObject's methods?

I tried implementing TryGetMember() and it gets called for the statement, but I have no idea what I have to return to make it work.

Any hints?


Reflector is your friend on this one. The code generated for your second line looks something like this (approximately):

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

If you can't use a real event for OnMyEvent (in which case you can lean on the default DynamicObject implementation), then you'll need to return something that implements AddAssign returning something like a multicast delegate. I'd suggest the former, if possible...

For fun, here's a hackish example that dynamically binds OnMyEvent to OnMyOtherEvent:

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


Invoke your Action with reflection:

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

Output: DO something!


I think you are confusing events with delegates. Events are effectively delegates, but you cannot use the 'add' and 'remove' accessors with delegates - however the += and -= works the same with both.

obj.OnMyEvent += (Action)(() => Console.WriteLine("DO something!"));

This is basically adding a target to the invocation list of a delegate, so that delegate must have a similar type (in this case a parameterless Action delegate).

The suggested implementation is below:

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

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

Note that you need an empty Action in your Action delegate - this is because if it is null the TryGetMember and TrySetMember wont work correctly.


I read all the answers and I really wanted to avoid the "TryGetMember" and "TrySetMember" solutions since it feels very unnatural.
(You'd be able to save the event in a variable, or write stuff like var x = dynObj.MyEvent + MyAction; without compile time or runtime errors.)

Looking in dahlbyk's Answer I saw there is a check for "IsEvent" in the decompiled code and I wanted that to just return true.

I'll try to keep it short - I did managed to make it work with a very very very hackish way (more then "regular" reflection hackish level!)

No methods from DynamicObject's API are called for this check except GetMetaObject. I tried overriding it but I couldn't get any positive results. Maybe someone smarter can prove I overdid it.

Without any other options I resorted the wrost way to solve this: reversing the inner working of .NET, using a runtime patcher and a lot of reflection.

The solution is hijacking the return value of a function called BindIsEvent in the type Microsoft.CSharp.RuntimeBinder.RuntimeBinder in the assembly Microsoft.CSharp.
It replaces its result from "false" to "true" when my specific target (SomeWrapperClass) and event name ("OnMyEvent") are provided.
(It's not really returning a bool, it returns an ugly internal class called EXPR which most of the reflection code is trying to create).
Reference source code provided by Microsoft here

My solution uses a lot of refletion so I wrote some helper functions (Steal, etc...)

For this solution you need to get the Harmony patching library from NuGet.

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()
        {
        }
    }
}

Tested on .NET framwork 4.8, Your mileage may vary (MS can change those internal APIs in any version of .NET).

0

精彩评论

暂无评论...
验证码 换一张
取 消