Working with Classes in vb6 with user selected fields - class

I found out how to create the properties of the class, like this:
private m_Name as string
public property get Name() as string
Name = m_Name
end sub
public property let Name(sval as string)
m_name = sval
end sub
The user will create a document and choose some fields (Name, Birthday, Phone....) inside this document, as I can't know exactly which fields will be chosen by the user, I thought create a class would be the best option.
After I create the class like above, how may I make a loop through this class to check which fields has been chosen by the user?
Any better option for my situation, please, let me know...

If I understood you correctly, you want to know which fields (out of a number of existing fields) user used/initialized?
I see several ways to do it:
1) If your variables do not have default values and must have a non-empty/non-zero values, then you can simply check if a variable is empty or zero. If it is, it hasn't been initialized.
If m_name = "" Then MsgBox "Variable is not initialized"
2) for each field you have, create a boolean fieldName_Initialized so for each field, you would have something like this:
private m_Name as string
private m_name_Initialized as Boolean
public property get Name() as string
Name = m_Name
end sub
public property let Name(sval as string)
m_name = sval
m_name_Initialized = True
end sub
3) you could have a list and add variable names to the list as they become initialized:
Make sure to add Microsoft Scripting Runtime to your References for Dictionary to work.
Dim initialized As Dictionary
Set initialized = New Dictionary
private m_Name as string
private m_name_Initialized as Boolean
public property get Name() as string
Name = m_Name
end sub
public property let Name(sval as string)
m_name = sval
initialized.Add "m_name", True
end sub
Then, to check if the var has been initialized:
If initialized.Exists("m_name") Then
' Var is initialized
4) similar to #3, except use an array of booleans. Tie specific var to a specific index, like m_name is index 0. This way you skip the hassle of controlling variable names (adds to maintenance cause as far as I know you can't get the name of the variable)
Personally, #1 is the most simple, but may not be possible in a certain situations. If #1 does not apply, I would personally pick #2, unless someone can figure out how to get a string representation of a variable name from the variable itself, then #3 is preferred.

I guess what you need is a kind of Nullable-behaviour. Yes you can do this ins VB6 with the datatype Variant. Then you can use the function "IsEmpty()" to check if a property was already set or not.
a little code-example:
Option Explicit
Private m_Vars()
'0 : Name
'1 : Birthday
'2 : Phone
Private Sub Class_Initialize()
ReDim m_Vars(0 To 2)
End Sub
Public Property Get Name() As String
Name = m_Vars(0)
End Property
Public Property Let Name(RHS As String)
m_Vars(0) = RHS
End Property
Public Property Get Birthday() As Date
Birthday = m_Vars(1)
End Property
Public Property Let Birthday(RHS As Date)
m_Vars(1) = RHS
End Property
Public Sub DoSomething()
Dim i As Integer
For i = 0 To UBound(m_Vars)
Dim v: v = m_Vars(i)
If IsEmpty(v) Then
MsgBox "is empty"
Else
MsgBox v
End If
Next
End Sub

If I understand correctly, the user will add tags or something to something like a document (e.g. [Name]) and you want to know how to map your class members to these tags. To do this, you dont really want to loop thru the class members, but the document tags to find out what is needed. When you find a "tag", submit it to your class to fill in the blank.
Part 1 is a parser to find the tags...my VB6 is very rusty, so pseudocode for this:
Const TagStart = "[" ' "$[" is better as it is not likely to appear
Const TagStop = "]" ' "]$" " " "
' make this a loop looking for tags
i = Instr(docScript, TagStart)
j = Instr(docScript, TagStop)
thisTag = Mid(docScript, TagStart, TagEnd )
' not correct, but the idea is to get the text WITH the Start and Stop markers
' like [Name] or [Address]
' get the translation...see below
docText = MyClass.Vocab(thisTag)
' put it back
Mid(docScript, TagStart, TagEnd) = docText
It is actually better to look for each possible legal tag (ie Instr(docScript, "[Name]")) which are stored in an array but you may have to do that in 2 loops to allow that a given tag could be requested more than once.
Part 2 supply the replacement text from MyClass:
Friend Function Vocab(tag as string) As String
Dim Ret as string
Select Case tag
Case "$[NAME]$"
ret = "Name: " & m_Name
' if the caption is part of the "script" then just:
'ret = m_Name
Case "$[ADDRESS]$"
ret = "Address: " & m_Addr
' if not found, return the tag so you can add new Vocab items
' or user can fix typos like '[NMAR]'
Case Else
ret = tag
...
End Select
Return Ret
End Function
The parsing routines in Part 1 could also be a method in your class to process the document "script" which calls a private Vocab.
Edit
a fraction of a 'script' might look like this:
Customer's Name:| $[CUST_FNAME]$ $[CUST_LNAME]$ (ignore the pipe (|) it was a table cell marker)
The parser looks thru the string to find "$[", when it does, it isolates the related tag $[CUST_FNAME]$. If you have a large number, the first part (CUST) can be used as a router to send it to the correct class. Next, call the method to get the translation:
newText = Cust.Vocab(thisTag)
Cust Class just looks at the tag and returns "Bob" or whatever and the parsing loop replaces the tag with the data:
Customer's Name:| Bob $[CUST_LNAME]$
Then just continue until all the tags have been replaced.
With "just" 22 vocab items, you could create a dedicated class for it:
Vocab.Translate(tag ...) as string
Case "$[CUST_FNAME]$"
return Cust.FirstName
...or
Are you trying to work out a way to do this via a DOC object from office? The above is more of from the ground up document composition type thing. For office I'd think you just need some sort of collection of replacement text.
HTH

If the user has a form that they do something to create their document, why not use simple checkbox controls, one for each possible field? If you want to use a loop to check for selected fields make the checkboxes a control array and loop though the array. You can assign the field name to the Tag property, then if the checkbox is checked add the field to an array.

Related

Convert list of strings to an enum list (One,Two,etc)

I've searched a lot but can't find a clear answer anywhere for string list -> enum.
I've got a list of strings that I want to turn into an enum that I can select from in Unity inspector.
Specifically, I'm trying to make an enum list of all the currently set-up Input buttons from project settings. I've got all the names, just don't know how to make it an enum or similar. Ideally showing up like a KeyCode variable in inspector.
Currently trying (and failing) with:
foreach (string s in names)
{
if (Enum.TryParse(s, true, out list))
Debug.Log(list);
else Debug.Log("FAILED");
}
"names" = static List<string> names;
"list" = static MyList list;
"MyList" = enum MyList { Null }
Returns "FAILED" 58 times for only 29 Input axis.
I want a simple solution, so if its not possible or relatively simple, I'll work out something else.
Code for getting the "names" list of strings (Works correctly):
var inputManager = AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/InputManager.asset")[0];
SerializedObject obj = new SerializedObject(inputManager);
SerializedProperty axisArray = obj.FindProperty("m_Axes");
if (axisArray.arraySize == 0)
Debug.Log("No Axes");
for (int i = 0; i < axisArray.arraySize; ++i)
{
var axis = axisArray.GetArrayElementAtIndex(i);
var name = axis.FindPropertyRelative("m_Name").stringValue;
names.Add(name);
}
Normally you can do
foreach (string colorName in Enum.GetNames(typeof(Colors))) which will iterate the names of the enums.
In your code above you havent shown what list is, nor where names has come from. However.
enum Things
{
Item1 = 0,
Item2 = 1
}
You can get the name from string name = Enum.GetName(typeof(Things), (int)Things.Item2) and you can get values from names with int value = (int)Enum.Parse(typeof(Things), nameOfThing)
So depending on what you actually want in a list and what you start with, iterate through and pick the relevant one
I ended up making my own solution, since (and please correct me if I'm wrong) the TryParse() and Parse() methods appear to need the enum to already contain entries with the same name (or index int) as the string to parse. This defeats the purpose for me, since I am doing this because I don't have the names in there already.
My solution ended up being to switch to having a single string input variable instead of an enum, then use Odin Inspector's ValidateInput attribute to check (for spelling errors, and) if the input variable matches any of the string entries in my dynamic list of InputManager input names (which I update manually using Odin Inspector's Button attribute and the code in the original post).
It's slightly less clean than I wanted, but does the job, so I'm satisfied.

Validate a textbox value to contain certain strings

I have a VB.NET form and the user would select from 40 different values.
I want instead of a dropbox with all 40 values to use a textbox that validates if the stirng given by user is one valuo out off those 40 words.
so for example I need the user writes something and validate the string is one of those 40 reserved words like "urgent","post"... maybe these words stored in an array, and then compared what user wrote against that?
TextBox1.Text.Contains("urgent")
TextBox1.Text.Contains("post")
TextBox1.Text.Contains("standard")
TextBox1.Text.Contains("stay")
Maybe a method
Public Function Contains(ByVal value As String) As Boolean
Return ( TextBox1.Text(value, ...) >= 0)
End Function
What would be the best way to do this?
You can create a List(of String) of your Reserved Words use the Contains method to check if what was typed was one of them using your Contains function something like this. I am not sure how large the Text you are going to be parsing so I am splitting it into individual words.
Public Class Form1
Dim reservedWords As List(Of String) = New List(Of String)({"urgent", "post", "standard", "stay"})
Private Sub TextBox1_TextChanged(sender As System.Object, e As System.EventArgs) Handles TextBox1.TextChanged
Dim text As String = CType(sender, TextBox).Text
If ContainsReservedWord(text) Then Beep()
End Sub
Public Function ContainsReservedWord(value As String) As Boolean
Dim x As Integer
Dim stringSplit As String() = value.Split
If stringSplit.Count > 0 Then
For x = 0 To stringSplit.Count - 1
If reservedWords.Contains(LCase(stringSplit(x))) Then Return True
Next
End If
Return False
End Function
End Class
Use the below code to validate the string that belongs to reserved words or not.
var lstReservedWords = new List<string> {"urgent", "post", "standard", "stay", .......};
bool isReservedWord = lstReservedWords.Any(r => String.Compare(r, TextBox1.Text, true) == 0);
Hope this helps.

Assignment of objects in VB6

I am attempting to create two identical objects in VB6 by assignment statements; something like this...
Dim myobj1 As Class1
Dim myobj2 As Class1
Set myobj1 = New Class1
myobj1.myval = 1
Set myobj2 = myobj1
It has become apparent that this doesn't create two objects but rather two references to the same object, which isn't what I am after. Is there any way to create a second object in this sort of way, or do I have to copy the object one member at a time...
Set myobj2 = new Class1
myobj2.mem1 = myobj1.mem1
...
?
Edit 2 Scott Whitlock has updated his excellent answer and I have incorporated his changes into this now-working code snippet.
Private Type MyMemento
Value1 As Integer
Value2 As String
End Type
Private Memento As MyMemento
Public Property Let myval(ByVal newval As Integer)
Memento.Value1 = newval
End Property
Public Property Get myval() As Integer
myval = Memento.Value1
End Property
Friend Property Let SetMemento(new_memento As MyMemento)
Memento = new_memento
End Property
Public Function Copy() As Class1
Dim Result As Class1
Set Result = New Class1
Result.SetMemento = Memento
Set Copy = Result
End Function
One then performs the assignment in the code thus...
Set mysecondobj = myfirstobj.Copy
Like many modern languages, VB6 has value types and reference types. Classes define reference types. On the other hand, your basic types like Integer are value types.
The basic difference is in assignment:
Dim a as Integer
Dim b as Integer
a = 2
b = a
a = 1
The result is that a is 1 and b is 2. That's because assignment in value types makes a copy. That's because each variable has space allocated for the value on the stack (in the case of VB6, an Integer takes up 2 bytes on the stack).
For classes, it works differently:
Dim a as MyClass
Dim b as MyClass
Set a = New MyClass
a.Value1 = 2
Set b = a
a.Value1 = 1
The result is that both a.Value1 and b.Value1 are 1. That's because the state of the object is stored in the heap, not on the stack. Only the reference to the object is stored on the stack, so Set b = a overwrites the reference. Interestingly, VB6 is explicit about this by forcing you to use the Set keyword. Most other modern languages don't require this.
Now, you can create your own value types (in VB6 they're called User Defined Types, but in most other languages they're called structs or structures). Here's a tutorial.
The differences between a class and a user defined type (aside from a class being a reference type and a UDT being a value type) is that a class can contain behaviors (methods and properties) where a UDT cannot. If you're just looking for a record-type class, then a UDT may be your solution.
You can use a mix of these techniques. Let's say you need a Class because you have certain behaviors and calculations that you want to include along with the data. You can use the memento pattern to hold the state of an object inside of a UDT:
Type MyMemento
Value1 As Integer
Value2 As String
End Type
In your class, make sure that all your internal state is stored inside a private member of type MyMemento. Write your properties and methods so they only use data in that one private member variable.
Now making a copy of your object is simple. Just write a new method on your class called Copy() that returns a new instance of your class and initialize it with a copy of its own memento:
Private Memento As MyMemento
Friend Sub SetMemento(NewMemento As MyMemento)
Memento = NewMemento
End Sub
Public Function Copy() as MyClass
Dim Result as MyClass
Set Result = new MyClass
Call Result.SetMemento(Memento)
Set Copy = Result
End Function
The Friend only hides it from stuff outside your project, so it doesn't do much to hide the SetMemento sub, but it's all you can do with VB6.
HTH
#Scott Whitlock, I was not able to make your code work but if it works it would be great.
I've created a regular module where I put the memento type
Type MyMemento
Value1 As Integer
Value2 As String
End Type
Then I create a class module called MyClass with the code
Private Memento As MyMemento
Friend Sub SetMemento(NewMemento As MyMemento)
Memento = NewMemento
End Sub
Public Function Copy() as MyClass
Dim Result as MyClass
Set Result = new MyClass
Result.SetMemento(Memento)
Set Copy = Result
End Function
Finally I try to call the copy function in another regular module like this
Sub Pruebas()
Dim Primero As MyClass, segundo As MyClass
Set Primero = New MyClass
Set segundo = New MyClass
Set segundo = Primero.Copy
End Sub
I get the message (below the picture): Error de compilacion: El tipo de agumento de ByRef no coincide
Here is an image (short of 10 points so here is the link): http://i.stack.imgur.com/KPdBR.gif
I was not able to get the message in English, I live in Spain.
Would you be so kind to provide with an example in VBA Excel?, I have been really trying to make this work.
Thanks for your work
===============================================
EDIT: Problem Solved:
The problem was on line "Result.SetMemento(Memento)", in VBA it needed to be called with "Call"
Public Function Copy() As MyClass
Dim Result As MyClass
Set Result = New MyClass
Call Result.SetMemento(Memento)
Set Copy = Result
End Function
It works great, thanks Scott Whitlock, you are a genius
or do I have to copy the object one member at a time...
Unfortunately yes.
It is possible (but technically very very difficult) to write a COM server in C++ that - using the IDispatch interface - will copy the value of each property, but really this is High Temple programming, if I had to do it, I don't I know if I could do it, but I'd be looking at something like 10 days work ( and I know how COM is implemented in C++, I'd also need to investigate to see if ATL framework has anything to help etc).
I worked with Vb3, 4,5 & 6 for something like 10 years (hands on, 5 days a week) and never found a good way to do this, beyond manually implementing serialisation patterns like Mementos and Save & Store, which really just boiled down to fancy ways of copying each member, one at a time.

Dynamic property names in VBA

I have a custom class module in VBA (Access) that is supposed to handle a large amount of external data. Currently I have two functions Read(name) and Write(name, value) that allows to read and set dynamic properties.
Is there a way to define a more syntactic way to read and write those data? I know that some objects in VBA have a special way of accessing data, for example the RecordSet, which allows to read and set data using myRS!property_name. Is there a way to do exactly the same for custom class modules?
The exclamation mark syntax is used to access members of a Scripting.Dictionary instance(you'll need to add a reference to Microsoft Scripting Runtime through Tools > References first). To use this syntaxyou'll need to be storing the information internally in a dictionary.
The quickest way to use it in a class is to give your class an object variable of type Scripting.Dictionary and set it up as follows:
Option Explicit
Dim d As Scripting.Dictionary
Private Sub Class_Initialize()
Set d = New Scripting.Dictionary
End Sub
Private Sub Class_Terminate()
Set d = Nothing
End Sub
Public Property Get IntData() As Scripting.Dictionary
Set IntData = d
End Property
Now you can access properties using myinstance.IntData!MyProperty = 1... but to get to where you want to be you need to use Charlie Pearson's technique for making IntData the default member for your class.
Once that's done, you can use the following syntax:
Dim m As MyClass
Set m = New MyClass
Debug.Print "Age = " & m!Age ' prints: Age =
m!Age = 27
Debug.Print "Age = " & m!Age ' prints: Age = 27
Set m = Nothing
Okay, thanks to Alain and KyleNZ I have now found a working way to do this, without having a collection or enumerable object below.
Basically, thanks to the name of the ! operator, I found out, that access via the bang/pling operator is equivalent to accessing the default member of an object. If the property Value is the default member of my class module, then there are three equivalent statements to access that property:
obj.Value("param")
obj("param")
obj!param
So to make a short syntax working for a custom class module, all one has to do is to define a default member. For example, I now used the following Value property:
Property Get Value(name As String) As String
Value = SomeLookupInMyXMLDocument(name)
End Property
Property Let Value(name As String, val As String) As String
SetSomeNodeValueInMyXMLDocument(name, val)
End Property
Normally, you could now access that like this:
obj.Value("foo") = "New value"
MsgBox obj.Value("foo")
Now to make that property the default member, you have to add a line to the Property definition:
Attribute Value.VB_UserMemId = 0
So, I end up with this:
Property Get Value(name As String) As String
Attribute Value.VB_UserMemId = 0
Value = SomeLookupInMyXMLDocument(name)
End Property
Property Let Value(name As String, val As String) As String
Attribute Value.VB_UserMemId = 0
SetSomeNodeValueInMyXMLDocument(name, val)
End Property
And after that, this works and equivalent to the code shown above:
obj("foo") = "New value"
MsgBox obj("foo")
' As well as
obj!foo = "New value"
MsgBox obj!foo
' Or for more complex `name` entries (i.e. with invalid identifier symbols)
obj![foo] = "New value"
MsgBox obj![foo]
Note that you have to add the Attribute Value.VB_UserMemId = 0 in some other editor than the VBA editor that ships with Microsoft Office, as that one hides Attribute directives for some reason.. You can easily export the module, open it in notepad, add the directives, and import it back in the VBA editor. As long as you don't change too much with the default member, the directive should not be removed (just make sure you check from time to time in an external editor).
See this other question: Bang Notation and Dot Notation in VBA and MS-Access
The bang operator (!) is shorthand for
accessing members of a Collection or
other enumerable object
If you make your class extend the Collection class in VBA then you should be able to take advantage of those operators. In the following question is an example of a user who extended the collection class:
Extend Collections Class VBA

How have you dealt with the lack of constructors in VB6?

VB6 classes have no parameterized constructors. What solution have you chosen for this? Using factory methods seems like the obvious choice, but surprise me!
I usually stick to factory methods, where I put the "constructors" for related classes in the same module (.BAS extension). Sadly, this is far from optimal since you can't really limit access to the normal object creation in VB6 - you just have to make a point of only creating your objects through the factory.
What makes it worse is having to jump between the actual object and your factory method, since organization in the IDE itself is cumbersome at best.
How about using the available class initializer? This behaves like a parameterless constructor:
Private Sub Class_Initialize()
' do initialization here
End Sub
I use a mix of factory functions (in parent classes) that then create an instance of the object and call a Friend Init() method.
Class CObjects:
Public Function Add(ByVal Param1 As String, ByVal Param2 As Long) As CObject
Dim Obj As CObject
Set Obj = New CObject
Obj.Init Param1, Param2
Set Add = Obj
End Function
Class CObject:
Friend Sub Init(ByVal Param1 As String, ByVal Param2 As Long)
If Param1 = "" Then Err.Raise 123, , "Param1 not set"
If Param2 < 0 Or Param2 > 15 Then Err.Raise 124, , "Param2 out of range"
'Init object state here
End Sub
I know the Friend scope won't have any effect in the project, but it acts as a warning that this is for internal use only.
If these objects are exposed via COM then the Init method can't be called, and setting the class to PublicNotCreatable stops it being created.
This solution is far from perfect, however, this is what I ended up doing for some of my legacy projects. It's somewhat memory-efficient for storage, but just looking up something definitely takes more time than otherwise required, especially for the huge structs.
Suppose you need to implement the following,
type user
id as long
name as String
desc as String
end type
But notice that VB6 natively supports Strings. So Instead of using structs, have "fake-struct" functions such as
Public Function user_raw(ByVal id as Long, ByRef name as String, ByRef desc as String) as String
Const RAW_DELIM as String = "|"
user_raw = cstr(id) & RAW_DELIM & name & RAW_DELIM & desc
End Function
So as a result, you will have passable-by-value (hence stackable into arrays, or multiline Strings), "fake-structs" made of strings such as
1|Foo|The boss of VB
2|Bar|Foo's apprentice
So the construct also counts as a crude way to perform to_string(). On the slightly good side, such storage is almost instantly convertible to the Markdown table syntax.
Later on, you can retrieve data from the struct by doing something like,
Public const RAW_DELIM as String = "|"
Public Const POS_USER_ID = 0
Public Const POS_USER_NAME = 1
Public Const POS_USER_DESC = 2
'...
public function get_raw_variable(byref user as String, byval var as integer) as String
dim buf() as String
buf = SPLIT(user, "|")
Assert ubound(buf) <= var
get_raw_variable = buf(var)
end function
The retrieval is sure inefficient, but is about the best you can have. Good luck