Loop a collection made by VBA class - vba

I'm trying to loop a collection. I can expand and see "the elements" inside VBE but get the error:
"Object doesn't support this property or method".
In a standard module I have
Public collItems As Collection
The collection is populated inside a userform module on initialization
collItems.add cItems
cItems is an object made from New clsItems, which is the class module.
The class module consists of many userform controls like this:
Private WithEvents frm As msforms.Frame
Private WithEvents lbl As msforms.Label
Private WithEvents cmd As msforms.CommandButton
Private WithEvents txt1 As msforms.TextBox
Private WithEvents txt2 As msforms.TextBox
+++
Not sure if Private is the way to go on this.
When the userform finishes loading, a dynamic number of frames with all these textboxes inside each frame appears. It looks like a spreadsheet made from data inside MS Project. The command buttons job is to change a lot of the textbox's attributes/properties.
I want to loop the collItems collection but then I get an error.
I don't have any get or let in my class, just a single set property.
It might look stupid to add 10 unique textboxes inside the class, but that's how I made it work so far. All the form objects are given names that refer to row and column during creation and helps me identify them.
The failing code looks like this:
Sub changeBox(ByRef name As String)
For Each item in collItems.item(CLng(Replace(name, "cmd", "")))
'blabla
Next item
End Sub
This test works and shows all the elements I want to loop:
Set test = collItems.item(3) 'Meaning row 3 in userform
How do I loop my specific textboxes and change their attributes?

So now I want to loop the collItems collection and write my code. But then I get an error...
The collection holds a sequence of objects of type cItems. Thus to loop the collection items, declare an object of this type to be used as the iterator.
Note, the class members must be publicly exposed in order to be accessible outside the cItems class. Not sure how you instantiate them, but they could be exposed through a public read-only property.
Public Property Get Form() As msforms.Frame
Set Form = frm
End property
To loop:
Dim objCurrent As cItems, frm As msforms.Frame
For Each objCurrent in collItems
'here you can use the objCurrent to access the current item.
Set frm = objCurrent.Form
'...
Next objCurrent
To be able to get a control by name, create a dictionary in the cItems class to store the controls and use the key to retrieve the objects.
See an example below:
'dictionary
Private m_boxes As Object
Public Sub AddTextBox(ByVal Key As String, ByVal obj As msforms.TextBox)
m_boxes.Add Key, obj
End Sub
Public Function GetTextBox(ByVal Key As String) As msforms.TextBox
Set GetTextBox = m_boxes(Key)
End Function
Private Sub Class_Initialize()
Set m_boxes = CreateObject("Scripting.Dictionary")
End Sub
Private Sub Class_Terminate()
Set m_boxes = Nothing
End Sub
The to call it:
Dim txt As msforms.TextBox
Set txt = objCurrent.GetTextBox("txt1")

Related

How to raise new event that triggered from 1 of multi instances of child-class in VBA?

I have example clsChild in VBA which have nothing than 1 var and 1 event
Private V#
Public Event Change()
Public Property Get Value()
Value = V
End Property
Public Property Let Value(inp#)
V = inp
RaiseEvent Change
End Property
then I create another class called clsParent, which have multi instances of clsChild:
Public ListofChild()
Public Event ParentChange()
Private Sub Class_Initialize()
redim Listofchild(1 to 3)
set listofchild(1) = new clsChild
set listofchild(2) = new clsChild
set listofchild(3) = new clsChild
End Sub
How could I raise parentChange event when 1 of those instances of clsChild change (known by change event of clsChild)?
You need a WithEvents reference to each child object in your parent class in order to be able to capture and handle its events.
Public Event ParentChange()
Private WithEvents objChild As clsChild
Private Sub Class_Initialize()
Set objChild = new clsChild
End Sub
'EventHandler
Private Sub objChild_Change()
RaiseEvent ParentChange
Debug.Print "Change event handled."
End Sub
WithEvents can only be declared at module level in Class modules.
Once you prefix an object with the WithEvents keyword, the object's events are listed in the events combo in VBE. Prefixing an object with the WithEvents keyword while the object has no events, a compile error occurs.
You can find plenty of resources online. See another example here.

Automatically terminate class while children still reference it

I have created a class where its children hold a necessary reference to their parent. This means, when the parent class goes out of scope in the routine which calls it, its terminate code is not called, as the children still hold a reference to it.
Is there a way of terminating the parent without having to manually terminate the children first too?
Current work around is to make the parent's terminate code (which contains code to terminate the children) public and to call it from the routine, but this is not ideal as then the parent is terminated twice (once manually, once when it leaves the caller sub's scope). But mainly I don't like having to call it manually
'Caller code
Sub runTest()
Dim testClass As parentClass
Set testClass = New parentClass
Set testClass = Nothing
End Sub
'parentClass
Private childrenGroup As New Collection
Private Sub Class_Initialize()
Dim childA As New childClass
Dim childB As New childClass
childA.Setup "childA", Me 'give name and parent reference to child class
childB.Setup "childB", Me
childrenGroup.Add childA 'add children to collection
childrenGroup.Add childB
End Sub
Public Sub Class_Terminate()
Set childrenGroup = Nothing
Debug.Print "Parent terminated"
End Sub
'childClass
Private className As String
Private parent As classParent
Public Sub Setup(itemName As String, parentObj As classParent)
Set parent = parentObj 'set the reference
className = itemName
End Sub
Public Property Get Name() As String
Name = className
End Property
Private Sub Class_Terminate()
Debug.Print Name;" was terminated" 'only called when parent terminates child
End Sub
Calling runTest() prints
childA was terminated
childB was terminated
Parent terminated
Parent terminated 'to get around this, you could just make another sub in the parent class
'to terminate its children
'but that still requires a manual call
Having reviewed your comments, I'm still not convinced you need to pass the parent into the child class. If your only reason for doing so is to create a kind of callback then you'd probably be better off passing an event delegate class to the child instead of the parent class and then simply handle the delegate's events in your parent class. As you've seen, your current structure is causing some object disposal issues and these can be avoided.
Simply create a class containing your child events. In this example I've called the class clsChildEvents:
Option Explicit
Public Event NameChange(obj As clsChild)
Public Sub NotifyNameChange(obj As clsChild)
RaiseEvent NameChange(obj)
End Sub
Now your code remains pretty much as is. The key difference is that you are passing the delegate instead of the parent to the child objects.
Parent class:
Option Explicit
Private WithEvents mChildEvents As clsChildEvents
Private mChildren As Collection
Public Sub SetUp()
Dim child As clsChild
Set child = New clsChild
child.SetUp "ChildA", mChildEvents
mChildren.Add child
Set child = New clsChild
child.SetUp "ChildB", mChildEvents
mChildren.Add child
End Sub
Private Sub mChildEvents_NameChange(obj As clsChild)
Debug.Print "New name for "; obj.Name
End Sub
Private Sub Class_Initialize()
Set mChildEvents = New clsChildEvents
Set mChildren = New Collection
End Sub
Private Sub Class_Terminate()
Debug.Print "Parent terminated."
End Sub
Child class:
Option Explicit
Private mClassName As String
Private mEvents As clsChildEvents
Public Sub SetUp(className As String, delegate As clsChildEvents)
Set mEvents = delegate
Me.Name = className
End Sub
Public Property Get Name() As String
Name = mClassName
End Property
Public Property Let Name(RHS As String)
mClassName = RHS
mEvents.NotifyNameChange Me
End Property
Private Sub Class_Terminate()
Debug.Print mClassName; " terminated."
End Sub
And then your module code:
Option Explicit
Public Sub Test()
Dim parent As clsParent
Set parent = New clsParent
parent.SetUp
Set parent = Nothing
End Sub
The immediate window output is as follows:
New name for ChildA
New name for ChildB
Parent terminated.
ChildA terminated.
ChildB terminated.
No, you can't automatically terminate the children by simply terminating the collection:
Public Sub Class_Terminate()
Set childrenGroup = Nothing
The Collection object does not clean up any object references when it is set equal to Nothing, and it doesn't have a 'RemoveAll' mechanism which does.
...And I wouldn't bet on 'Remove' doing that either, one item at a time. So you do indeed need to loop backwards through the members, in exactly the 'manual' process you're trying to avoid:
Public Sub Class_Terminate()
Dim i As Integer
For i = childrenGroup.Count To 1 Step - 1
Set childrenGroup(i) = Nothing
childrenGroup.Remove i
Next I
Set childrenGroup = Nothing
My advice would be: use a Scripting.Dictionary object instead of the VBA Collection:
Private childrenGroup As New Scripting.Dictionary
You'll need a reference to the Scripting Runtime (or to the Windows Scripting Host Object Model) and you may be surprised by the changed order of .Add Item, Key - and you will definitely be surprised by what happens when you request an item with a non-existent key.
Nevertheless, this works. The Dictionary does have a RemoveAll method, and this will clear all the references in its .Items when you call it:
Public Sub Class_Terminate()
childrenGroup.RemoveAll
You do need to call RemoveAll - it doesn't happen automatically if the dictionary goes out of scope - but that's as close as you'll get.
Note, also, that VBA runs on reference counting: if anything else has a reference to the children, the objects won't be released.

Create and loop through collection subset of controls

Im making a small vb.net windows form application in which I have 4 ComboBoxes. I would like to add the ComboBoxes to a collection and be able to loop through that collection to refer to each one.
There are other ComboBoxes on the form so I cannot just use the collection for the entire form (the form layout cannot be changed, e.g. to add a container, etc).
I was thinking something like the following:
Public Class Form1
Dim IoTypeCombos As New ControlCollection(Me) From {Me.IO1_ComboBox, Me.IO2_ComboBox, Me.IO3_ComboBox, Me.IO4_ComboBox}
Dim IoTypes As New Collection() From {"Out 0", "Out 1", "Input", "Analog"}
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
For Each cb As combobox In Me.IoTypeCombos
FillComboBox(cb, Types)
Next
End Sub
Function FillComboBox(cb As Control, cc As Collection) As Boolean
Dim cbc As ComboBox = CType(cb, ComboBox)
If cc.Count = 0 Then
Return False
End If
For Each cn In cc
cbc.Items.Add(cn)
Next
Return True
End Function
This doesn't raise any exception, BUT it doesn't populate the ComboBoxes either :(
The FillComboBox() works perfectly if I pass a single control to it.
What am I doing wrong? Thanks
This line is illegal:
Public Class Form1
Dim IoTypeCombos As New ControlCollection(Me) From {Me.IO1_ComboBox,
Me.IO2_ComboBox, Me.IO3_ComboBox, Me.IO4_ComboBox }
That code will run before the constructor, before Me or ION_ComboBox exist. In this case, the resulting collection contains nothing since there is nothing to put in it yet.
In other cases, referencing controls before they exist can result in a NullReference being thrown, but due to an odd bug it may not be reported. When that happens, the rest of the code is skipped and the form simply shown.
In either case, the solution is to declare your collection at the form level, but populate it in the form load event once the controls do exist. I would also use a Collection(Of T) instead (an array or List(Of T) will also work, the OP uses/asks about a collection though):
Imports System.Collections.ObjectModel
Public Class Form1
Dim IoTypeCombos As Collection(Of ComboBox) ' form and controls Do No Exist yet
Public Sub New
'...
InitializeComponent()
' NOW they exist
End Sub
Sub Form_Load
IoTypeCombos = New Collection(Of ComboBox)
IoTypeCombos.Add(IO1_ComboBox)
IoTypeCombos.Add(IO2_ComboBox)
...
If you use a List(Of ComboBox), you can populate it different ways:
' in the ctor:
IoTypeCombos = New List(Of ComboBox)({IO1_ComboBox, IO2_ComboBox...})
' using AddRange:
IoTypeCombos.AddRange({IO1_ComboBox, IO2_ComboBox...})
Not sure if you need the where clause, but if you have other comboboxes that do not have names like this and do not want them in the collection then you do need it.
Dim IoTypeComboboxes =
Me.Controls.OfType(Of Combobox)().Where(Function(cb) cb.Name.StartsWith("IO")).ToList()
'on yourFormName
'added :
'45 PictureBox:("PicBarNum1_NotLastOdig" to "PicBarNum45_NotLastOdig")
'added:
'45 PictureBox:("PicBarNum1_UkOdig" to "PicBarNum45_UkOdig")
Public Class yourFormName
Private picbarlistNum1to45_UkOdig As New List(Of PictureBox)
Private picbarlistNum1to45_UkLastNotOdig As New List(Of PictureBox)
Private sub yourFormName_Load
Call AddPicBoxesInList_OdigNoOdig()
End sub
Private Sub AddPicBoxesInList_OdigNoOdig()
picbarlistNum1to45_UkOdig.Clear()
picbarlistNum1to45_UkLastNotOdig.Clear()
picbarlistNum1to45_UkOdig = Me.Controls(0).Controls.OfType(Of PictureBox)()
.Where(Function(pb) pb.Name.StartsWith("PicBarNum") And
pb.Name.EndsWith("_UkOdig")).ToList()
picbarlistNum1to45_UkLastNotOdig = Me.Controls(0).Controls.OfType(Of
PictureBox)().Where(Function(pb) pb.Name.StartsWith("PicBarNum") And
pb.Name.EndsWith("_NotLastOdig")).ToList()
End Sub
End Class

VB.net MVC understanding and binding

I am trying to seperate my code logic from my gui as in MVC principles, what I am trying to achieve is quite simple I believe
I have my Form1, which contains a textbox and button, once the button is clicked it loads a function in my controller class which adds a string to a database using entity and then should update the textbox with this name.
I thought what I would need to do is pass the original form through and then databind to the textbox object on the form, this is where I have come unstuck though, as my logic fails...
Public Class Form1
Private mf As New MainForm(Me)
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
mf.buttonClick()
End Sub
End Class
Public Class MainForm
Private Property a As Form
Public Sub New(ByVal s As Form)
a = s
End Sub
Function buttonClick() As Boolean
Dim context As TestDBEntities2 = New TestDBEntities2
Dim newCategory As tTable = New tTable
newCategory.Name = "Test1 " & Today.DayOfWeek
context.tTables.Add(newCategory)
context.SaveChanges()
Dim current As String = newCategory.Name
a.DataBindings.Add("text", "TextBox1", current)
Return True
End Function
End Class
and my error:
Cannot bind to the property or column Test1 6 on the DataSource.
Am I looking at this the right way? Or am I so far off that there is an obvious reason this doesn't work?
Any input would be appreciated! Whats the best way to pass data back to a source without returning it in as a result of a function?
You should consider changing your code a bit, so that it reflects more the MVC structure:
use Events to exchange data and indicate action triggers, instead of using the form object
normally the controller has knowledge of the form and not the other way around, so swap this in your project. This reflects also the first point
So a possible solution for a Windows Forms application could look like this:
The form that has one button and one text field, one event to signal the button click and one WriteOnly property to fill the TextBox from outside the form:
Public Class MainForm
Public Event GenerateAndShowEvent()
' allow text box filling
Public WriteOnly Property SetTextBoxContent()
Set(ByVal value)
generatedInputTextBox.Text = value
End Set
End Property
Private Sub generateAndShowButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles generateAndShowButton.Click
' forward message: inform the subscriber that something happened
RaiseEvent GenerateAndShowEvent()
End Sub
End Class
The controller class has knowledge of the form, creates it, binds (listens) to the form's button click event and makes the form ReadOnly for the world (exposes the form):
Public Class MainFormController
' the form that the controller manages
Private dialog As MainForm = Nothing
Public Sub New()
dialog = New MainForm()
' bind to the form button click event in order to generate the text and response
AddHandler dialog.GenerateAndShowEvent, AddressOf Me.GenerateAndShowText
End Sub
' allow the world to access readonly the form - used to start the application
Public ReadOnly Property GetMainForm()
Get
Return dialog
End Get
End Property
Private Sub GenerateAndShowText()
' create the text
Dim text As String = "Test test test"
' access the Database ...
' give the generated text to the UI = MainForm dialog!
dialog.SetTextBoxContent = text
End Sub
End Class
Now what left is to create first the controller, that creates the form and use the form to show it. This can be done like this:
Create an AppStarter module with a Main method:
Module AppStarter
Sub Main()
Application.EnableVisualStyles()
Application.SetCompatibleTextRenderingDefault(False)
' create the controller object
Dim controller As MainFormController = New MainFormController()
' use the public property to get the dialog
Application.Run(controller.GetMainForm)
End Sub
End Module
In your project settings uncheck the enable application framework setting and set the AppStarter module as the start point of your project:
Now you have a Windows Form Project using the MVC pattern.
If you still want to use DataBinding for the TextBox control, then create a Data Transfer Object or DTO that represents the fields you will transfer from your controller to the form:
Public Class DataContainer
Private t As String
Private i As Integer
Public Property Text() As String
Get
Return t
End Get
Set(ByVal value As String)
t = value
End Set
End Property
Public Property Id() As Integer
Get
Return i
End Get
Set(ByVal value As Integer)
i = value
End Set
End Property
End Class
Then add a BindingSource for your TextBox and configure it to use the DTObject:
Now bind the TextBox control to the DataBinding control:
What left is to add a public setter for the TextBox data binding control in the form:
Public Property TextBoxDataSource()
Get
Return TextBoxBindingSource.DataSource
End Get
Set(ByVal value)
TextBoxBindingSource.DataSource = value
End Set
End Property
and transfer the data from the controller:
Private Sub GenerateAndShowText()
' create the text
Dim text As String = "Test test test"
' access the Database ...
' give the generated text to the UI = MainForm dialog!
'dialog.SetTextBoxContent = text
Dim data As DataContainer = New DataContainer
data.Text = text
data.Id = 1 ' not used currently
dialog.TextBoxDataSource = data
End Sub
The binding can also be set programmatically - instead of doing this over the control property window, add the following code in the constructor of the form:
Public Sub New()
InitializeComponent()
' bind the TextBox control manually to the binding source
' first Text is the TextBox.Text property
' last Text is the DataContainer.Text property
generatedInputTextBox.DataBindings.Add(New Binding("Text", TextBoxBindingSource, "Text"))
End Sub
You seem to misunderstand the Add method.
The first argument is the name of the control's property to which you are binding. This should be "Text", not "text".
The second argument is an object that contains the data you want to bind. You have passed the name of the target control, rather than the source of the data. You are also binding to the form rather than the text box. So what you have said is that you want to bind the form's text property to data that can be extracted from the string "TextBox1".
The third argument says where to go to find the data. For example, if you passed a FileInfo object for the second argument, and you wanted to bind the file's path, you would pass the string "FullName", because that is the name of the property containing the data you want. So you have told the binding to look for a property on the string class called "Test1 6", which is why you have received the error message saying it can't be found.
I think what you want is
a.TextBox1.DataBindings.Add("Text", newCategory, "Name");

How do I reference a control array outside the sub that created it?

I'm writing a program that creates forms at runtime using this code,
Dim a As Integer = Screen.AllScreens.Length
Dim frm2(a) As Form
For b As Integer = 0 To a - 1
frm2(b) = new form
' ... set attributes to the form here
frm2(b).Show()
Next
My questions is how can I later from another sub access these forms? For instance if I wanted to draw graphics on these forms how would I access them? They're not available outside the subroutine that created them and you can't make a public array of controls in vb.net?
Just declare the frm2 array outside of your Sub method, at the class-level. For future reference, variables declared at the class-level are called fields. Fields are scoped to the class, so any method in the class can access them. For instance:
Public Class MyClass
Private frm2() As Form
Public Sub CreateForms()
Dim a As Integer = Screen.AllScreens.Length
ReDim frm2(a)
For b As Integer = 0 To a - 1
frm2(b) = New Form()
' ... set attributes to the form here
frm2(b).Show()
Next
End Sub
End Class
For what it's worth, I would recommend using a List(Of Form) rather than an array. That way you don't have to worry about re-sizing it, for instance:
Public Class MyClass
Private forms As New List(Of Form)()
Public Sub CreateForms()
For i As Integer = 0 To Screen.AllScreens.Length - 1
Dim frm2 As New Form()
' ... set attributes to the form here
frm2.Show()
forms.Add(frm2)
Next
End Sub
End Class
Rather than defining the forms in the sub itself, if you create them as form variables. Then you can access these from any sub within that form.
You should then be able to create whatever you need on the form, or preferably create a Public Sub on the form which can be called from elsewhere.