.netvb.netdelegatesaddressof

Get Actual Parameter Types From Action Delegate


Context

Here's a very specific problem for ya. To sum things up, I have a class which queues delegates to run later. I know each method will only have one argument, so I stores these in a list of Action(Object). I can't use generics because different methods can have parameters of different types, so I use Object instead.

When it's time for the delegate to execute, the user passes an argument which will then be passed to the delegate method. The problem is in type checking: I wan't to throw a specific exception if the type of the parameter they supplied doesn't match the type that the delegate is expecting.

For example, if pass this method into my class:

Sub Test(SomeNumber As Integer)

and then try to run:

MyClass.ExecuteDelegate("DelegateArgument")

I want to be able to throw a certain exception saying that they tried to use a String as an Integer and include some custom information. The trouble is I can't find a way of doing this.

Problem

Since I'm storing the delegates as Action(Object) I have no idea what the actual type of the parameter is. I have not found any way to find this info, so I cannot compare the type of the parameter to the type of the user-supplied argument. When I use Action(Object).Method.GetParameters it only returns Object

Alternatively I've tried using a Try-Catch block to check for an InvalidCastException when I try to call Action(Object).Invoke() but that also catches any InvalidCastException within the delegate method, which i don't want.

Is there any way to achieve what I'm trying to do?


Solution

  • I ended up (after a lot of Googling) finding another angle and piecing together a solution I'm happy with.

    I stumbled onto this, which in itself wasn't really an answer (and had a lot of C# that I wasn't able read, let alone to translate into VB), but it did give me an idea. Here's how it ended up:

    My original sub to "register" a method with my class looked (simplified) like this:

    RegisterOperation(Routine As Action(Of Object))
    

    And it was called like this:

    RegisterOperation(AddressOf RoutineMethod)
    

    I've now overloaded this sub with one like this:

    RegisterOperation(Routine As MethodInfo, Optional Instance As Object = Nothing)
    

    And it can be called like this:

    RegisterOperation([GetType].GetMethod(NameOf(Routine))) 'For shared methods
    RegisterOperation([GetType].GetMethod(NameOf(Routine)), Me) 'For instance methods
    

    Sure it's not as pretty as AddressOf, but it's not too ornery and it works perfectly.

    I convert the MethodInfo into a delegate like this:

    Dim ParamTypes = (From P In Routine.GetParameters() Select P.ParameterType)
    Routine.CreateDelegate(Expression.GetDelegateType(ParamTypes.Concat({GetType(Void)}).ToArray), Instance)
    

    This creates a delegate which keeps all the original parameter data intact, and allows the user to register a method without having to declare the argument type in multiple places, while still giving me access to the number and type of the method's arguments so I can validate them.

    In addition, I have to give some credit to @TnTinMn for showing me that I could store my delegates as [Delegate] as opposed to hard-coding it as a specific Action(Of T). This allowed me to store all these various types of delegates together as well as leave my original implementation in place for backwards-compatibility. As long as I insure the parameter number and and types are correct, everything goes smoothly.

    EDIT:

    After using this a bit, I found a way to simplify it further. If the method the user is "registering" is declared within the class that is calling RegisterOperation, then I you can use StackTrace to find the calling type as shown here.

    I implemented this in another overload so that users only have to pass the name of the method like this:

    RegisterOperation(NameOf(Routine))
    
    'To get the method from the name:
    Dim Routine = (New StackTrace).GetFrame(1).GetMethod.ReflectedType.GetMethod(RoutineName)
    

    This makes it just as clean as AddressOf!