[Edited the code]
Sorry for newbie question. I was trying to test nested classes in VBA for Excel but got error message. Can anyone help me understand why? Thank you!
Inside class:
' CLASS MODULE - cInside
' Member variables
Private m_Value As Integer
' Properties
Property Get Value() As Integer
Value = m_Value
End Property
Property Let Value(i As Integer)
m_Value = i
End Property
' Methods
Sub init()
m_Value = 0
End Sub
Sub Inc()
m_Value = m_Value + 1
End Sub
Outside class:
' CLASS MODULE - cOutside
' Member variables
Private m_Num1 As New cInside
Private m_Num2 As New cInside
' Properties
Property Get Num1() As cInside
Num1 = m_Num1.Value
End Property
Property Get Num2() As cInside
Num2 = m_Num2.Value
End Property
Property Set Num1(i As cInside)
Set m_Num1 = i
End Property
Property Set Num2(i As cInside)
Set m_Num2 = i
End Property
Main program:
Sub Main()
Dim o As New cOutside
Dim i As New cInside
i.Value = 9
i.Inc
Debug.Print i.Value '<-- this works
Set o.Num1 = i
o.Num1.Inc '<-- object variable or with block variable not set
Debug.Print (o.Num1.Value)
End Sub
Thank you again!
n.Num1 returns an Integer, which isn't an instance of cInside, and therefore doesn't have an Inc member, which makes it an invalid qualifier for the compiler to complain about ;-)
Your cOutside class needs to expose it somehow:
Property Get ComposedObject1() As cInside
Set ComposedObject1 = m_Num1
End Property
Then the calling code could do this:
n.ComposedObject1.Inc
I called that "composed", because what you're doing isn't "nesting", but "composition". VBA doesn't supported nested classes, which would be a class module defined inside another class module, like so:
Class Outside
'... members...
Class Inside
'...members...
End Class
End Class
VB.NET can do that, but in VBA 1 class module can only define 1 class.
I have a class function I want to feed multiple parameters but I am having trouble as I am new to using classes in VB.
This is what I currently have working with a single parameter
Set objLoc = new Location
objLoc.getLoc = strOffice
Need it to work something like this
Set objLoc = new Location
objLoc.getLoc = (strOffice, strDep)
Location Class
Class Location
Private strPhone, strFax, strStreet, strCSZ
Public Property Let getLoc(strOffice)
if LCase(strOffice) = LCase("foo") then
strPhone = "999-999-9999"
strFax = "888-888-8888 fax"
strStreet = "..."
strCSZ = "..."
ElseIf LCase(strOffice) = LCase("bar") then
strPhone = "777-777-7777"
strFax = "555-555-5555 fax"
strStreet = "..."
strCSZ = "..."
Else
End If
End Property
Public Property Get Street
Street = strStreet
End Property
Public Property Get CSZ
CSZ = strCSZ
End Property
Public Property Get Fax
Fax = strFax
End Property
Public Property Get Phone
Phone = strPhone
End Property
End Class
You can't SET a property with more than 1 value, you need a Subroutine to set both OR you need to add a new property for the other value.
So, option #1 (add a sub): (not a vbScript expert but have years of VB.NET and VB:
Public Sub SetOfficeAndDept(office,dept)
... code ...
End Sub
option #2, (add a new property):
Public Property Let Dept(d)
Dept = d
End Property
option #3, (add a new constructor)
Public Sub New(office, dept)
..set your vars...
End Sub
to use it:
Set l = new Location(ofc,dept)
I'm using List(Of T) to contains my database field (invoice_id and item_id) and want to display it in DataGridView. First, I declare each of them as a class and then add them to my list then display it in DataGridView but when I compile it, the program is not responding.
My guess is that my database source is too large, because when I change the source (database field), it worked nicely. So how do I solve this List(Of T) capacity issues?
This is my code:
Sub view()
Dim msql2 As String
msql2 = "select invoice_id, item_id from detail"
Dim arayD As New List(Of INVOICE)
CMD2 = New MySqlCommand(msql2, conn.konek)
Try
Dim res = CMD2.ExecuteReader()
Dim INVO As INVOICE = Nothing
While res.Read()
INVO = New INVOICE
With INVO
.invoice_id = hasil2.GetString("invoice_id")
.item_id = hasil2.GetString("item_id")
End With
arayD.Add(INVO)
End While
dgv.DataSource = arayD
Catch ex As Exception
MessageBox.Show("ERROR")
End Try
End Sub
Public Class INVOICE
Private _kodeF As Integer
Public Property invoice_id() As Integer
Get
Return _kodeF
End Get
Set(ByVal value As Integer)
_kodeF = value
End Set
End Property
Private _kodeBrg As String
Public Property item_id() As String
Get
Return _kodeBrg
End Get
Set(ByVal value As String)
_kodeBrg = value
End Set
End Property
End Class
if you want a fast solution then add the following line inside your While Loop :
While res.Read()
Application.DoEvents()
...
End While
Or use a BackgroundWorker as the following:
Private WithEvents bgWorker As New System.ComponentModel.BackgroundWorker
Private arayD As New List(Of INVOICE)
Sub view()
bgWorker.RunWorkerAsync()
End Sub
Private Sub bgWorker_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles bgWorker.DoWork
Dim dReader As DataReader
Using YourConnection
Using YourCommand
YourConnection.Open()
dReader = YourCommand.ExecuteReader()
If dReader.HasRows Then
While dReader.Read
arayD.Add(New INVOICE With {
.invoice_id = hasil2.GetString("invoice_id"),
.item_id = hasil2.GetString("item_id")
}
)
End While
End If
End Using
End Using
End Sub
Private Sub bgWorker_RunWorkerCompleted(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles bgWorker.RunWorkerCompleted
MsgBox("Ready to go")
dgv.DataSource = arayD
End Sub
Also you can get the count of the result and use it with a progressbar.
So I was wondering, how can I return multiple values from a function, sub or type in VBA?
I've got this main sub which is supposed to collect data from several functions, but a function can only return one value it seems. So how can I return multiple ones to a sub?
You might want want to rethink the structure of you application, if you really, really want one method to return multiple values.
Either break things apart, so distinct methods return distinct values, or figure out a logical grouping and build an object to hold that data that can in turn be returned.
' this is the VB6/VBA equivalent of a struct
' data, no methods
Private Type settings
root As String
path As String
name_first As String
name_last As String
overwrite_prompt As Boolean
End Type
Public Sub Main()
Dim mySettings As settings
mySettings = getSettings()
End Sub
' if you want this to be public, you're better off with a class instead of a User-Defined-Type (UDT)
Private Function getSettings() As settings
Dim sets As settings
With sets ' retrieve values here
.root = "foo"
.path = "bar"
.name_first = "Don"
.name_last = "Knuth"
.overwrite_prompt = False
End With
' return a single struct, vb6/vba-style
getSettings = sets
End Function
You could try returning a VBA Collection.
As long as you dealing with pair values, like "Version=1.31", you could store the identifier as a key ("Version") and the actual value (1.31) as the item itself.
Dim c As New Collection
Dim item as Variant
Dim key as String
key = "Version"
item = 1.31
c.Add item, key
'Then return c
Accessing the values after that it's a breeze:
c.Item("Version") 'Returns 1.31
or
c("Version") '.Item is the default member
Does it make sense?
Ideas :
Use pass by reference (ByRef)
Build a User Defined Type to hold the stuff you want to return, and return that.
Similar to 2 - build a class to represent the information returned, and return objects of that class...
You can also use a variant array as the return result to return a sequence of arbitrary values:
Function f(i As Integer, s As String) As Variant()
f = Array(i + 1, "ate my " + s, Array(1#, 2#, 3#))
End Function
Sub test()
result = f(2, "hat")
i1 = result(0)
s1 = result(1)
a1 = result(2)
End Sub
Ugly and bug prone because your caller needs to know what's being returned to use the result, but occasionally useful nonetheless.
A function returns one value, but it can "output" any number of values. A sample code:
Function Test (ByVal Input1 As Integer, ByVal Input2 As Integer, _
ByRef Output1 As Integer, ByRef Output2 As Integer) As Integer
Output1 = Input1 + Input2
Output2 = Input1 - Input2
Test = Output1 + Output2
End Function
Sub Test2()
Dim Ret As Integer, Input1 As Integer, Input2 As Integer, _
Output1 As integer, Output2 As Integer
Input1 = 1
Input2 = 2
Ret = Test(Input1, Input2, Output1, Output2)
Sheet1.Range("A1") = Ret ' 2
Sheet1.Range("A2") = Output1 ' 3
Sheet1.Range("A3") = Output2 '-1
End Sub
you can return 2 or more values to a function in VBA or any other visual basic stuff but you need to use the pointer method called Byref. See my example below. I will make a function to add and subtract 2 values say 5,6
sub Macro1
' now you call the function this way
dim o1 as integer, o2 as integer
AddSubtract 5, 6, o1, o2
msgbox o2
msgbox o1
end sub
function AddSubtract(a as integer, b as integer, ByRef sum as integer, ByRef dif as integer)
sum = a + b
dif = b - 1
end function
Not elegant, but if you don't use your method overlappingly you can also use global variables, defined by the Public statement at the beginning of your code, before the Subs.
You have to be cautious though, once you change a public value, it will be held throughout your code in all Subs and Functions.
I always approach returning more than one result from a function by always returning an ArrayList. By using an ArrayList I can return only one item, consisting of many multiple values, mixing between Strings and Integers.
Once I have the ArrayList returned in my main sub, I simply use ArrayList.Item(i).ToString where i is the index of the value I want to return from the ArrayList
An example:
Public Function Set_Database_Path()
Dim Result As ArrayList = New ArrayList
Dim fd As OpenFileDialog = New OpenFileDialog()
fd.Title = "Open File Dialog"
fd.InitialDirectory = "C:\"
fd.RestoreDirectory = True
fd.Filter = "All files (*.*)|*.*|All files (*.*)|*.*"
fd.FilterIndex = 2
fd.Multiselect = False
If fd.ShowDialog() = DialogResult.OK Then
Dim Database_Location = Path.GetFullPath(fd.FileName)
Dim Database_Connection_Var = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=""" & Database_Location & """"
Result.Add(Database_Connection_Var)
Result.Add(Database_Location)
Return (Result)
Else
Return (Nothing)
End If
End Function
And then call the Function like this:
Private Sub Main_Load()
Dim PathArray As ArrayList
PathArray = Set_Database_Path()
My.Settings.Database_Connection_String = PathArray.Item(0).ToString
My.Settings.FilePath = PathArray.Item(1).ToString
My.Settings.Save()
End Sub
you could connect all the data you need from the file to a single string, and in the excel sheet seperate it with text to column.
here is an example i did for same issue, enjoy:
Sub CP()
Dim ToolFile As String
Cells(3, 2).Select
For i = 0 To 5
r = ActiveCell.Row
ToolFile = Cells(r, 7).Value
On Error Resume Next
ActiveCell.Value = CP_getdatta(ToolFile)
'seperate data by "-"
Selection.TextToColumns Destination:=Range("C3"), DataType:=xlDelimited, _
TextQualifier:=xlDoubleQuote, ConsecutiveDelimiter:=False, Tab:=True, _
Semicolon:=False, Comma:=False, Space:=False, Other:=True, OtherChar _
:="-", FieldInfo:=Array(Array(1, 1), Array(2, 1)), TrailingMinusNumbers:=True
Cells(r + 1, 2).Select
Next
End Sub
Function CP_getdatta(ToolFile As String) As String
Workbooks.Open Filename:=ToolFile, UpdateLinks:=False, ReadOnly:=True
Range("A56000").Select
Selection.End(xlUp).Select
x = CStr(ActiveCell.Value)
ActiveCell.Offset(0, 20).Select
Selection.End(xlToLeft).Select
While IsNumeric(ActiveCell.Value) = False
ActiveCell.Offset(0, -1).Select
Wend
' combine data to 1 string
CP_getdatta = CStr(x & "-" & ActiveCell.Value)
ActiveWindow.Close False
End Function
I have an IEnumerable(of Employee) with a ParentID/ChildID relationship with itself that I can databind to a TreeView and it populates the hierarchy perfectly. However, I want to be able to manually loop through all the records and create all the nodes programmatically so that I can change the attributes for each node based on the data for that given item/none.
Is there a tutorial out there that explains how to do this? I've seen many that use datasets and datatables but none that show how to do it in Linq to SQL (IEnumerable)
UPDATE:
Here's how I used to do it with a DataSet - I just can't seem to find how to do the same with IEnumerable.
Private Sub GenerateTreeView()
Dim ds As New DataSet()
Dim tasktree As New Task(_taskID)
Dim dt As DataTable = tasktree.GetTaskTree()
ds.Tables.Add(dt)
ds.Relations.Add("NodeRelation", dt.Columns("TaskID"), dt.Columns("ParentID"))
Dim dbRow As DataRow
For Each dbRow In dt.Rows
If dbRow("TaskID") = _taskID Then
Dim node As RadTreeNode = CreateNode(dbRow("Subject").ToString(), False, dbRow("TaskID").ToString())
RadTree1.Nodes.Add(node)
RecursivelyPopulate(dbRow, node)
End If
Next dbRow
End Sub
Private Sub RecursivelyPopulate(ByVal dbRow As DataRow, ByVal node As RadTreeNode)
Dim childRow As DataRow
Dim StrikeThrough As String = ""
Dim ExpandNode As Boolean = True
For Each childRow In dbRow.GetChildRows("NodeRelation")
Select Case childRow("StatusTypeID")
Case 2
StrikeThrough = "ActiveTask"
Case 3
StrikeThrough = "CompletedTask"
ExpandNode = False
Case 4, 5
StrikeThrough = "ClosedTask"
ExpandNode = False
Case Else
StrikeThrough = "InactiveTask"
ExpandNode = False
End Select
Dim childNode As RadTreeNode = CreateNode("<span class=""" & StrikeThrough & """>" & childRow("Subject").ToString() & "</span>", ExpandNode, childRow("TaskID").ToString())
node.Nodes.Add(childNode)
RecursivelyPopulate(childRow, childNode)
ExpandNode = True
Next childRow
End Sub
Private Function CreateNode(ByVal [text] As String, ByVal expanded As Boolean, ByVal id As String) As RadTreeNode
Dim node As New RadTreeNode([text])
node.Expanded = expanded
Return node
End Function
If you just need a way of enumerating the tree you can implement this as a generator, it might look strange, you're probably better of with a user defined enumerator but it's essentially the same thing.
public interface IGetChildItems<TEntity>
{
IEnumerable<TEntity> GetChildItems();
}
public static IEnumerable<TEntity> Flatten<TEntity>(TEntity root)
where TEntity : IGetChildItems<TEntity>
{
var stack = new Stack<TEntity>();
stack.Push(root);
while (stack.Count > 0)
{
var item = stack.Pop();
foreach (var child in item.GetChildItems())
{
stack.Push(child);
}
yield return item;
}
}
The type constraint where TEntity : IGetChildItems is just to signify that you need to abstract how to descend the hierarchy. Without the above code would not compile.
This will enumerate the tree in a breadth first fashion, it will yield the parent element first then it's children, and then the children of those children. You can easily customize the above code to achieve a different behavior.
Edit:
The yield return stuff tells the compiler that it should return a value then continue. yield is a context keyword and it's only allowed inside an iterative statement. A generator is a simple way of writing a IEnumerable data source. The compiler will build a state machine from this code and create an enumerable anonymous class. Apparently the yield keyword does not exist in VB.NET. But you can still write a class which does this.
Imports System
Imports System.Collections
Imports System.Collections.Generic
Public Class HierarchyEnumerator(Of TEntity As IGetChildItems(Of TEntity))
Implements IEnumerator(Of TEntity), IDisposable, IEnumerator
Public Sub New(ByVal root As TEntity)
Me.stack = New Stack(Of TEntity)
Me.stack.Push(root)
End Sub
Public Sub Dispose()
End Sub
Public Function MoveNext() As Boolean
Do While (Me.stack.Count > 0)
Dim item As TEntity = Me.stack.Pop
Dim child As TEntity
For Each child In item.GetChildItems
Me.stack.Push(child)
Next
Me.current = item
Return True
Loop
Return False
End Function
Public Sub Reset()
Throw New NotSupportedException
End Sub
Public ReadOnly Property Current() As TEntity
Get
Return Me.current
End Get
End Property
Private ReadOnly Property System.Collections.IEnumerator.Current As Object
Get
Return Me.Current
End Get
End Property
Private current As TEntity
Private stack As Stack(Of TEntity)
End Class