How do I assign an object type dynamically? - vb.net

I am trying to loop through all of the controls in a panel. Some of the controls as classes that I created. In those classes, I want a subroutine run when the object is removed. So I am trying to create a temporary object that I can use to run that routine.
For Each window As Control In main_window.Controls
If window.Handle = hdl Then
Dim temp_window as window.getType()
temp_window.close_me()
main_window.Controls.Remove(window)
End If
Next
However, the getType assignment is not allowed.
How can I accomplish this?

Object.GetType is not what you want, it returns the Type instance of the object which contains meta data of that type, normally used for reflection.
What is the actual type that you want? It has to have a close_me method. You could use OfType:
Dim windowsToClose = main_window.Controls.OfType(Of YourWindowType)().
Where(Function(w) w.Handle = hdl).
ToArray()
For Each window In windowsToClose
window.close_me()
main_window.Controls.Remove(window)
Next
Your For Each doesn't work for another reason: you can't remove items from the collection while you are enumerating it. Above approach stores the windows you want to remove in an array.

The right way to do this is to use a base class that your controls Inherit or an interface that your controls Implement with close_me on the base or interface. Then, you can TryCast each member of Controls to the base or interface and, if it succeeds, call close_me on it. If you use the base class approach, you may wish to make it abstract (MustInherit) and then close_me would be MustOverride, depending on if the behavior should be different in each derived type.
e.g. assuming you use ICloseable,
Interface ICloseable
Sub close_me()
End Interface
'...
For Each window As Control In main_window.Controls
If window.Handle = hdl Then
Dim asCloseable = TryCast(window, ICloseable)
If asCloseable IsNot Nothing Then
asCloseable.close_me()
EndIf
EndIf
Next

Related

How to handle object declaration in VBA (Error 91)

I'm stuck in VBA and I couldn't find a good answer in the other questions related to error 91. I want to create an object and store variables and arrays inside that object. I tried an approach like I would do in js:
Dim block As Object
...
Set block = Nothing
block.Name = "Unbekannter Prüfblock"
block.Awf = "Unbekannter Anwendungsfall"
block.Conditions = Array()
block.Checks = Array()
I use the "Set block = Nothing" because I will use it multiple times in a loop.
But all I get is error 91 - Object variable not set
How can I set the object?
Do I really have to declare everything in vba?
Isn't there a "stop annoying me with declaration notices" toggle? ;-)
Update
Thank you all so much for the detailed answers!
As suggested I created a class for "block" and also a class for "condition" and "check". Block for example:
Option Explicit
Public name As String
Public awf As String
Public conditions As Collection
Public checks As Collection
Then I use it inside my code like this:
Dim bl As Block
Dim co As Condition
Dim ce As Check
Set bl = New Block
bl.name = ws.Range("B" & i).value
bl.awf = ws.Range("B" & i).value
Set co = New Condition
co.attr = ws.Range("B" & i).value
co.value = ws.Range("C" & i).value
bl.conditions.Add co
VBA isn't Javascript; objects and their members cannot be created inline, they need a class definition.
When you make a member call against an object, you refer to it by name, and whenever that name refers to a null reference (Nothing) you'll get error 91.
To fix it, you need to ensure every member call is made against a valid object reference. Using the Set keyword you can assign such a reference, and to create a new instance of an object you can use the New keyword followed by the name of the class that defines the type you want a new instance of:
Dim Block As Object
Block.Something = 42 ' Error 91
Set Block = New SomeClass ' set reference
Block.Something = 42 ' OK
Note that because the object is declared As Object, every member call is late-bound (resolved at run-time); if the member doesn't exist (or if there's a typo), you'll get error 438 at run-time.
You can move this error to compile-time with early binding by using a more specific data type for the declaration:
Dim Block As SomeClass
Because the members of SomeClass are known at compile-time, the IDE will now provide you with a member completion list when you type up a member call, and typos will no longer be valid at compile-time: strive to remain in the early-bound realm whenever possible! Note: As Variant (explicit or not) is also similarly late-bound.
So we add a new class module and call it SomeClass and we add a couple of public fields:
Option Explicit
Public Name As String
Public Case As String
Public Condition As Variant
Public Check As Variant
And now you can create and consume a new instance of that class, and add instances of it to a collection to process later (note: you can't do that with a UDT/Type).
The VBIDE settings have an annoying option ("automatic syntax check", IIRC) that immediately pops a message box whenever there's a compilation error on the current line; uncheck it (invalid lines will appear in red, without a message box), but do have the "require variable declaration" setting checked: it will add Option Explicit to every module, and that will spare you from a number of easily avoidable run-time errors, moving them to compile-time.
In JS, you can add properties (together with values) on the fly to an object. That's not possible in VBA (and most other languages).
Your declaration Dim block As Object is defining a variable that is supposed to point to an Object. But it isn't pointing to anything yet, per default it is initialized with Nothing, which is, literally, nothing, and has neither properties nor methods, it's just a placeholder to signal "I don't point to anything yet". Furthermore, Object cannot be instantiated.
in VBA, you assign something to an object variable with Set (this is different to most other languages). If you want to create a new object, you use the keyword New.
However, before you do that, you need to know what kind of object (which class) you need. This can be an existing class, eg a Worksheet or a Range in Excel, or it can be an own defined class (by creating a new class module in your code). In any case, you need to define the properties and the methods of that class. Considering the most simple class module Class1 (of course you should think about a more usefull name):
Option Explicit
Public name as String
Public case as String
(I will not start to talk about private members and getter and setter).
You then write
Dim block As Class1
Set block = New Class1
block.name = "Unbekannter Prüfblock"
(But block.data = "Hello world" will not be possible as data is not a member of your class.)
Big advantage of this attempt is that the compiler can show you when you for example mistyped a property name before you even start your code (Debug->Compile). In JS, you will get either a runtime error or a new property on the fly - hard to find nasty bugs.
If you later need a new (empty) object, just create a new object using New. If the old object is not referenced anywhere else, the VBA runtime will take care that the memory is freed (so no need to write Set block = Nothing).
In case you really don't know the properties in advance (eg when you read XML or JSON files or a key-value list from an Excel sheet...), you can consider to use a Collection or a Dictionary. Plenty of examples on SO and elsewhere.
One remark to block.Condition = Array(). Arrays in VBA are not really dynamic, you cannot add or remove entries easily during runtime. In VBA, you have static and dynamic arrays:
Dim a(1 to 10) as String ' Static array with 10 members (at compile time)
Dim b() as String ' Dynamic array.
However, for dynamic members you need to define how many members you need before you write something into it, you use the ReDim statement for that. Useful if you can calculate the number of members in advance:
Redim b(1 to maxNames) ' Dynamic array, now with maxNames members (whatever maxNames is)
You can change the array size of a dynamic array with Redim Preserve, but that should be an exception (bad programming style, inperformant). Without Preserve, you will get a new array, but the former data is lost.
Redim Preserve b(1 to maxNames+10) ' Now with 10 more members.
If you really don't know the number of members and it can change often during runtime, again a Collection or a Dictionary can be the better alternative. Note that for example a Dictionary can itself a Dictionary as value, which allows to define Tree structures.
Regarding your issue adding to the collection:
You need to add this code to your class module "Block" - only then you can add objects to the collections
Private Sub Class_Initialize()
Set conditions = New Collection
set checks = new Collection
End Sub

Accessing an element of a custom type list by property value

Let's say I have a class called ControlInfo, which has the following properties:
Public Property Control As Control
Public Property ControlState As Int32
Now I want to create a method for accessing a specific instance of ControlInfo from a List, so I do this:
Public Function GetInstance(control As Control) As ControlInfo
For Each c As ControlInfo In list
If c.Control.Name = control.Name Then
Return c
End If
Next
Return Nothing
End Function
Is there a better way to access specific instance from a list of a custom class based on its property value? Is the loop approach bad for performance?
Sidenote: I have also considered the implementation of a Dictionary other than a List (so I would access an instance via a key, which can be a lot better than looping through all the values), but I feel that if I resort to that I would be avoiding a problem instead of solving it. Maybe I am being paranoid but I would like to know other valid approaches here, and be more certain of what should/shouldn't be done.
Thanks.

Get a list of Interface properties

I am working with a COM library in Visual Basic (VB.NET). I am trying to get a list of properties associated with an Interface; however, I am not able to get a list of interface properties. Can someone direct me on the best way to list properties on an Interface?
Below is some sample code that loops over all the properties of a class called "TextBox". The output from this code is a list all the class properties.
This particular code doesn't seem to work for interfaces. By this I mean that this code doesn't return the properties of an interface.
Dim txt As New TextBox
Dim type As Type = txt.GetType()
Dim properties() As PropertyInfo = type.GetProperties()
For Each p As PropertyInfo In properties
OutputWindow(p.Name)
Next
Image of COM Library with Interface HYSYS.Valve
Just replace txt.GetType() with the GetType() operator to specify type names instead:
Dim type As Type = GetType(HYSYS.Valve)
You would only use <object>.GetType() when you already have an existing instance of an object. To get the properties of a type in general, for instance a TextBox, it is better to do GetType(TextBox).

How to access this variable?

I'm new to VB and I'm currently migrating a vb6 app I did not write to .net and I'm struggling with this error,
If TypeOf Application.OpenForms.Item(i) Is frmAddChangeDelete Then
'UPGRADE_ISSUE: Control ctrlAddChangeDelete1 could not be resolved because it was within the generic namespace Form. Click for more: 'ms-help://MS.VSCC.v90/dv_commoner/local/redirect.htm?keyword="084D22AD-ECB1-400F-B4C7-418ECEC5E36E"'
If **Application.OpenForms.Item(i).ctrlAddChangeDelete1.ParentFormNumber = intFormNumber** Then
If Application.OpenForms.Item(i).Text = "Add Proofed Expense Items" Then
boolAddProofed = True
Exit For
ctrlAddChangeDelete1 is supposedly calling the friend class ctrlAddChangeDelete from a separate VB file, so I'm not sure why it's saying that
"'ctrlAddChangeDelete1' is not a member of 'System.Windows.Forms.Form'."
Any help is appreciated, thank you !
Application.OpenForms is a collection not strongly typed.
When you reference elements there you get back a generic Form.
In a generic Form there is no control named ctrlAddChangeDelete1
If you have a form derived class named frmAddChangeDelete and this class has a control named ctrlAddChangeDelete1 then you need to cast the reference stored in the OpenForms collection to your specific form class before trying to reference that control.
Moreover, to access that control from external code, you should also have the Modifiers property set to Public instead of the default Internal. Otherwise you will not be able to access the control from any code external to the class.
To retrieve correctly your form you can write
Dim delForm = Application.OpenForms.
OfType(Of frmAddChangeDelete)
FirstOrDefault()
If delForm Is Nothing Then
' No form of type frmAddChangeDelete is present in the collection
' write here your message and exit ?
Else
' Now you can use delForm without searching again in the collection
......
The code above uses the IEnumerable.OfType extension and this requires the Imports System.Linq.
If you don't want to use this then you can always use the TryCast operator to get the reference to the correct class
' Still you need a for loop up ^^^^ before these lines
Dim delForm = TryCast(Application.OpenForms(i), frmAddChangeDelete)
if delForm Is Nothing then
....
else
....

vb.net Invoke and Inheritance Design

this is my first post here.
I like to have some advice on designing a new module for our software.
It's written in vb6 and i want to rewrite it in vb.net. So I want to refactor it.
It does the following:
We got many templates for OpenOffice Documents. (about 300)
Each one has an unique identifier which is in a database and comes as the input.
Dending on this number i want to create the document. e.g. calc or writer or later something else.
Then call a method named doc[template_num] or sth.
I read about invoking methods. I think this as a good way to dynamically call methods by number. Tried to do this. I think I understood now how it works.
But I dont know how to handle the document type.
I want to reuse big parts of code due to all calc documents are created equal at the beginning. But filling the cells is just bit diffrent.
I dont know how to inherit and do method calls with invoke.
Maybe someone has a little code snip for me which can explain this to.
Or maybe some other good idea how to handle this problem.
I am thankful for any new thought on this.
You could, of course, just have a giant Select Case block, like this:
Public Sub CreateDoc(templateNum As Integer)
Select Case templateNum
Case 1
CreateDoc1()
Case 2
CreateDoc2()
Case 3
CreateDoc3()
End Select
End Sub
If you had a fairly small number of case statements that would probably be the better, simpler method. If however, as you described, you have many different possible ID's to look for, then that becomes impractical and I can appreciate your desire to invoke the correct method without a big Select Case block.
However, before I get into that, I feel like I should give you another option. Perhaps you don't really need to have that many different methods. Is it possible, for instance, that half of the template ID's actually all need to be handled in the same way, and the only difference is the template file name that needs to be different. If so, it is conceivable that the Select Case block wouldn't need to be that big. For instance:
Public Sub CreateDoc(templateNum As Integer)
Select Case templateNum
Case 1 To 100
CreateWriterDoc(GetTemplateFilePath(templateNum))
Case 101 To 200
CreateCalcDoc(GetTemplateFilePath(templateNum))
End Select
End Sub
Private Sub CreateWriterDoc(templateFilePath As String)
' Launch Writer with given template file
End Sub
Private Sub CreateCalcDoc(templateFilePath As String)
' Launch Calc with given template file
End Sub
Private Function GetTemplateFilePath(templateNum As Integer) As String
' Retrieve template file path for given ID
' (from DB, Array, List, Dictionary, etc.) and return it
End Sub
To me, that seems like a much simpler solution, if such a thing is possible. If not--if you really have entirely different logic which needs to be executed for each template ID, then there are several ways to do it. The first option would be to create a list of delegates which point to the appropriate method to call for each ID. For instance, you could store them in an array, like this:
Dim createDocDelegates() As Action =
{
AddressOf CreateDoc1,
AddressOf CreateDoc2,
AddressOf CreateDoc3
}
So now, those three delegates are indexed by number (0 through 2) in an array. You can invoke them by number, like this:
createDocDelegates(1).Invoke() ' Calls CreateDoc2
Or, if your ID's aren't sequential, you may want to use a Dictionary(Of Integer, Action), instead, like this:
Dim createDocDelegates As New Dictionary(Of Integer, Action)()
createDocDelegates.Add(1, AddressOf CreateDoc1)
createDocDelegates.Add(7, AddressOf CreateDoc7)
createDocDelegates.Add(20, AddressOf CreateDoc20)
Then you can call one by ID, like this:
createDocDelegates(7).Invoke() ' Calls CreateDoc7
Another option would be to create an Interface for an object that creates a document, like this:
Public Interface IDocCreator
Sub CreateDoc()
End Interface
Then you could implement a separate class for each type of template (each implementing that same interface). For instance:
Public Class Doc1Creator
Implements IDocCreator
Public Sub CreateDoc() Implements IDocCreator
' Do work
End Sub
End Class
Public Class Doc2Creator
Implements IDocCreator
Public Sub CreateDoc() Implements IDocCreator
' Do different work
End Sub
End Class
Then, you can create a list of those objects, like this:
Dim docCreators() As IDocCreator =
{
New DocCreator1(),
New DocCreator2()
}
Or:
Dim docCreators As New Dictionary(Of Integer, IDocCreator)()
docCreators.Add(1, New DocCreator1())
docCreators.Add(7, New DocCreator7())
docCreators.Add(20, New DocCreator7())
Then you can call one like this:
docCreators(1).CreateDoc()
The IDocCreator approach is very similar to the Delegate approach. Neither is the right or wrong way to do it. It depends on your style, what you are comfortable with, and your requirements. The main advantage of the IDocCreator approach is that you could easily add more properties and methods to it in the future. For instance, lets say that you also want to somewhere store a user-friendly, descriptive name for each template. It would be very easy to add a ReadOnly Property Name As String property to the IDocCreator interface, but if you go the list-of-delegates route, that would be more difficult and sloppy.
In any of the above examples, however, you still have to add the complete list of methods or delegates somewhere. So, while it's slightly less ugly than a giant Select Case block, it may still not be enough. If so, the technology you need to use is called Reflection. The System.Reflection namespace contains much of the reflection-related functionality. Reflection allows you to dynamically access and invoke portions of your code. For instance, you can use reflection to get a list of all of the properties or methods that are defined by a given class. Or you can use reflection to get a list of types that are defined by your assembly. Using reflection, it would be possible to get a method, by string name, and then invoke it. So, for instance, if you want to call the "CreateDoc1" method on the current object, you could do it like this:
Me.GetType().GetMethod("CreateDoc1").Invoke(Me, {})
Since you are calling it by its string name, you could build the method name via concatenation:
Dim methodName As String = "CreateDoc" & templateNum.ToString()
Me.GetType().GetMethod(methodName).Invoke(Me, {})
However, if you are going to use the reflection approach, instead of using the name of the method, like that, it would be cleaner, in my opinion, to use a custom attribute to tag each method. For instance, you could create an attribute class which allows you to decorate your methods like this:
<CreateDocMethod(1)>
Public Sub CreateDoc1()
' ...
End Sub
Then, at run-time, you can use reflection to find all of the methods that have that particular attribute and then invoke the right one.
The reason that I saved the discussion on reflection for last is because it is not as efficient and it can lead to brittle code. As such, it's best to only use reflection as a last-resort. There's nothing wrong with reflection, if you really need it, but, as a general rule, if there is another reasonable way to do something, which doesn't require reflection, then you probably ought to be doing it that other way.