Home > Software design >  Call generic method and pass generic delegate parameter using Type set at runtime
Call generic method and pass generic delegate parameter using Type set at runtime

Time:10-08

EDIT: This is a Unity 2018.4 project, which uses a custom version of Mono. Scripting runtime version is ".NET 4.x Equivalent" and API compatibility level is ".NET Standard 2.0"


I have a custom event system using generics with AddListener<T> and RemoveListener<T> methods that accept a EventDelegate<T> parameter. Elsewhere in code, given a Type determined at runtime, I'd like to add and remove listeners using a delegate of the appropriate type and simply know when the event was raised. I don't need to know of any payload that comes with the event.

It may be worth nothing that <T> is always derived from a specific abstract base type.

In the following examples, I have a static generic method on a static class that takes a generic delegate as a parameter. I'm trying to invoke the generic method using Type at runtime and I'd like to pass the generic delegate of Type as a parameter. Is this possible without having an established method with the proper signature at compile time?

Static class with generic delegate and generic method that accepts the delegate as a parameter:

public static class GameEvent
{
   public delegate void EventDelegate<in T>(T e) where T : BaseEvent;

   public static void AddListener<T>(EventDelegate<T> listener) where T : BaseEvent
   {
      ...
   }

   public abstract class BaseEvent : EventArgs
   {
   }
}

Type will always be derived from a specific base type.

Example derived class:

public class CustomGameEvent : GameEvent.BaseEvent
{
   public CustomGameEvent(object a, object b) // Constructor has 0 or more parameters of unknown type.
   {
   }
}

The above setup works well, and I have numerous custom events that are functioning.

For example, in some class method, I would add a listener and implement a delegate method for receiving the event,

private void SomeMethod()
{
   GameEvent.AddListener<CustomGameEvent>(this.CustomGameEventHandler);
}

private void CustomGameEventHandler(CustomGameEvent e)
{
   ... // This works and I can access properties of `e`.
}

Now onto my issue.

Elsewhere, I would like to call GameEvent.AddListner<T> using a Type determined at runtime, where Type is derived from GameEvent.BaseEvent, however I'm not sure what to pass as a parameter when invoking the method. That's where I'm stuck.

I setup a method that receives a GameEvent.BaseEvent parameter instead of the derived Type. Does it even make sense to have a method that accepts the base type when I'd like the delegate method to accept the derived type?

private Type type = typeof(CustomGameEvent); // Some type that inherits from BaseEvent.

private void GameEventHandler(GameEvent.BaseEvent e)
{
   // Do something here. Don't care that e is the base type.
   // I just want to know when the `Type` event was raised.
   // Accept any parameter that is derived from GameEvent.BaseEvent.
}

private void AddListener()
{
   var mi = typeof(GameEvent).GetMethod(nameof(GameEvent.AddListener));
   // -> void AddListener[T](EventDelegate`1)

   var miGeneric = mi?.MakeGenericMethod(type);
   // -> void AddListener[CustomGameEvent](EventDelegate`1)

   var delegateType = typeof(GameEvent.EventDelegate<>).MakeGenericType(type);
   // -> GameEvent EventDelegate`1[CustomGameEvent]

   var parameters = new object[] { (GameEvent.EventDelegate<GameEvent.BaseEvent>)this.GameEventHandler };
   miGeneric?.Invoke(null, parameters);
   // -> When "AddListener<CustomGameEvent>()" is invoked, it receives a 
   //    "GameEvent EventDelegate`1[GameEvent BaseEvent]" not a
   //    "GameEvent EventDelegate`1[CustomGameEvent]"
}

This works for the most part and does what I'd expect, because I'm explicitly casting the this.GameEventHandler to (GameEvent.EventDelegate<GameEvent.BaseEvent>) and passing that as a parameter. But I'd like to cast the parameter object to the type stored in delegateType, so that AddListener<CustomGameEvent>() actually receives an EventDelegate<CustomGameEvent> and not a EventDelegate<GameEvent.BaseEvent>. Is that possible, and if so, how?

Can GameEventHandler(GameEvent.BaseEvent e) even be used as the delegate method if the object was cast to EventDelegate<CustomGameEvent> or would the invoke fail due to the signature mismatch?

I've tried using Convert.ChangeType(), however that fails with InvalidCastException: Object must implement IConvertible..

   var parameters = new object[]
      {
         Convert.ChangeType(
            (GameEvent.EventDelegate<GameEvent.BaseEvent>)this.GameEventHandler,
            delegateType)
      };

I also tried using a generic static helper method CastInto<T>(object obj), but that didn't cast as I hoped it would.

private void AddListener()
{
   ... // delegateType is stored

   var miCastInto = this.GetType().GetMethod(nameof(CastInto)).MakeGenericMethod(delegateType);
   // -> EventDelegate`1 CastInto[EventDelegate`1](System.Object), but is that what
   //    I want? Should it be:  EventDelegate`1[CustomGameEvent]
   //       CastInto[EventDelegate`1[CustomGameEvent]](System.Object)

   object o1 = (GameEvent.EventDelegate<GameEvent.BaseEvent>)this.GameEventHandler;
   // -> GameEvent.EventDelegate<GameEvent.BaseEvent> ... as expected.

   object o2 = miCastInto.Invoke(null, new[] { o });
   // -> GameEvent.EventDelegate<GameEvent.BaseEvent> ... not <CustomGameEvent>.

   var parameters = new object[] { castedObject }; // <- Wrong parameter type.

   ...
}

public static T CastInto<T>(object obj)
{
   return (T)obj;
}

I'm not even sure if I should be trying to finesse the GameEventHandler(GameEvent.BaseEvent e) delegate to typeDelegate. If it can be converted would GameEventHandler(GameEvent.BaseEvent e) still be called when the event was raised?

I saw Delegate.CreateDelegate(), but I don't really understand if that should be used here or how it should be used.

Whatever delegate is established, it would also be used with a similar GameEvent.RemoveListener<T> call at a later time (using the same Type), so the delegate probably has to be stored with the class instance and not re-created when used later. I'm assuming the second delegate creation would be seen as distinct (not the same reference to the original creation).

CodePudding user response:

You can use Delegate.CreateDelegate to create a delegate of a specific Type object.

var delegateType = typeof(GameEvent.EventDelegate<>).MakeGenericType(type);
// -> GameEvent EventDelegate`1[CustomGameEvent]

var methodInfo = typeof(YourClass).GetMethod(
    "GameEventHandler", BindingFlags.NonPublic | BindingFlags.Instance);

var parameters = new object[] { 
    Delegate.CreateDelegate(delegateType, this, methodInfo) 
};

The overload that you are calling here creates a delegate that calls an instance method. The second parameter indicates the instance on which you want the method to be called.

  • Related