VBA - Using .NET class library - vb.net

We have a custom class library that has been built from the ground up that performs a variety of functions that are required for the business model in place. We also use VBA to automate some data insertion from standard Microsoft packages and from SolidWorks.
To date we have basically re-written the code in the VBA application macro's, but now are moving to include the class library into the VBA references. We've registered the class library for COM interop, and made sure that it is COM visible. The file is referencable, we have added the <ClassInterface(ClassInterfaceType.AutoDual)> _ tag above each of the Public Classes, so that intellisense 'works'.
With that said, the problem now arises - when we reference the class library, for this instance let's call it Test_Object, it is picked up and seems to work just fine. So we go ahead and try a small sample to make sure it's using the public functions and returning expected values:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim test As New Test_Object.Formatting
Dim t As String
t = test.extractNumber("abc12g3y45")
Target.Value = t
End Sub
This works as expected, returning 12345 in the selected cell/s.
However, when I try a different class, following the exact same procedure, I get an error (Object variable or With block variable not set). Code is as follows:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim test As New Test_Object.SQLCalls
Dim t As String
t = test.SQLNumber("SELECT TOP 1 ID from testdb.dbo.TESTTABLE") 'where the string literal in the parentheses is a parameter that is passed.
Target.Value = t
End Sub
This fails on the t = test.SQLNumber line. It also fails on another function within that SQLCalls class, a function that returns the date in SQL format (so it is not anything to do with the connection to the database).
Can anyone assist in what could be causing this error? I've googled for hours to no avail, and am willing to try whatever it takes to get this working.
Cheers.
EDIT: (added in the .SQLNumber() method)
Function SQLNumber(query As String) As Double
Dim tno As Double
Try
Using SQLConnection As SqlConnection = New SqlConnection(Connection_String_Current)
SQLConnection.Open()
SQLCommand = New SqlCommand(query, SQLConnection)
tno = SQLCommand.ExecuteScalar
End Using
Catch ex As System.Exception
MsgBox(ex.Message)
End Try
Return tno
End Function
For comparison, the extractNumber() method:
Function extractNumber(extstr As String) As Double
Dim i As Integer = 1
Dim tempstr As String
Dim extno As String = ""
Do Until i > Len(extstr)
tempstr = Mid(extstr, i, 1)
If tempstr = "0" Or tempstr = "1" Or tempstr = "2" Or tempstr = "3" Or tempstr = "4" Or tempstr = "5" Or tempstr = "6" Or tempstr = "7" Or tempstr = "8" Or tempstr = "9" Or tempstr = "." Then
extno = extno & tempstr
End If
i = i + 1
Loop
If IsNumeric(extno) Then
Return CDbl(extno)
Else
Return 0
End If
End Function

With the help of vba4all, we managed to delve down right to the issue.
When I tried to create a new instance of an object using Dim x as new Test_Object.SQLCalls, I was completely oblivious to the fact that I had not re-entered this crucial line:
<ClassInterface(ClassInterfaceType.None)> _.
Prior to doing this, I had this in my object explorer which has both the ISQLCalls and SQLCalls in the Classes section
But wait, ISQLCalls isn't a class, it's an interface!
By entering the <ClassInterface(ClassInterfaceType.None)> _ back in the SQLCalls class, the object explorer looked a bit better:
And low and behold, I could now create a new instance of the class, and the methods were exposed.
tldr:
I needed to explicitly declare the interface and use <InterfaceType(ComInterfaceType.InterfaceIsDual)> on the interface and <ClassInterface(ClassInterfaceType.None)> on the class.
Many thanks to vba4all, who selflessly devoted their time to assist in this issue.

Related

VB.Net | Is there a way to reference a dynamic amount of variables as arguments to function/sub?

I'm trying to pass a dynamic amount of variables to a Sub by using ByRef;
Essentially I'm trying to create a module that I can easily import into my projects and make handling the file saving/loading process automated.
The Sub/Function would take a number of variables as references and then loop through them changing each one's value.
I realize I'm missing a crucial point in how visual basic's syntax works but I haven't been able to figure out what I need to do.
The code I've written for this is:
Public Sub LoadSaveToVars(ByRef KeyNamesAndVars() As Object, ByVal FileLoc As String = "")
If isEven(KeyNamesAndVars.Length) Then
Dim Contents As String = My.Computer.FileSystem.ReadAllText(FileLoc)
Dim isOnName As Boolean = True
Dim CurrentVal As String = ""
For i = 0 To KeyNamesAndVars.Length - 1
If isOnName Then
CurrentVal = GetStringValue(KeyNamesAndVars(i), Contents) 'Get the value of the key with the key name in the array
isOnName = False
Else
KeyNamesAndVars(i) = CurrentVal 'Set the variable referenced in the array to the value
isOnName = True
End If
Next
Else
Throw New ArgumentOutOfRangeException("The key names and variables supplied are not even.", "Error loading to variables!")
End If
End Sub
And here's how I try to use this function:
Dim TestVar1 As String = ""
Dim TestVar2 As String = ""
LoadSaveToVars({"key1", TestVar1, "key2", TestVar2})
To keep this question clean I did not include the other functions, but I did make a poor attempt at drawing what I want to happen: https://gyazo.com/eee34b8dff766401f73772bb0fef981a
In the end, I want TestVar1 to be equal to "val1" and TestVar2 to be equal to "val2" and to be able to extend this to a dynamic number of variables. Is this possible?

Getentity method in vb.net + Autocad?

ACADAPP = System.Runtime.InteropServices.Marshal.GetActiveObject("AutoCAD.Application")
ACADDOC = ACADAPP.Documents.ActiveDocument
second_POINT = ACADDOC.Utility.GetEntity(select_object, , "Select Object <Enter to Exit> : ")
ACADDOC.Utility.GetEntity returns an error as
type mismatch
in vb.net autocad,when I'm trying with vb6 it works fine.
What about that 2nd empty parameter - is that correct? According to the specification, it expects an object - a point.
object.GetEntity Object, PickedPoint [, Prompt]
Such as...
ThisDrawing.Utility.GetEntity returnObj, basePnt, "Prompt, i.e. Select an object"
By the way - is that really a VB.NET? Or Visual Basic for Application (VBA)? Notice, there are significant differences in syntax and capabilities... The AutoDesk general documentation (incl. online) would be for VBA, not VB.NET.
EDIT:
Dim returnObj As AcadObject
Dim basePnt As Variant
ThisDrawing.Utility.GetEntity returnObj, basePnt, "Select an object"
Note, that this example is for VBA, I've never worked with VB.NET and ACAD, I'm not even sure how it is supported.
Make sure you handle empty selection too...
Here's a simple function that will return a selected object.
The PromptEntityResult's ObjectId Property is the actual returned entity, which you will have to get to with a transaction.
Public Shared Function GetEntity() As PromptEntityResult
Dim retVal As PromptEntityResult = Nothing
Dim oDoc As Document = Core.Application.DocumentManager.MdiActiveDocument
Dim oEd As Editor = oDoc.Editor
Dim oPeo As New PromptEntityOptions(Environment.NewLine & "Please select an object")
With oPeo
.SetRejectMessage(Environment.NewLine & "Cannot select that object.")
.AllowNone = False
.AllowObjectOnLockedLayer = True
End With
retVal = oEd.GetEntity(oPeo)
Return retVal
End Function

Create Instance of Class with String VBA [duplicate]

Is there a way to set an object to the new instance of a class by using the text name of the class?
I will have a library of classes, and depending on some other variable, I want to get one of these classes at runtime.
E.g. I have "CTest1", "CTest2", "CTest3"
I would have function similar to the below
Function GetTestClass(lngClassNo as long) as Object
Dim strClassName as String
strClassName = "CTest" & CStr(lngClassNo)
Set GetTestClass = New instance of class(strClassName)
End Function
CallByName function can help you. Let's say there are some class modules in your project: clsSample0, clsSample1 and clsSample2. Add a new class module named clsSpawner, which lists all target classes as public variables having the same names, and declared with New keyword:
Public clsSample0 As New clsSample0
Public clsSample1 As New clsSample1
Public clsSample2 As New clsSample2
In a standard module add Function Spawn() code:
Function Spawn(sClassName) As Object
Set Spawn = CallByName(New clsSpawner, sClassName, VbGet)
End Function
Test it with some code like this:
Sub TestSpawn()
Dim objSample0a As Object
Dim objSample0b As Object
Dim objSample1 As Object
Dim objSample2 As Object
Set objSample0a = Spawn("clsSample0")
Set objSample0b = Spawn("clsSample0")
Set objSample1 = Spawn("clsSample1")
Set objSample2 = Spawn("clsSample2")
Debug.Print TypeName(objSample0a) ' clsSample0
Debug.Print TypeName(objSample0b) ' clsSample0
Debug.Print objSample0a Is objSample0b ' False
Debug.Print TypeName(objSample1) ' clsSample1
Debug.Print TypeName(objSample2) ' clsSample2
End Sub
How does it work? Spawn function instantiates clsSpawner and calls the clsSpawner instance to return requested property, and actually clsSpawner instance creates a new instance of the target class due to declaration with New keyword and returns the reference.
There's no reflection in VBA, so I don't think this is possible. You'd have to do something like the following I'm afraid:
Function GetTestClass(lngClassNo as long) as Object
Select Case lngClassNo
Case 1
Set GetTestClass = New CTest1
Case 2
Set GetTestClass = New CTest2
...
End Select
End Function
Unless that is your CTest classes are defined in a COM DLL, in which case you could use the CreateObject statement. You would need to use VB6 to create such a DLL though, you can't create DLLs in Excel, Access, etc.
Function GetTestClass(lngClassNo as long) as Object
Set GetTestClass = CreateObject("MyDll.CTest" & lngClassNo)
End Function
You can use metaprogramming to do this, although it does seem like quite a hack.
Here is an example that uses a couple of helper functions (omitted for brevity):
Public Function CreateInstance(typeName As String) As Object
Dim module As VBComponent
Set module = LazilyCreateMPCache()
If Not FunctionExists(typeName, module) Then
Call AddInstanceCreationHelper(typeName, module)
End If
Dim instanceCreationHelperName As String
instanceCreationHelperName = module.name & ".GetInstanceOf" & typeName
Set CreateInstance = Application.Run(instanceCreationHelperName)
End Function
Sub AddInstanceCreationHelper(typeName As String, module As VBComponent)
Dim strCode As String
strCode = _
"Public Function GetInstanceOf" & typeName & "() As " & typeName & vbCrLf & _
"Set GetInstanceOf" & typeName & " = New " & typeName & vbCrLf & _
"End Function"
Call AddFunction(strCode, module)
End Sub
VB class definitions are really defining COM interfaces behind the scenes, so one can define data types as an abstract interface definition with concrete implementations using the implements keyword.
To get any sort of polymorphism you have to do this, otherwise you will have problems with casting. It is somewhat fiddly but technically possible to do this with VB. If you want to dig into it find some of the advanced VB books by Dan Appleman or Matthew Kurland. I'm not sure if they're still in print but they're probably available through Amazon Marketplace.
This works with VB6 and I'm fairly sure it works with VBA.
You might be able to do it with a collection class or object array. All the objects are in one array.
In your class have a .Name property and when you create an instance of it do this:
Dim CTest() as New CTest
For n = 1 to 10
Redim Preserve CTest(n)
CTest(n).Name = "CTest" & CStr(n)
Next l
Quick and dirty. The above example would return 10 CTest objects in a single object array. You could also ditch the .Name and just use CTest(n).

How to harness auto-completion of strings?

I'm writing an application in which I have to pass strings as parameters. Like these:
GetValue("InternetGatewayDevice.DeviceInfo.Description")
GetValue("InternetGatewayDevice.DeviceInfo.HardwareVersion")
CheckValue("InternetGatewayDevice.DeviceInfo.Manufacturer")
ScrambleValue("InternetGatewayDevice.DeviceInfo.ModelName")
DeleteValue("InternetGatewayDevice.DeviceInfo.ProcessStatus.Process.1")
The full list is about 10500 entries, and i tought that i'd be really lost in searching if i misspell something.
So I am trying to declare a namespace for every string segment (separated by ".") and declare the last as a simple class that widens to a String of its FullName (except the base app namespace):
Class xconv
Public Shared Widening Operator CType(ByVal d As xconv) As String
Dim a As String = d.GetType.FullName
Dim b As New List(Of String)(Strings.Split(a, "."))
Dim c As String = Strings.Join(b.Skip(1).ToArray, ".")
Return c
End Operator
End Class
So I'd have these declarations:
Namespace InternetGatewayDevice
Namespace DeviceInfo
Class Description
Inherits xconv
End Class
End Namespace
End Namespace
This way IntelliSense is more than happy to autocomplete that string for me.
Now I'd have to do this for every possible string, so I opted (in order to retain my sanity) to make a method that does that:
Sub Create_Autocomlete_List()
Dim pathlist As New List(Of String)(IO.File.ReadAllLines("D:\list.txt"))
Dim def_list As New List(Of String)
Dim thedoc As String = ""
For Each kl As String In pathlist
Dim locdoc As String = ""
Dim el() As String = Strings.Split(kl, ".")
Dim elc As Integer = el.Length - 1
Dim elz As Integer = -1
Dim cdoc As String
For Each ol As String In el
elz += 1
If elz = elc Then
locdoc += "Class " + ol + vbCrLf + _
"Inherits xconv" + vbCrLf + _
"End Class"
Else
locdoc += "Namespace " + ol + vbCrLf
cdoc += vbCrLf + "End Namespace"
End If
Next
locdoc += cdoc
thedoc += locdoc + vbCrLf + vbCrLf
Next
IO.File.WriteAllText("D:\start_list_dot_net.txt", thedoc)
End Sub
The real problem is that this is HORRIBLY SLOW and memory-intense (now i dot a OutOfMemory Exception), and I have no idea on how Intellisense would perform with the (not available in the near future) output of the Create_Autocomlete_List() sub.
I believe that it would be very slow.
So the real questions are: Am I doing this right? Is there any better way to map a list of strings to auto-completable strings? Is there any "standard" way to do this?
What would you do in this case?
I don't know how Visual Studio is going to perform with thousands of classes, but your Create_Autocomlete_List method can be optimized to minimize memory usage by not storing everything in memory as you build the source code. This should also speed things up considerably.
It can also be simplified, since nested namespaces can be declared on one line, e.g. Namespace First.Second.Third.
Sub Create_Autocomlete_List()
Using output As StreamWriter = IO.File.CreateText("D:\start_list_dot_net.txt")
For Each line As String In IO.File.ReadLines("D:\list.txt")
Dim lastDotPos As Integer = line.LastIndexOf("."c)
Dim nsName As String = line.Substring(0, lastDotPos)
Dim clsName As String = line.Substring(lastDotPos + 1)
output.Write("Namespace ")
output.WriteLine(nsName)
output.Write(" Class ")
output.WriteLine(clsName)
output.WriteLine(" Inherits xconv")
output.WriteLine(" End Class")
output.WriteLine("End Namespace")
output.WriteLine()
Next
End Using
End Sub
Note the use of File.ReadLines instead of File.ReadAllLines, which returns an IEnumerable instead of an array. Also note that the output is written directly to the file, instead of being built in memory.
Note Based on your sample data, you may run into issues where the last node is not a valid class name. e.g. InternetGatewayDevice.DeviceInfo.ProcessStatus.Process.1 - 1 is not a valid class name in VB.NET. You will need to devise some mechanism to deal with this - maybe some unique prefix that you could strip in your widening operator.
I'm also not sure how usable the resulting classes will be, since presumably you would need to pass an instance to the methods:
GetValue(New InternetGatewayDevice.DeviceInfo.Description())
It seems like it would be nicer to have Shared strings on a class:
Namespace InternetGatewayDevice
Class DeviceInfo
Public Shared Description As String = "Description"
Public Shared HardwareVersion As String = "HardwareVersion"
' etc.
End Class
End Namespace
So you could just reference those strings:
GetValue(InternetGatewayDevice.DeviceInfo.Description)
However, I think that would be a lot harder to generate without creating name clashes due to the various levels of nesting.

split string error in a compiled VB.NET class

I'm having some trouble compiling some VB code I wrote to split a string based on a set of predefined delimeters (comma, semicolon, colon, etc). I have successfully written some code that can be loaded inside a custom VB component (I place this code inside a VB.NET component in a plug-in called Grasshopper) and everything works fine. For instance, let's say my incoming string is "123,456". When I feed this string into the VB code I wrote, I get a new list where the first value is "123" and the second value is "456".
However, I have been trying to compile this code into it's own class so I can load it inside Grasshopper separately from the standard VB component. When I try to compile this code, it isn't separating the string into a new list with two values. Instead, I get a message that says "System.String []". Do you guys see anything wrong in my compile code? You can find an screenshot image of my problem at the following link: click to see image
This is the VB code for the compiled class:
Public Class SplitString
Inherits GH_Component
Public Sub New()
MyBase.New("Split String", "Split", "Splits a string based on delimeters", "FireFly", "Serial")
End Sub
Public Overrides ReadOnly Property ComponentGuid() As System.Guid
Get
Return New Guid("3205caae-03a8-409d-8778-6b0f8971df52")
End Get
End Property
Protected Overrides ReadOnly Property Internal_Icon_24x24() As System.Drawing.Bitmap
Get
Return My.Resources.icon_splitstring
End Get
End Property
Protected Overrides Sub RegisterInputParams(ByVal pManager As Grasshopper.Kernel.GH_Component.GH_InputParamManager)
pManager.Register_StringParam("String", "S", "Incoming string separated by a delimeter like a comma, semi-colon, colon, or forward slash", False)
End Sub
Protected Overrides Sub RegisterOutputParams(ByVal pManager As Grasshopper.Kernel.GH_Component.GH_OutputParamManager)
pManager.Register_StringParam("Tokenized Output", "O", "Tokenized Output")
End Sub
Protected Overrides Sub SolveInstance(ByVal DA As Grasshopper.Kernel.IGH_DataAccess)
Dim myString As String
DA.GetData(0, myString)
myString = myString.Replace(",", "|")
myString = myString.Replace(":", "|")
myString = myString.Replace(";", "|")
myString = myString.Replace("/", "|")
myString = myString.Replace(")(", "|")
myString = myString.Replace("(", String.Empty)
myString = myString.Replace(")", String.Empty)
Dim parts As String() = myString.Split("|"c)
DA.SetData(0, parts)
End Sub
End Class
This is the custom VB code I created inside Grasshopper:
Private Sub RunScript(ByVal myString As String, ByRef A As Object)
myString = myString.Replace(",", "|")
myString = myString.Replace(":", "|")
myString = myString.Replace(";", "|")
myString = myString.Replace("/", "|")
myString = myString.Replace(")(", "|")
myString = myString.Replace("(", String.Empty)
myString = myString.Replace(")", String.Empty)
Dim parts As String() = myString.Split("|"c)
A = parts
End Sub
'
'
End Class
Well, knowing nothing about Grasshopper, I'm just going to have to guess...
System.String [] is what .NET would print if you called ToString() on a string array. So, I'm gonna guess that you've given Grasshopper an array where it wants a single string.
So, with a little further guessing, how 'bout we try:
Dim parts As String() = myString.Split("|"c)
For I = 0 to parts.Length -1
DA.SetData(I, parts[I])
Well, I tried the code snippet you suggested... but it didn't quite work. I think the problem in the original code is that I'm trying to send a list of values (ie. parts) to a single output node. So, when I use DA.SetData(0,parts) I'm writing a list of values to the first output node of my compiled component. However, I think the problem is that the component doesn't know that parts is a list. In the example I gave before, if my incoming string is "123,456" then my result split list should have two values (123 and 456). I don't think I have declared parts to be a list. Do you have any ideas on how to do this? Again, if you click on the link in the original email (while using Internet Explorer... I'm not sure why Firefox isn't opening it) you should see a screenshot of the setup in the Grasshopper plugin which should help give you an idea of what's going on. Thanks again for your help.