Background:
I have a legacy application written in ASP (VBScript) which call COM components written in VB6. We are upgrading in phases and need to first update COM components to .NET while maintaining interoperability with ASP.
Situation:
I created a sample .NET class and exposed it to COM system using various attributes as per MSDN documentation
I am able to instantiate the class
I am able to call the desired method on the class
Problem:
Argument values are not being sent to COM method. e.g. all numeric types have value 0 inside the method; all reference types have null value inside the method. Literal strings are passed properly to the method. However, string variables are not passed.
Same issue with return value. Regardless of value returned by the method, the variable on ASP has default value (0 or null as the case may be).
COM Code
<ComVisible(True)>
<InterfaceType(ComInterfaceType.InterfaceIsIDispatch)>
Public Interface IClass1
Function Triple(ByVal input As String) As Integer
End Interface
<ComVisible(True)>
<ProgId("TESTCOM.Class1")>
<ClassInterface(ClassInterfaceType.None)>
Public Class Class1
Inherits ServicedComponent
Implements IClass1
Public Sub New()
' needed for COM
End Sub
Public Function Triple(input As String) As Integer Implements IClass1.Triple
IO.File.WriteAllText("C:\TestCOM.Class1_Triple.log", String.Format("[{0:s}] Input: {1}", Date.Now, input)) ''' this file is updated; so I know method is being called.
Return 97
End Function
End Class
Alternate COM Code
<ComVisible(True)>
<ProgId("TESTCOM.Class1")>
<ComClass("E26FE8A0-8AC7-4824-9776-30ECDD473AA3")>
Public Class Class1
'Inherits ServicedComponent
'Implements IClass1
Public Sub New()
' needed for COM
End Sub
Private Const LogMessageFormat As String = "[{0:s}] Input: {1}, {2}" + vbCrLf
Public Function Triple(csinput As String, nInput As Integer) As Integer 'Implements IClass1.Triple
IO.File.AppendAllText("C:\TestCOM.Class1_Triple.log",
String.Format(LogMessageFormat, Date.Now, if(csinput isnot Nothing, csinput, "**NULL**"), nInput))
Return 97
End Function
End Class
ASP Code
dim testNETCOM
set testNETCOM = Server.CreateObject("TESTCOM.Class1")
' ** use this to check if there was any error during instantiation
If Err.Number <> 0 Then
Response.Redirect("http://"&Err.Description&"/")
'Response.Write (Err.Description& "<br><br>")
else
'Response.Redirect("http://no-error/")
End If
' ** use this to check if object is actually instantiated
if testNETCOM is nothing then
Response.Redirect("http://no-com/")
else
'Response.Redirect("http://yes-com/")
end if
dim nInput
set nInput = 41
dim nOutput
set nOutput = -1
set nOutput = CLng(testNETCOM.Triple("test message")) ' this string is received in the method
set nOutput = CLng(testNETCOM.Triple(CStr(nInput))) ' this string is not received in the method
' ** use this to check if return value is what we expected
if nOutput <> 0 then
Response.Redirect("http://test/")
else
Response.Redirect("http://notest/") ''' **this happens**
end if
The issue is in the syntax on ASP
For simple data types, do not use 'set'
For objects, use 'set'
Because I was using set for simple types, there was an error during the assignment process and the value of the variable for null/nothing/empty.
set nInput = 41 ' this is wrong
nInput = 41 ' this is right
set nOutput = CLng(testNETCOM.Triple(CStr(nInput))) ' this is wrong
nOutput = CLng(testNETCOM.Triple(CStr(nInput))) ' this is right
Give either of these a try:
<ComVisible(True)>
<Guid("79571A9D-2345-48D9-8F86-7F6761A97DBA")>
<InterfaceType(ComInterfaceType.InterfaceIsIDispatch)>
Public Interface IClass1
Function Triple(ByVal input As String) As Integer
End Interface
<ComVisible(True)>
<Guid("E26FE8A0-8AC7-4824-9776-30ECDD473AA3")>
<ProgId("TESTCOM.Class1")>
<ClassInterface(ClassInterfaceType.None)>
Public Class Class1
Inherits ServicedComponent
Implements IClass1
Public Sub New()
' needed for COM
End Sub
Public Function Triple(input As String) As Integer Implements IClass1.Triple
IO.File.WriteAllText("C:\TestCOM.Class1_Triple.log", String.Format("[{0:s}] Input: {1}", Date.Now, input)) ''' this file is updated; so I know method is being called.
Return 97
End Function
End Class
Or:
<ComVisible(True)>
<ProgId("TESTCOM.Class1")>
<ComClass("E26FE8A0-8AC7-4824-9776-30ECDD473AA3", "79571A9D-2345-48D9-8F86-7F6761A97DBA")>
Public Class Class1
'Inherits ServicedComponent
'Implements IClass1
Public Sub New()
' needed for COM
End Sub
Private Const LogMessageFormat As String = "[{0:s}] Input: {1}, {2}" + vbCrLf
Public Function Triple(csinput As String, nInput As Integer) As Integer 'Implements IClass1.Triple
IO.File.AppendAllText("C:\TestCOM.Class1_Triple.log",
String.Format(LogMessageFormat, Date.Now, if(csinput isnot Nothing, csinput, "**NULL**"), nInput))
Return 97
End Function
End Class
Related
Module MyModule
Public Property alpha As String
Public Property beta As String
Public Property charlie As String
End MyModule
Public Class MyClass
Sub New()
Dim variableName as String = "beta"
MyModule.GetField(variableName) = "new value"
End Sub
End Class
I'm looking for a way to set the value of a variable in the module. But only the variable with the given variable name. The example above obviously does not work, but it provides an idea of my goal.
Imports System.Reflection
Public Class Form1
Sub New()
SetVariableByVariableName("beta", "new value")
End Sub
Public Sub SetVariableByVariableName(ByVal variableName As String, ByVal value As String)
Dim myFieldInfo As FieldInfo = GetType(MyModule).GetField(variableName, BindingFlags.Public Or BindingFlags.Static)
If myFieldInfo IsNot Nothing Then
myFieldInfo.SetValue(Nothing, value)
Console.WriteLine($"The value of {variableName} is now: {GetType(MyModule).GetField(variableName).GetValue(Nothing)}")
Else
Console.WriteLine($"Error: {variableName} does not exist in MyModule.")
End If
End Sub
End Class
Module MyModule
Public Property alpha As String
Public Property beta As String
Public Property charlie As String
End Module
You are on the right track with using reflection and FieldInfo to access the field in the MyModule module by its name. To set the value of the field, you need to pass two parameters to the SetValue method: the instance of the object (in this case, Nothing since the field is static), and the new value to assign to the field.
Here's how you can modify your SetVariableByVariableName method to achieve your goal:
Public Sub SetVariableByVariableName(ByVal variableName As String, ByVal value As String)
Dim myFieldInfo As FieldInfo = GetType(MyModule).GetField(variableName, BindingFlags.Public Or BindingFlags.Static)
If myFieldInfo IsNot Nothing Then
myFieldInfo.SetValue(Nothing, value)
Console.WriteLine($"The value of {variableName} is now: {MyModule.GetType().GetField(variableName).GetValue(Nothing)}")
Else
Console.WriteLine($"Error: {variableName} does not exist in MyModule.")
End If
End Sub
This method takes two parameters: the name of the variable you want to modify, and the new value you want to assign to it. It first retrieves the FieldInfo object for the variable with the given name from the MyModule module, using the GetField method of the Type class. Note that we have to specify the BindingFlags.Public and BindingFlags.Static flags to indicate that we want to access a public static field.
Next, it checks if the field exists. If it does, it calls the SetValue method on the FieldInfo object to assign the new value to the field. Finally, it retrieves the value of the field again to confirm that the new value has been assigned correctly, and prints it to the console.
If the field does not exist, it prints an error message to the console.
You can call this method from your MyClass constructor like this:
Public Class MyClass
Sub New()
SetVariableByVariableName("beta", "new value")
End Sub
End Class
This should set the value of the beta variable in the MyModule module to "new value" when a new instance of MyClass is created.
Here's an example based on my previous answer here:
MyModule:
Module MyModule
Public Property alpha As String
Public Property beta As String = "default beta"
Public Property charlie As String
End Module
Module1, which accesses the above "by name":
Imports System.Reflection
Module Module1
Sub Main()
Console.WriteLine("Module Property accessed directly: " & MyModule.beta)
Console.WriteLine("Module Property accessed 'by name': " & GetModulePropertyByName("MyModule", "beta"))
Console.WriteLine("Changing value...")
SetModulePropertyByName("MyModule", "beta", "Can you hear me now?")
Console.WriteLine("Module Property accessed directly: " & MyModule.beta)
Console.WriteLine("Module Property accessed 'by name': " & GetModulePropertyByName("MyModule", "beta"))
Console.WriteLine()
Console.Write("Press any key to quit...")
Console.ReadKey()
End Sub
Public Sub SetModulePropertyByName(ByVal moduleName As String, ByVal moduleField As String, ByVal value As String)
Dim myType As Type = Nothing
Dim myModule As [Module] = Nothing
For Each x In Assembly.GetExecutingAssembly().GetModules
For Each y In x.GetTypes
If y.Name.ToUpper = moduleName.ToUpper Then
myType = y
Exit For
End If
Next
If Not IsNothing(myType) Then
Exit For
End If
Next
If Not IsNothing(myType) Then
Dim flags As BindingFlags = BindingFlags.IgnoreCase Or BindingFlags.NonPublic Or BindingFlags.Public Or BindingFlags.Static Or BindingFlags.Instance
Dim pi As PropertyInfo = myType.GetProperty(moduleField, flags)
If Not IsNothing(pi) Then
pi.SetValue(Nothing, value)
End If
End If
End Sub
Public Function GetModulePropertyByName(ByVal moduleName As String, ByVal moduleField As String) As Object
Dim O As Object = Nothing
Dim myType As Type = Nothing
Dim myModule As [Module] = Nothing
For Each x In Assembly.GetExecutingAssembly().GetModules
For Each y In x.GetTypes
If y.Name.ToUpper = moduleName.ToUpper Then
myType = y
Exit For
End If
Next
If Not IsNothing(myType) Then
Exit For
End If
Next
If Not IsNothing(myType) Then
Dim flags As BindingFlags = BindingFlags.IgnoreCase Or BindingFlags.NonPublic Or BindingFlags.Public Or BindingFlags.Static Or BindingFlags.Instance
Dim pi As PropertyInfo = myType.GetProperty(moduleField, flags)
If Not IsNothing(pi) Then
O = pi.GetValue(Nothing)
End If
End If
Return O
End Function
End Module
Output:
Module Property accessed directly: default beta
Module Property accessed 'by name': default beta
Changing value...
Module Property accessed directly: Can you hear me now?
Module Property accessed 'by name': Can you hear me now?
Press any key to quit...
I want to use named error codes within my app. This should ensure, that every developer does not confuse numeric-only error codes with other codes, and also reduces the time a developer needs to realize what the error code should represent.
Compare this example:
Function New() As Integer
Return 0
End Function
with this example:
Function New() As Integer
Return ErrorCodes.ERROR_SUCCESS
End Function
Of course, I could let the developers write like the following:
Function New() As Integer
Return 0 ' ERROR_SUCCESS
End Function
However, the code above raises a pitfall when a developer updates the actual return code but forgets about the comment. Some developer look at the actual return code and some at the comment. I want to mitigate that confusion.
I come up the following class (extract):
Public Class ErrorCodes
Private msName As String = Nothing
Private miValue As Integer = 0
Public Shared ReadOnly ERROR_SUCCESS As ErrorCodes = New ErrorCodes("ERROR_SUCCESS", 0)
Private Sub New(ByVal psName As String, ByVal piValue As Integer)
msName = psName
miValue = piValue
End Sub
Public ReadOnly Property [Name] As String
Get
Return msName
End Get
End Property
Public ReadOnly Property [Value] As Integer
Get
Return miValue
End Get
End Property
Public Overrides Function ToString() As String
Return String.Format("[{0}]{1}", msName, miValue)
End Function
End Class
Now I want to use this ErrorCodes class like in the following example:
Function New() As Integer
Return ErrorCodes.ERROR_SUCCESS
End Function
As expected, I will produce an exception (type conversion) since the actual value I return is a instance of the class ErrorCodes instead of the generic data type Integer.
As you can see with the ToString() function, I let the class automatically/implicitly converts the instanced object into the generic data type String, when the class instance is assigned to a String typed variable.
Is there a way to do the same with the generic data type Integer like I did with ToString()?
I am using the .NET Framework 4.0, as for compatibility reasons with Windows XP SP3.
Another way to say what I want:
Dim stringVariable As String = ErrorCodes.ERROR_SUCCESS ' should be "[0]ERROR_SUCCESS".
Dim integerVariable As Integer = ErrorCodes.ERROR_SUCCESS ' should be 0.
I do not want to trigger implicit conversion warnings/errors, or to force the developer to typecast explicitly.
Yes you can do that with the use of Conversion Operators.
Here is the code:
Public Class Form1
Public Class ErrorCodes
Private msName As String = Nothing
Private miValue As Integer = 0
Public Shared Widening Operator CType(ByVal ec As ErrorCodes) As String
Return ec.ToString
End Operator
Public Shared Narrowing Operator CType(ByVal ec As ErrorCodes) As Integer
Return ec.Value
End Operator
Public Shared ReadOnly ERROR_SUCCESS As ErrorCodes = New ErrorCodes("ERROR_SUCCESS", 0)
Public Shared ReadOnly ERROR_FAILED As ErrorCodes = New ErrorCodes("ERROR_FAILED", 1)
Private Sub New(ByVal psName As String, ByVal piValue As Integer)
msName = psName
miValue = piValue
End Sub
Public ReadOnly Property [Name] As String
Get
Return msName
End Get
End Property
Public ReadOnly Property [Value] As Integer
Get
Return miValue
End Get
End Property
Public Overrides Function ToString() As String
Return String.Format("[{0}]{1}", msName, miValue)
End Function
End Class
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim em As String = ErrorCodes.ERROR_SUCCESS
Dim ev As Integer = ErrorCodes.ERROR_SUCCESS
Dim mm As String = String.Format("String: {0}, Value: {1}", em, ev)
MsgBox(mm)
End Sub
End Class
More info here
Hope this helps.
This, as jmcilhinney pointed out, uses Enums and the Description attribute.
Here is the class
'requires
' Imports System.Reflection
' Imports System.ComponentModel
Public Class ErrorCodes
Public Enum ErrCode 'these are the error codes
'note that the codes should be unique
<Description("Success")> ERROR_SUCCESS = 0
<Description("Error A")> ERROR_A = 1
End Enum
Public Class InfoForErrCode
Public TheDescription As String
Public TheValue As Integer
Public AsString As String
End Class
Public Shared Function Info(TheError As ErrCode) As InfoForErrCode
Dim rv As New InfoForErrCode
rv.TheDescription = GetDescription(TheError)
rv.TheValue = TheError
rv.AsString = TheError.ToString
Return rv
End Function
Private Shared Function GetDescription(TheError As ErrCode) As String
Dim rv As String = ""
Dim fi As FieldInfo = TheError.GetType().GetField(TheError.ToString())
Dim attr() As DescriptionAttribute
attr = DirectCast(fi.GetCustomAttributes(GetType(DescriptionAttribute),
False), DescriptionAttribute())
If attr.Length > 0 Then
rv = attr(0).Description
Else
rv = TheError.ToString()
End If
Return rv
End Function
End Class
And here is how it can be used
Dim foo As ErrorCodes.ErrCode = ErrorCodes.ErrCode.ERROR_SUCCESS
Dim inf As ErrorCodes.InfoForErrCode = ErrorCodes.Info(foo)
Stop 'examine inf
foo = ErrorCodes.ErrCode.ERROR_A
inf = ErrorCodes.Info(foo)
Stop 'examine inf
Is this possible?
Public Class Foo
public var as string
public sub Load(byval filename as string)
'This is done
[...]
oItem = [...] 'Code to deserialize
me = oItem 'THIS LINE
end sub
end class
So that you can load foo from the serialized file like so:
Public Sub Main
Dim f as Foo = new Foo()
f.load("myFile")
end sub
Up until now i've had a function that that returns Foo as Object (I'm trying to make the serialize / deserialize process general so I can just copy and paste and avoid explicitly setting each variable)
No, what you want to do in your case is have Load be a Shared Function that returns the newly created item.
More like:
Public Class Foo
public var as string
public Shared Function Load(byval filename as string) As Foo
'This is done
[...]
oItem = [...] 'Code to deserialize
Return oItem
end sub
end class
Public Sub Main
Dim f as Foo
f = Foo.load("myFile")
end sub
Also, rather than embedding the deserialization directly into each class, you can have a general purpose method, such as:
''' <summary>
''' This method is used to deserialize an object from a file that was serialized using the SoapFormatter
''' </summary>
''' <param name="sFileName"></param>
''' <returns></returns>
''' <remarks></remarks>
Public Function Deserialize(Of T)(ByVal sFileName As String) As T
' Exceptions are handled by the caller
Using oStream As Stream = File.Open(sFileName, FileMode.Open, IO.FileAccess.Read)
If oStream IsNot Nothing Then
Return DirectCast((New SoapFormatter).Deserialize(oStream), T)
End If
End Using
Return Nothing
End Function
which you can then call as:
Public Sub Main
Dim f as Foo
f = Deserialize(Of Foo)("myFile")
end sub
The code below works for the class that I hard coded "XCCustomers" in my RetrieveIDandName method where I use CType. However, I would like to be able to pass in various classes and property names to get the integer and string LIST returned. For example, in my code below, I would like to also pass in "XCEmployees" to my RetrieveIDandName method. I feel so close... I was hoping someone knew how to use CType where I can pass in the class name as a string variable.
Note, all the other examples I have seen and tried fail because we are using Option Strict On which disallows late binding. That is why I need to use CType.
I also studied the "Activator.CreateInstance" code examples to try to get the class reference instance by string name but I was unable to get CType to work with that.
When I use obj.GetType.Name or obj.GetType.FullName in place of the "XCCustomers" in CType(obj, XCCustomers)(i)
I get the error "Type 'obj.GetType.Name' is not defined" or "Type 'obj.GetType.FullName' is not defined"
Thanks for your help.
Rick
'+++++++++++++++++++++++++++++++
Imports DataLaasXC.Business
Imports DataLaasXC.Utilities
Public Class ucCustomerList
'Here is the calling method:
Public Sub CallingSub()
Dim customerList As New XCCustomers()
Dim customerIdAndName As New List(Of XCCustomer) = RetrieveIDandName(customerList, "CustomerId", " CustomerName")
'This code below fails because I had to hard code “XCCustomer” in the “Dim item...” section of my RetrieveEmployeesIDandName method.
Dim employeeList As New XCEmployees()
Dim employeeIdAndName As New List(Of XCEmployee) = RetrieveIDandName(employeeList, "EmployeeId", " EmployeeName")
'doing stuff here...
End Sub
'Here is the method where I would like to use the class name string when I use CType:
Private Function RetrieveIDandName(ByVal obj As Object, ByVal idPropName As String, ByVal namePropName As String) As List(Of IntStringPair)
Dim selectedItems As List(Of IntStringPair) = New List(Of IntStringPair)
Dim fullyQualifiedClassName As String = obj.GetType.FullName
Dim count As Integer = CInt(obj.GetType().GetProperty("Count").GetValue(obj, Nothing))
If (count > 0) Then
For i As Integer = 0 To count - 1
'Rather than hard coding “XCCustomer” below, I want to use something like “obj.GetType.Name”???
Dim Item As IntStringPair = New IntStringPair(CInt(CType(obj, XCCustomers)(i).GetType().GetProperty("CustomerId").GetValue(CType(obj, XCCustomers)(i), Nothing)), _
CStr(CType(obj, XCCustomers)(i).GetType().GetProperty("CustomerName").GetValue(CType(obj, XCCustomers)(i), Nothing)))
selectedItems.Add(Item)
Next
End If
Return selectedItems
End Function
End Class
'+++++++++++++++++++++++++++++++
' Below are the supporting classes if you need to see what else is happening:
Namespace DataLaasXC.Utilities
Public Class IntStringPair
Public Sub New(ByVal _Key As Integer, ByVal _Value As String)
Value = _Value
Key = _Key
End Sub
Public Property Value As String
Public Property Key As Integer
End Class
End Namespace
'+++++++++++++++++++++++++++++++
Namespace DataLaasXC.Business
Public Class XCCustomer
Public Property CustomerId As Integer
Public Property CustomerName As String
End Class
End Namespace
'+++++++++++++++++++++++++++++++
Namespace DataLaasXC.Business
Public Class XCCustomers
Inherits List(Of XCCustomer)
Public Sub New()
PopulateCustomersFromDatabase()
End Sub
Public Sub New(ByVal GetEmpty As Boolean)
End Sub
End Class
End Namespace
'+++++++++++++++++++++++++++++++
Namespace DataLaasXC.Business
Public Class XCEmployee
Public Property EmployeeId As Integer
Public Property EmployeeName As String
End Class
End Namespace
'+++++++++++++++++++++++++++++++
Namespace DataLaasXC.Business
Public Class XCEmployees
Inherits List(Of XCEmployee)
Public Sub New()
PopulateEmployeesFromDatabase()
End Sub
Public Sub New(ByVal GetEmpty As Boolean)
End Sub
End Class
End Namespace
From MSDN
CType(expression, typename)
. . .
typename : Any expression that is legal
within an As clause in a Dim
statement, that is, the name of any
data type, object, structure, class,
or interface.
This is basically saying you can't use CType dynamically, just statically. i.e. At the point where the code is compiled the compiler needs to know what typename is going to be.
You can't change this at runtime.
Hope this helps.
Since List(Of T) implements the non-generic IList interface, you could change your function declaration to:
Private Function RetrieveIDandName(ByVal obj As System.Collections.IList, ByVal idPropName As String, ByVal namePropName As String) As List(Of IntStringPair)
And then your troublesome line would become (with also using the property name parameters):
Dim Item As IntStringPair = New IntStringPair(CInt(obj(i).GetType().GetProperty(idPropName).GetValue(obj(i), Nothing)), _
CStr(obj(i).GetType().GetProperty(namePropName).GetValue(obj(i), Nothing)))
Of course, you could still have the first parameter by Object, and then attempt to cast to IList, but that's up to you.
ctype is used to convert in object type.
I find myself calling functions from lambdas frequently as the provided delegate does not match or does not have sufficient parameters. It is irritating that I cannot do lambda on subroutines. Each time I want to do this I have to wrap my subroutine in a function which returns nothing. Not pretty, but it works.
Is there another way of doing this that makes this smoother/prettier?
I have read that this whole lambda inadequacy will probably be fixed in VS2010/VB10 so my question is more out of curiosity.
A simple Example:
Public Class ProcessingClass
Public Delegate Sub ProcessData(ByVal index As Integer)
Public Function ProcessList(ByVal processData As ProcessData)
' for each in some list processData(index) or whatever'
End Function
End Class
Public Class Main
Private Sub ProcessingSub(ByVal index As Integer, _
ByRef result As Integer)
' (...) My custom processing '
End Sub
Private Function ProcessingFunction(ByVal index As Integer, _
ByRef result As Integer) As Object
ProcessingSub(index, result)
Return Nothing
End Function
Public Sub Main()
Dim processingClass As New ProcessingClass
Dim result As Integer
' The following throws a compiler error as '
' ProcessingSub does not produce a value'
processingClass.ProcessList( _
Function(index As Integer) ProcessingSub(index, result))
' The following is the workaround that'
' I find myself using too frequently.'
processingClass.ProcessList( _
Function(index As Integer) ProcessingFunction(index, result))
End Sub
End Class
If you find that you are doing it too often and generally with the same type of data, you can wrap the delegate in a class.
Create a base class that converts to the delegate:
Public MustInherit Class ProcessDataBase
Public Shared Widening Operator CType(operand As ProcessDataBase) as ProcessingClass.ProcessData
Return AddressOf operand.Process
End Sub
Protected MustOverride Sub Process(index As Integer)
End Class
Inherit from the class:
Public Class ProcessResult
Inherits ProcessDataBase
Public Result As Integer
Protected Overrides Sub Process(index as Integer)
' Your processing, result is modified.
End SUb
End Class
Use it:
Public Class Main()
Public Sub Main()
Dim processingClass As New ProcessingClass
Dim processor As New ProcessResult
processingClass.ProcessList(processor)
Dim result as integer=processor.Result
End Sub
End Class
It IS fixed in VB10, the VS10 Beta is available, if it's an option for you to use it. In VB10 you have lambdas without a return value, and inline subs/functions.
For now, maybe you could just forget lambdas and work with delegates instead? Something like:
processingClass.ProcessList(AddressOf ProcessingSub)