VBA Array Public Method cant be modified by components - arrays

I'm working with a rather complex problem in VBA and I decided to structure it in different classes.
One of the classes containd an array and I need to modify the component individualy outside the class.
The problem can de reduced to the definition of the class
Public a As Variant
Private Sub Class_Initialize()
ReDim a(5) As Double
' Set some value
a(1) = 20
End Sub
And a sub to access the data:
Sub Test()
Dim b As New DummyTest
b.a(4) = 7
Debug.Print (b.a(1))
Debug.Print (b.a(4))
End Sub
The result on the debug window is 20 0. I've checked this result with an inspection and confirm that the property a cannot be modified from the exterior of the class.
Funny part is that the property can be modified if replaced by other array, for example:
Sub Test2()
Dim b As New DummyTest
b.a = Array(1, 2, 3, 4, 5)
Debug.Print (b.a(1))
Debug.Print (b.a(4))
End Sub
Behaves as expected. I'm confused as element access is allowed inside the class context and in any other situation, but in this specific case it doesn't wotk. There is no error msg, it simply refuses to change the content of the array.
I'll find work around solutions but missing this way to access data is being a really PITA.
Hope you can help me with this issue.
Thank you in advance!

Your "property" is breaking encapsulation, which pretty much defeats its entire purpose.
Public a As Variant
From a public interface standpoint, this is a read-write property (if you added a new class module with Implements DummyTest you'd have to add Property Get and Property Let members for it). From the class' standpoint, it's a public instance field.
When a class encapsulates an array, a collection, a dictionary, or any other data structure, the last thing you want is for that data structure to be publicly exposed, with anyone anywhere being able to just overwrite the class' entire internal state as it pleases - you're not getting any of the advantages of classes that way.
First step to proper encapsulation is to make the instance field Private. Next step is to expose actual Property accessors. You want the array data to be writable? Expose an indexed Property Let member for it.
Private a As Variant
Private Sub Class_Initialize()
ReDim a(0 To 10)
End Sub
Public Property Get Value(ByVal index As Long) As Variant
Value = a(index)
End Property
Public Property Let Value(ByVal index As Long, ByVal newvalue As Variant)
a(index) = newvalue
End Property
This code behaves exactly as advertised:
Public Sub Test()
Dim b As DummyTest
Set b = New DummyTest
b.Value(4) = 7
Debug.Print b.Value(1)
Debug.Print b.Value(4)
End Sub
Note that because the array is encapsulated, that client code is absolutely unable to re-assign the array itself: the array is abstracted away and only accessible through the means exposed by the class.

Reference variable in VBA must be declared, so that we know what type of object we are referring to
i.e. Dim b As DummyTest ' No new required in this step
Before using a declared reference to store information create an instance of the Class.
i.e. Set b = New DummyTest
This might seem confusing but is actually very logical. Because b is a Class the Dim statement sets up b to hold the address of a DummyTest object. At the time of the Dim statement no object has been created (no memory allocated) so the address of b is nothing (no value or null). The set statement tells VBA to create an instance of your DummyTest class (allocate the memory it needs) and puts the address of the instance (memory) into variable b. Then because VBA knows that b points to an instance of a class you never see the address of b when you use it, just the data to which b is pointing. You can get the the address b is holding using the function Objptr but this is only something you would do in very special/specific circumstances

Related

How do I redefine the class of a variable programmatically?

Ok, so I have 2 classes, Note and HeldNote. HeldNote inherits from Note and adds only 2 variables, plus a different way of setting visual dimensions. I have an array of an undefined amount of items, with each item defined as a member of class Note.
Occasionally during my program, I want one of these items in the array to be a HeldNote rather than a Note, and I do not know which one until the program is running.
VB.Net seems fine with me declaring array(i) = New HeldNote but then wont let me access the variables that HeldNote has but Note does not. Using Breaks in the program leads me to believe that array(i) is not actually being redefined as a HeldNote rather than a Note.
So, my question is, how can I make array(i) a HeldNote, rather than a Note?
I'm using VB.net 2008.
Edit: I asked my Teacher about this problem and he said he didn't know and told me to look it up. I couldn't anything online (had been looking since before i asked him) so I've come here for help.
The Array of Note (better use a List(Of Note) if you are unsure about the size) can store both Note and HeldNote because the latter derives from the first.
But, when you store an HeldNote in the array it is not automatically transformed in a Note, it is still an HeldNote, what you need is to try casting it back to an HeldNote
For example
Sub Main
Dim X(2) As Note
X(0) = New Note() WIth {.Note = "A Message"}
X(1) = New HeldNote() With {.Note = "Held Message", .Author = "Steve"}
Dim h As HeldNote
h = TryCast(X(0), HeldNote)
If h Is Nothing Then
Console.WriteLine("X(0) is NOT an HeldNote")
Else
Console.WriteLine(h.Author)
End If
h = TryCast(X(1), HeldNote)
If h Is Nothing Then
Console.WriteLine("X(1) is NOT an HeldNote")
Else
Console.WriteLine(h.Author)
End If
End Sub
Public Class Note
Public Note As String
End Class
Public Class HeldNote
Inherits Note
Public Author As String
End Class
In thisway you still have your HeldNote without using an array of Object that is something to avoid because you loose all the strong typing allowed by a specific Note array (In an Object array you could store anything. A Button or a String makes no difference)
A few suggestions:
1) Change your design and have just one class rather than one that inherits from another.
2) Declare your array as type Object. Each element of an Object array can be of a different type. This way you can assign each element's type at runtime.
Dim notearray As Object() = New Object() {New Note(), New HeldNote(), New HeldNote(), New Note()}
This is not a code issue, it's almost a OO question.
"I have an array of an undefined amount of items, with each item defined as a member of class Note."
Then all of them are "Note". You can create class instances so:
Note theNote = new HeldNote()
but you can't "downgrade" a instance on execution declared so:
Note theNote = new Note()".
If you need "randomly" a type change, you can create a ListOf(HeldNote).
Then, you can decide the real type so:
Dim aList as new ListOf(HeldNote)
aList.add(new HeldNote())
aList.add(new HeldNote())
aList.add(new HeldNote())
Dim oNote as Note
oNote = Ctype(aList(0),Note)
Dim oNote as HeldNote
oNote = aList(1)
...

arrayVariable() As Object vs. arrayVariable As Object() – not the same in Property declaration?

Until today, I thought that the following two notations are the same (edit: Dim was replaced by Property)
Property arrayVariable() As Object
Property arrayVariable As Object()
Today I found that former one throws error Option Strict On disallows late binding. while the latter compiles OK in expression dictionary1.TryGetValue(CStr(arrayVariable(0)), result).
Please what is the difference between them?
I would always use the second notation if it also allowed to specify the array dimensions. It doesn't, so I stuck with the first form (less clean one, because part of type specification - parenthesis - are before As) in order to be consistent across declarations. And now I see that even the first one isn't universal...
It really looks like a weak point of Visual Basic that two forms exist for one thing and their usage is not straightforward but has catches like this.
Full source code reproducing the issue:
Public Class Class1
Dim _A1() As Object
Dim _A2() As Object
ReadOnly Property A1() As Object ' 1st form of declaration
Get
Return _A1
End Get
End Property
ReadOnly Property A2 As Object() ' 2nd form of declaration
Get
Return _A2
End Get
End Property
End Class
Sub Main()
Dim c1 As New Class1
Dim d1 As New Dictionary(Of String, String)
Dim value As String = ""
d1.TryGetValue(CStr(c1.A1(0)), value) '<- Option Strict On disallows late binding.
d1.TryGetValue(CStr(c1.A2(0)), value) '<- No error here.
End Sub
The problem is that they are Properties so the () is ignored as an Array specifier and thinks of it as an empty parameter collection. Look at how the compiler see them - even the compiler thinks you had an Object and not a Object() and so in your example the A1(0) is an index of an object which is not defined so it thinks you have done some late binding and made it accept an array.
If you are not using a Property and an Object type either declaration is valid.
Dim data() As String
Same as
Dim data As String()
If you highlight either variable the intellisence shows you:
Dim data() As String
Here's the minimum amount of code required to replicate your issue:
Dim y As Object
Dim x = y(0)
This has nothing to do with the declaration of arrays. You're just trying to convert an Object to an array which option strict doesn't allow.
Additional notes:
(not sure why this has been downvoted)
Changing Object to Integer reveals the first form mentioned in question is not taken as declaration.
Public Class Class1
Dim _A1 As Integer()
Dim _A2 As Integer()
ReadOnly Property A1() As Integer
Get
Return _A1
' error at above line: Value of type '1-dimensional array of Integer' cannot
' be converted to 'Integer'.
End Get
End Property
ReadOnly Property A2 As Integer()
Get
Return _A2
End Get
End Property
End Class
Proper way is
ReadOnly Property A1() As Integer()

Erase an array - is it necessary?

Please have a look at the code below:
Imports System.Data.Common
Imports System.Data.SqlClient
Imports System.Data.OracleClient
Public Class clsParameterValues
Implements IDisposable
Private paramValues(0) As DbParameter
Public Function AssignParameterValues(ByVal strParameterName As String, ByVal strParameterValue As String, ByVal intDatabaseType As Integer) As Integer
Dim intArrayBound As Integer
intArrayBound = UBound(paramValues)
If intArrayBound > 0 Then
ReDim paramValues(intArrayBound)
End If
If intDatabaseType = 1 Then
paramValues(intArrayBound) = New SqlParameter(strParameterName, strParameterValue)
ElseIf intDatabaseType = 2 Then
paramValues(intArrayBound) = New OracleParameter(strParameterName, strParameterValue)
'paramValues(intArrayBound) = New OracleParameter(":" & strParameterName, OracleType.Int32)
'paramValues(intArrayBound).Value = strParameterValue
End If
Return intArrayBound
End Function
Public Function getParameterValues() As DbParameter()
Return paramValues
End Function
Public Sub Dispose() Implements IDisposable.Dispose
Erase paramValues
paramValues = Nothing
End Sub
End Class
The webpage function looks like this:
Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Dim objParameterValues As New clsParameterValues
Using objParameterValues
'Use the objParameterValues class here.
End Using
objParameterValues=nothing
End Using
End Sub
I am using IDisposable.Dispose to erase the array before setting it to Nothing. I believe this is bad practice because the Array class does not implement IDisposable. Is it even necessary to erase an array and set it to Nothing? (Does the garbage collector deal with this?)
Public Sub Dispose() Implements IDisposable.Dispose
Erase paramValues
paramValues = Nothing
End Sub
The Erase statement dates from old versions of Basic, the kind where manual memory management was useful. No more, memory management is automatic in .NET. It is still supported for compatibility reasons. All it does is set the array reference to Nothing. So your code is equivalent to:
Public Sub Dispose() Implements IDisposable.Dispose
paramValues = Nothing
paramValues = Nothing
End Sub
So no point to it. You should never implement IDisposible to set a variable to Nothing, that's not the interface's contract. A disposed object may never be used again. So no point in setting the array reference to null since that doesn't actually do anything to the real array object on the garbage collected heap. I cannot otherwise see a scenario where you would want to help, any clsParameterValues object should have a limited lifetime. It just isn't useful anymore when you null the array reference.
Just remove the IDisposable implementation.
Is it even necessary to erase an array and set it to nothing?
No. The GC does that in its own time. In fact, I’m not even sure whether Erase actually frees money or whether it simply points the array somewhere else.It just sets the variable to Nothing. Erase probably exists out of compatibility with VB6. C# for instance doesn’t have it (and neither does it have ReDim, although Array.Resize exists of course, albeit with slightly different semantics).
On the other hand, if your array contained IDisposable objects (which it doesn’t) you should dispose those in your Dispose method by iterating over the array and disposing them in turn. But again, the array itself doesn’t need to be erased.
As a general remark, your code reads very much like VB6. Change that. Don’t declare variables without initialising them. Don’t use UBound etc (there’s the .Length property instead). If you find yourself using ReDim very often use a System.Collections.Generic.List instead of an array.

How to have a global Dictionary in VB.NET/WPF application to save data from different windows?

I am new to VB.NET and WPF.
I am building a "Questionnaire" app. Users will be presented sequentially with different questions/tasks (windows). After they respond on each question/task and press a "submit" button a new window will open with a new question/task, and previous window will close. After each question, when the button is pressed, I need to store data to some global object. After all questions are answered the data of this object should be written out to the output file.
I figured out that Dictionary will be the best to store the results after each window.
I am not sure how, where to create this global Dictionary and how to access it. Should I use View Model? If yes, can you give an example? Or, should it be just a simple class with shared property? (something like this)
EDIT 2: I tried many different ways recommended online
GlobalModule:
Module GlobalModule
Public Foo As String
End Module
GlobalVariables:
Public Class GlobalVariables
Public Shared UserName As String = "Tim Johnson"
Public Shared UserAge As Integer = 39
End Class
Global properties:
Public Class Globals
Public Shared Property One As String
Get
Return TryCast(Application.Current.Properties("One"), String)
End Get
Set(ByVal value As String)
Application.Current.Properties("One") = value
End Set
End Property
Public Shared Property Two As Integer
Get
Return Convert.ToInt32(Application.Current.Properties("Two"))
End Get
Set(ByVal value As Integer)
Application.Current.Properties("Two") = value
End Set
End Property
End Class
Here is where I save the data to global variables/properties in the first window. I need to store data in this subroutine before closing an old window and opening a new window. I use MessageBox just for testing.
Private Sub btnEnter_Click(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles btnEnter.Click
Dim instructionWindow As InstructionsWindow
instructionWindow = New InstructionsWindow()
Application.Current.Properties("number") = textBoxValue.Text
Globals.One = "2"
Globals.Two = 3
MessageBox.Show("GlobalVariables: UserName=" & GlobalVariables.UserName & " UserAge=" & GlobalVariables.UserAge)
GlobalVariables.UserName = "Viktor"
GlobalVariables.UserAge = 34
GlobalModule.Foo = "Test Foo"
'testing if it saved tha value
'MessageBox.Show(Application.Current.Properties("number"))
Application.Current.MainWindow.Close()
instructionWindow.ShowDialog()
End Sub
Next subroutine is where I am trying to retrieve the value from global Properties/variables in the second window, but message boxes come out empty. There might also the case that I am assigning values in a wrong way, or not reading them in a right way (casting?) :
Private Sub FlowDocReader_Initialized(ByVal sender As Object, ByVal e As System.EventArgs) Handles FlowDocReader.Initialized
' Get a reference to the Application base class instance.
Dim currentApplication As Application = Application.Current
MessageBox.Show(currentApplication.Properties("number"))
MessageBox.Show("One = " & Globals.One & " Two = " & Globals.Two)
MessageBox.Show("GlobalVariables: UserName=" & GlobalVariables.UserName & " UserAge=" & GlobalVariables.UserAge)
MessageBox.Show("GlobalModule.Foo = " & GlobalModule.Foo)
Dim filename As String = My.Computer.FileSystem.CurrentDirectory & "\instructions.txt"
Dim paragraph As Paragraph = New Paragraph()
paragraph.Inlines.Add(System.IO.File.ReadAllText(filename))
Dim document As FlowDocument = New FlowDocument(paragraph)
FlowDocReader.Document = document
End Sub
Thanks.
You can make public Dictionary property for form and put your dictionry to this property or make constructor with Dictionary argument.
You already have this dictionary Application.Properties
Look here, please.
First, you can define a dictionary (list of lists) as follows at the beginning of a form or in a module
Dim dic As New Dictionary(Of String, List(Of String))
As the user completes questions on a form, write the partucular form number and query results to a single record in the dic before going to the next form (place this code into the "Next" button):
'Assume q1response=3, q2response=4,..., qpresponse="text", etc.
Dim myValues As New List(Of String)
myValues.Add(formname)
myValues.Add(q1response)
myValues.Add(q2response)
.
.
myValues.Add(qpresponse)
dic.Add(username, myValues)
When a user is done, there will be multiple records in the dictionary, each of which starts with their name and is followed by question responses. You can loop through multiple dictionary records, where each record is for a user using the following:
For Each DictionaryEntry In dic 'this loops through dic entries
Dim str As List(Of String) = DictionaryEntry.Value
'here you can do whatever you want with results while you read through dic records
'username will be = str(0)
'formname will be str(1)
'q1 response on "formname" will be str(2)
'q2 response on "formname" will be str(3)
'q3 response on "formname" will be str(4)
...
Next
The trick is that there will be multiple dictionary records with results for one user, where record one can have results like "John Doe,page1,q1,q2,q3" and record 2 will be "John Doe,page2,q4,q5,q6." Specifically, the "str" in the above loop will be an array of string data containing all the items within each dictionary record, that is, in str(0), str(1), str(2),... This is the information you need to work with or move, save, analyze, etc.
You can always put all the code I provided in a class (which will be independent of any form) and dimension the sic is a Sub New in this class, with the updating .Add values lines in their own sub in this same class). Then just Dim Updater As New MyNewClassName. Call the Updater in each continue button using Call Updater.SubNameWithAddValues(q1,q2,...qp). It won't matter where you are in your program since you using a specific class. The one thing I noticed with my code is that you can only use the line that adds the "key" or the username once, so use it after the last query -so put it in a Sub Finished in your new class and call as Call Updater.Finished(username,q30,q31,last)

Type mismatch on an Array assignment in ASP

Its been a while since I've worked with ASP, but I'm getting a Type mismatch error on what seems to be a simple assignment statement. Can anyone shed some light on why this might be happening.
This works, but when I try to foreach an unassigned Questions block I get an Object not a collection error
Class Survey
public ID
public Title
public Questions
End Class
Sub Test()
Dim oSurvey
Set oSurvey = new Survey
Dim aQuestions(2)
Set aQuestions(0) = new Question
' Other aQuestions assignments
oSurvey.Questions = aQuestions
End Sub
Alternately If I setup questions as a dynamic array then the assignment fails with a type mismatch error.
Class Survey
public ID
public Title
public Questions()
End Class
Sub Test()
Dim oSurvey
Set oSurvey = new Survey
Dim aQuestions(2)
Set aQuestions(0) = new Question
' Other aQuestions assignments
' Throws a Type mismatch error here
oSurvey.Questions = aQuestions
End Sub
Any thoughts?
To answer your question as to what is actually going on.
when I try to foreach an unassigned
Questions block I get an Object not a
collection error
For Each enumerates a set of variants from the source variable, it does this by acquiring an IEnumVARIANT. If the source variable holds an object it is expected to have an implementation of this interface. If it is an array VBScript creates an implementation dynamically and it can only do this if the array has been dimensioned. Anything else in the source variable (such as Empty in this case) will result in an error.
then the assignment fails with a type mismatch error.
The left hand side of an assignment operation must always be a variant. Hence its not possible copy the contents of one dynamic array to another via a simple assignment.
Your first approach is reasonably sound but you need a way to represent an empty array without crashing out a For Each. You can use this little trick:-
Function EmptyArray
EmptyArray = Split("", " ")
End Function
Class Survey
public ID
public Title
public Questions
Private Sub Class_Initialize
Questions = EmptyArray
End Sub
End Class
Now if you try to For Each the Questions before it has been assigned a real array the for each will do nothing as expected. Also if you use UBound(Questions) + 1 to get the count of questions that will still be accurate since UBound(EmptyArray) is -1.
If I try to paste the same code into VBA (excel or word), it doesn't compile.
It shows error on line public Questions() saying
---------------------------
Microsoft Visual Basic
---------------------------
Compile error:
Constants, fixed-length strings, arrays, user-defined types and Declare statements not allowed as Public members of object modules
---------------------------
OK Help
---------------------------
In the first example, you haven't defined it as an array (it is a variant & hence can be assigned any value).
Update: I tested this in ASP.NET not realizing the question was about classic ASP. I've modified the code below to work with classic ASP, though I haven't been able to test it yet:
Class Question
...
End Class
Class Survey
Public ID
Public Title
Public Questions As Question()
End Class
Sub Test()
Dim oSurvey As New Survey
Dim aQuestions(0 To 2) As Question
Set aQuestions(0) = New Question
...
Set oSurvey.Questions = aQuestions
End Sub
In your first example, Survey.Questions isn't a collection; in your second, it is an array of Type Variant.
So I ended up sticking with the Array declaration. Additionally, When I tried to ReDim the array like so I got an error.
ReDim oSurvey.Questions(2)
So I created a sub routine to ReDim the Array, and this worked.
Class Survey
public ID
public Title
public Questions()
sub ReDimQuestions(count)
ReDim Questions(count)
end sub
End Class
Sub Test()
Dim oSurvey
Set oSurvey = new Survey
oSurvey.ReDimQuestions 2
Set oSurvey.Questions(0) = new Question
' Other aQuestions assignments
End Sub

Resources