1

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?

Keith Stein
  • 6,235
  • 4
  • 17
  • 36
  • I'd recommend avoiding anything that negates strong typing (like using `object`) or that can fail due to an invalid cast at runtime. If you've gotten to the point where this seems necessary then it's good to take a step back, and then another, until this doesn't seem necessary. – Scott Hannen May 11 '17 at 02:21
  • I poked around at this some. If this were C# you could use `Action`, which would allow you to add actions that take different types of parameters, and then at runtime inspect the methods to see what the actual parameter type is. The equivalent in VB.NET is `object` without `Option Strict On`. It's never, ever a good idea to disable strict type checking. I'd be interested to know what the underlying problem is you're trying to solve. But type safety is your friend. If it stops being your friend then something else is wrong. – Scott Hannen May 11 '17 at 02:42
  • @scott-hannen I already am using Object with Option Strict Off (because it is off by default in VB.NET). – Keith Stein May 11 '17 at 14:08
  • I recommend turning option strict on. Think of it as a phenomenal, free upgrade to the language. The first round of compiler errors will be a pain, but also eye opening. – Scott Hannen May 11 '17 at 14:58

3 Answers3

0

Introduce a class which contains information of your method and use this for storing delegates.

Public Class MethodData
    Public ReadOnly Property ArgumentType As Type
    Public ReadOnly Property Method As Action(Of Object)

    Public Sub New(argumentType As Type, method As Action(Of Object))
        ArgumentType = argumentType
        Method = method
    End Sub
End Class

Public Sub MethodWithInteger(argument As Integer)

End Sub

Public Sub MethodWithString(argument As String)

End Sub

Dim myDelegates = New List(Of MethodData) From
{
    New MethodData(GetType(Integer), AddressOf MethodWithInteger),
    New MethodData(GetType(String), AddressOf MethodWithString)
}

'execute method
Dim inputArgument = "notInteger"
Dim methodWithInteger = myDelegates(0)
If methodData.ArgumentType Is inputArgument.GetType() Then
    methodData.Method.Invoke(inputArgument)
End If

You can put execute logic in the class MethodData.

Public Class MethodData
    Public ReadOnly Property ArgumentType As Type
    Public ReadOnly Property Method As Action(Of Object)

    Public Sub New(argumentType As Type, method As Action(Of Object))
        ArgumentType = argumentType
        Method = method
    End Sub

    Public Sub Execute(input As Object)
        If ArgumentType Is input.GetType() Then
            Method.Invoke(input)
        End If
    End Sub
End Class

Notice that code above(obviously your code too) will compile only when Option Strict set to Off. If you set it to On, as should be, you will be forced to cast or wrap your methods to Action(Of Object).

Fabio
  • 31,528
  • 4
  • 33
  • 72
0

I do not agree that you can not use generics. In the example shown below, I have defined the queue as a Dictionary to allow for retrieval based on an ID. I did this mainly as I dis not understand how you envisioned retrieving a given Action.

The queue itself stores the Actions as a delegate and the class provides methods to add and run the Action. The argument type information of the Action is retrieved via Reflection.

Public Class DelegateQueue
    Private queue As New Dictionary(Of String, [Delegate])

    Public Sub AddMethod(Of T)(id As String, action As Action(Of T))
        queue.Add(id, action)
    End Sub

    Public Sub RunMethod(Of T)(id As String, arg As T)
        Dim meth As [Delegate] = Nothing
        If queue.TryGetValue(id, meth) Then
            Dim parameters As Reflection.ParameterInfo() = meth.Method.GetParameters
            Dim passedType As Type = GetType(T)
            If parameters.Length > 0 AndAlso parameters(0).ParameterType Is passedType Then
                Dim act As Action(Of T) = CType(meth, Global.System.Action(Of T))
                act.Invoke(arg)
            Else
                Throw New ArgumentException(String.Format("passed argument of type {0} to Action(Of {1}). Method ID {2}", passedType.Name, parameters(0).ParameterType.Name, id))
            End If
        Else
            Throw New ArgumentException("Method ID "" " & id & """ not found.")
        End If
    End Sub
End Class

Example usage:

Sub Demo()
    Dim q As New DelegateQueue
    q.AddMethod(Of Int32)("S1", AddressOf S1)
    q.AddMethod(Of String)("S2", AddressOf S2)

    q.RunMethod("S1", 23)
    q.RunMethod("S1", "fred") ' throws runtime error
End Sub

Private Sub S1(i As Int32)
End Sub

Private Sub S2(i As String)
End Sub
TnTinMn
  • 11,522
  • 3
  • 18
  • 39
  • This is probably the best idea I've seen so far- I hadn't thought of using generics quite like that, or using Delegate as a base class for storing the methods. It works and only requires minimum changes to the way delegates are added to the list. My only problem is that it requires the argument type be hard-coded twice for every method: once in the method itself and once again when the method is added. It would be great if there were some way to infer or carry that data from the method itself. BTW in the real implementation I do use Dictionary, I just left that out for simplicity's sake. – Keith Stein May 11 '17 at 00:45
0

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!

Community
  • 1
  • 1
Keith Stein
  • 6,235
  • 4
  • 17
  • 36