Skip to content

Commit

Permalink
Add event support
Browse files Browse the repository at this point in the history
  • Loading branch information
JLChnToZ committed Jun 2, 2023
1 parent 5ca5b97 commit 45e095a
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 47 deletions.
2 changes: 2 additions & 0 deletions JLChnToZ.CommonUtils.Dynamic.Test/TestSubject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ internal class TestSubject: IInterface {

private Dictionary<string, string> undelyDictionary = new Dictionary<string, string>();

private event Action TestEvent;

private string this[string key] {
get {
lastCalledMethod = "this.get";
Expand Down
6 changes: 5 additions & 1 deletion JLChnToZ.CommonUtils.Dynamic/Limitless.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections;
using System.Linq.Expressions;
using System.Dynamic;

namespace JLChnToZ.CommonUtils.Dynamic {
Expand Down Expand Up @@ -110,6 +111,10 @@ public override bool TryGetMember(GetMemberBinder binder, out object result) {
result = new LimitlessInvokable(target, methods);
return true;
}
if (typeInfo.TryGetEvent(binder.Name, out var evt)) {
result = new LimitlessEvent(target, evt);
return true;
}
if (typeInfo.TryGetSubType(binder.Name, out var subType)) {
result = Static(subType);
return true;
Expand Down Expand Up @@ -283,5 +288,4 @@ public override bool Equals(object obj) {
return true;
}
}

}
81 changes: 81 additions & 0 deletions JLChnToZ.CommonUtils.Dynamic/LimitlessEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Dynamic;
using System.Linq.Expressions;
using System.Reflection;

namespace JLChnToZ.CommonUtils.Dynamic {
using static Utilites;
/// <summary>Special dynamic object that can be used to access events.</summary>
/// <remarks>This should not be directly used. Use <see cref="Limitless"/> instead.</remarks>
public class LimitlessEvent: LimitlessInvokable {
readonly EventInfo eventInfo;
readonly Lazy<MethodInfo> addMethod, removeMethod;
Delegate backingDelegate;

protected override MethodInfo[] MethodInfos {
get {
methodInfos[0] = eventInfo.GetUndelyRaiseMethod(out backingDelegate, target);
if (methodInfos[0] == null)
throw new InvalidOperationException("Event does not have a raise method.");
return methodInfos;
}
}

protected override object InvokeTarget => backingDelegate ?? target;

internal LimitlessEvent(object target, EventInfo eventInfo) : base(target, new MethodInfo[1]) {
this.eventInfo = eventInfo;
addMethod = new Lazy<MethodInfo>(GetAddMethod);
removeMethod = new Lazy<MethodInfo>(GetRemoveMethod);
}

MethodInfo GetAddMethod() => eventInfo.GetAddMethod(true);

MethodInfo GetRemoveMethod() => eventInfo.GetRemoveMethod(true);

public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object result) {
arg = InternalUnwrap(arg);
var eventType = eventInfo.EventHandlerType;
Delegate targetDelegate = null;
if (arg is Delegate argDelegate) {
var delegateType = argDelegate.GetType();
if (delegateType == eventType || delegateType.IsSubclassOf(eventType))
targetDelegate = argDelegate;
else {
// Rewrap delegate to avoid type mismatch
targetDelegate = Delegate.CreateDelegate(eventType, argDelegate, delegateType.GetMethod("Invoke"), false);
if (targetDelegate == null) { // Failed to rewrap, possibly incorrect signature
result = null;
return false;
}
}
} else if (arg is LimitlessInvokable invokable && !invokable.TryCreateDelegate(eventType, out targetDelegate)) {
result = null;
return false;
}
// Null is allowed, it will be ignored
switch (binder.Operation) {
case ExpressionType.AddAssign: {
var addMethod = this.addMethod.Value;
if (addMethod != null) {
addMethod.Invoke(target, new [] { targetDelegate });
result = this;
return true;
}
break;
}
case ExpressionType.SubtractAssign: {
var removeMethod = this.removeMethod.Value;
if (removeMethod != null) {
removeMethod.Invoke(target, new [] { targetDelegate });
result = this;
return true;
}
break;
}
}
result = null;
return false;
}
}
}
39 changes: 25 additions & 14 deletions JLChnToZ.CommonUtils.Dynamic/LimitlessInvokable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ namespace JLChnToZ.CommonUtils.Dynamic {
/// <summary>Special dynamic object that can be used to invoke methods with same name but different signatures.</summary>
/// <remarks>This should not be directly used. Use <see cref="Limitless"/> instead.</remarks>
public class LimitlessInvokable: DynamicObject {
readonly MethodInfo[] methodInfos;
readonly object target;
protected readonly MethodInfo[] methodInfos;
protected readonly object target;

protected virtual MethodInfo[] MethodInfos => methodInfos;

protected virtual object InvokeTarget => target;

internal LimitlessInvokable(object target, MethodInfo[] methodInfos) {
if (methodInfos == null || methodInfos.Length == 0)
throw new ArgumentNullException(nameof(methodInfos));
this.target = target;
this.methodInfos = methodInfos;
}
Expand Down Expand Up @@ -58,8 +60,8 @@ public override bool TryInvoke(InvokeBinder binder, object[] args, out object re

bool TryInvoke(object[] args, out object result) {
var safeArgs = args;
if (TryGetMatchingMethod(methodInfos, ref safeArgs, out var methodInfo)) {
result = InternalWrap(methodInfo.Invoke(target, safeArgs));
if (TryGetMatchingMethod(MethodInfos, ref safeArgs, out var methodInfo)) {
result = InternalWrap(methodInfo.Invoke(InvokeTarget, safeArgs));
InternalWrap(safeArgs, args);
return true;
}
Expand All @@ -73,28 +75,37 @@ public dynamic Invoke(params object[] args) {
throw new InvalidOperationException("No matching method found.");
}

public override bool TryConvert(ConvertBinder binder, out object result) =>
TryCreateDelegate(binder.Type, out result);
public override bool TryConvert(ConvertBinder binder, out object result) {
if (TryCreateDelegate(binder.Type, out var resultDelegate)) {
result = resultDelegate;
return true;
}
result = null;
return false;
}

/// <summary>Creates a delegate with the given type.</summary>
public dynamic CreateDelegate(Type delegateType) {
public Delegate CreateDelegate(Type delegateType) {
if (TryCreateDelegate(delegateType, out var result)) return result;
throw new InvalidOperationException("No matching method found.");
}

bool TryCreateDelegate(Type delegateType, out object result) {
public T CreateDelegate<T>() where T : Delegate => CreateDelegate(typeof(T)) as T;

internal protected bool TryCreateDelegate(Type delegateType, out Delegate result) {
if (delegateType.IsSubclassOf(typeof(Delegate))) {
var invokeMethod = delegateType.GetMethod("Invoke");
if (invokeMethod != null) {
var matched = Type.DefaultBinder.SelectMethod(
DEFAULT_FLAGS, methodInfos,
DEFAULT_FLAGS, MethodInfos,
Array.ConvertAll(invokeMethod.GetParameters(), GetParameterType), null
) as MethodInfo;
if (matched != null && matched.ReturnType == invokeMethod.ReturnType) {
var target = InvokeTarget;
result = target == null ?
Delegate.CreateDelegate(delegateType, matched, false) :
Delegate.CreateDelegate(delegateType, target, matched, false);
return true;
return result != null;
}
}
}
Expand All @@ -108,7 +119,7 @@ bool TryCreateDelegate(Type delegateType, out object result) {
LimitlessInvokable ResolveGenerics(Type[] types) {
if (types == null) throw new ArgumentNullException(nameof(types));
var filteredMethods = new List<MethodInfo>();
foreach (var methodInfo in methodInfos) {
foreach (var methodInfo in MethodInfos) {
var genericParams = methodInfo.GetGenericArguments();
if (genericParams.Length != types.Length) continue;
try {
Expand All @@ -117,7 +128,7 @@ LimitlessInvokable ResolveGenerics(Type[] types) {
throw ex;
} // Try next if it fails
}
return new LimitlessInvokable(target, filteredMethods.ToArray());
return new LimitlessInvokable(InvokeTarget, filteredMethods.ToArray());
}
}
}
95 changes: 63 additions & 32 deletions JLChnToZ.CommonUtils.Dynamic/TypeInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ namespace JLChnToZ.CommonUtils.Dynamic {
/// <summary>Internal struct that holds type members information.</summary>
public readonly struct TypeInfo {
static readonly Dictionary<Type, TypeInfo> cache = new Dictionary<Type, TypeInfo>();
readonly Type type;
internal readonly Type type;
readonly ConstructorInfo[] constructors;
readonly PropertyInfo[] indexers;
readonly Dictionary<string, MethodInfo[]> methods;
readonly Dictionary<string, PropertyInfo> properties;
readonly Dictionary<string, FieldInfo> fields;
readonly Dictionary<string, EventInfo> events;
readonly Dictionary<Type, MethodInfo> castInOperators;
readonly Dictionary<Type, MethodInfo> castOutOperators;
readonly Dictionary<string, Type> subTypes;
Expand All @@ -30,49 +31,77 @@ public static TypeInfo Get(Type type) {
methods = new Dictionary<string, MethodInfo[]>();
properties = new Dictionary<string, PropertyInfo>();
fields = new Dictionary<string, FieldInfo>();
events = new Dictionary<string, EventInfo>();
castInOperators = new Dictionary<Type, MethodInfo>();
castOutOperators = new Dictionary<Type, MethodInfo>();
subTypes = new Dictionary<string, Type>();
var tempMethods = new Dictionary<string, List<MethodInfo>>();
foreach (var m in type.GetMethods(DEFAULT_FLAGS)) {
var methodName = m.Name;
if (m.ContainsGenericParameters) {
int methodCountIndex = methodName.LastIndexOf('`');
if (methodCountIndex >= 0) methodName = methodName.Substring(0, methodCountIndex);
}
if (!tempMethods.TryGetValue(methodName, out var list))
tempMethods[methodName] = list = new List<MethodInfo>();
list.Add(m);
switch (methodName) {
case "op_Implicit":
case "op_Explicit": {
var parameters = m.GetParameters();
var returnType = m.ReturnType;
if (parameters.Length == 1 && returnType != typeof(void)) {
var inputType = parameters[0].ParameterType;
if (returnType == type)
castInOperators[inputType] = m;
else if (inputType == type)
castOutOperators[returnType] = m;
var tempIndexers = new List<PropertyInfo>();
var tempConstructors = new List<ConstructorInfo>();
foreach (var member in type.GetMembers(DEFAULT_FLAGS))
switch (member.MemberType) {
case MemberTypes.Constructor: {
tempConstructors.Add(member as ConstructorInfo);
break;
}
case MemberTypes.Event: {
var eventInfo = member as EventInfo;
events[eventInfo.Name] = eventInfo;
break;
}
case MemberTypes.Field: {
var field = member as FieldInfo;
fields[field.Name] = field;
break;
}
case MemberTypes.Method: {
var method = member as MethodInfo;
var methodName = method.Name;
switch (methodName) {
case "op_Implicit":
case "op_Explicit": {
var parameters = method.GetParameters();
var returnType = method.ReturnType;
if (parameters.Length == 1 && returnType != typeof(void)) {
var inputType = parameters[0].ParameterType;
if (returnType == type)
castInOperators[inputType] = method;
else if (inputType == type)
castOutOperators[returnType] = method;
}
break;
}
default: {
if (method.ContainsGenericParameters) {
int methodCountIndex = methodName.LastIndexOf('`');
if (methodCountIndex >= 0) methodName = methodName.Substring(0, methodCountIndex);
}
if (!tempMethods.TryGetValue(methodName, out var list))
tempMethods[methodName] = list = new List<MethodInfo>();
list.Add(method);
break;
}
}
break;
}
case MemberTypes.Property: {
var property = member as PropertyInfo;
if (property.GetIndexParameters().Length > 0) {
tempIndexers.Add(property);
break;
}
properties[property.Name] = property;
break;
}
}
}
foreach (var kv in tempMethods) methods[kv.Key] = kv.Value.ToArray();
var tempIndexers = new List<PropertyInfo>();
foreach (var p in type.GetProperties(DEFAULT_FLAGS)) {
properties[p.Name] = p;
if (p.GetIndexParameters().Length > 0) tempIndexers.Add(p);
}
foreach (var pair in tempMethods) methods[pair.Key] = pair.Value.ToArray();
indexers = tempIndexers.ToArray();
foreach (var f in type.GetFields(DEFAULT_FLAGS)) fields[f.Name] = f;
constructors = type.GetConstructors(INSTANCE_FLAGS);
constructors = tempConstructors.ToArray();
foreach (var t in type.GetNestedTypes(DEFAULT_FLAGS)) subTypes[t.Name] = t;
}

internal bool TryGetValue(object instance, string key, out object value) {
if (properties.TryGetValue(key, out var property) && property.CanRead && property.GetIndexParameters().Length == 0) {
if (properties.TryGetValue(key, out var property) && property.CanRead) {
value = InternalWrap(property.GetValue(instance));
return true;
}
Expand Down Expand Up @@ -105,7 +134,7 @@ internal bool TryGetValue(object instance, object[] indexes, out object value) {
}

internal bool TrySetValue(object instance, string key, object value) {
if (properties.TryGetValue(key, out var property) && property.CanWrite && property.GetIndexParameters().Length == 0) {
if (properties.TryGetValue(key, out var property) && property.CanWrite) {
property.SetValue(InternalUnwrap(instance), value);
return true;
}
Expand Down Expand Up @@ -175,5 +204,7 @@ internal bool TryConstruct(object[] args, out object result) {
}

internal bool TryGetSubType(string name, out Type type) => subTypes.TryGetValue(name, out type);

internal bool TryGetEvent(string name, out EventInfo evt) => events.TryGetValue(name, out evt);
}
}
17 changes: 17 additions & 0 deletions JLChnToZ.CommonUtils.Dynamic/Utilites.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,23 @@ public static string ToOperatorMethodName(this ExpressionType expressionType) {
default: return ""; // Fail silently
}
}

public static MethodInfo GetUndelyRaiseMethod(this EventInfo eventInfo, out Delegate backingDelegate, object instance = null) {
var raiseMethod = eventInfo.GetRaiseMethod(true);
backingDelegate = null;
if (raiseMethod == null) {
var instanceType = instance?.GetType() ?? eventInfo.DeclaringType;
var backingField = instanceType.GetField(eventInfo.Name, DEFAULT_FLAGS);
if (backingField != null) {
var backingFieldType = backingField.FieldType;
if (backingFieldType.IsSubclassOf(typeof(Delegate))) {
raiseMethod = backingFieldType.GetMethod("Invoke");
backingDelegate = backingField.GetValue(instance) as Delegate;
}
}
}
return raiseMethod;
}
}

internal enum MethodMatchLevel {
Expand Down

0 comments on commit 45e095a

Please sign in to comment.