How can I implement event acessors with DynamicObject in C#
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).
精彩评论