I'm using VBA in an Access application and I would like to have a n-tuple object that contains values of different data types. Then I would like a collection of those objects.
If I was doing this in javascript it would look like:
var myStructure = {
name: "blah"
age: 33
moreStuff : "test"
};
And then I would need a collection of myStructure. How can I best accomplish this in VBA?
You can define your own variable type with code such as:
Public Type T_Person
name as string
dateOfBirth as date
....
email() as string (*)
....
End type
You can then declare a T_person type in your code with:
Dim currentPerson as T_Person
currentPerson.name = myName
currentPerson.dateOfBirth = myDate
currentPerson.email(1) = myFirstEmail
....
(*) I do not remember the details for declaring arrays in such circumstances. You might have to determine array's length when defining the variable. Please check help.
The same result can also be reached by declaring a class module named, for example, "Person". In this class module, you'll be not only able to follow the objet properties (such as name, dateOfBirth, etc), but also object events (initialisation and deletion). You'll be also able to create methods on this object. You code would then look like:
Dim myPerson as Person
set myPerson = New Person
myPerson.name = myName
myPerson.dateOfBirth = myDate
if myPerson.age > 18 then (*)
'the guy is an adult'
myPerson.createAccount
Else
'the guy is not ...'
Endif
(*) Here, age is a calculated proerty of your object, available when dateOfBirth is not null. Please google "VBA class module" to find different examples for the implementation of a class module in VBA.
Now, if you want to manage a collection of similar "objects" (here, Persons), you will have to go through the creation of a class module for your objects collection (a "Persons" class module for example) and make use of the "Collection" objet available in VBA. You will then end with 2 different class modules: Person (will hold each person's detail), and Persons (will hold the collection of Persons). You'll then be able to have code like this:
Public myPersons as Persons 'at the app level, 1 main collection'
myPersons.add .... 'adding a new person to your collection'
myPersons.count ... 'counting number of persons in myPersons'
Please google on "VBA collection object" for examples on Collection management in VBA. Check my code proposal, as this was written on the fly, and without VBA help file.
The "Class" solution is clearly more powerfull, but more complex than the "Type". Go for it if you need it. It is definitely worth the pain!
PS: I am not very happy with my namings here, as this can lead to very confusing code when dealing with the myPersons collection and myPerson instance of o Person object. I'd advise you to find a more obvious one, like "PersonCollection" and "Person", or even "Individual"
You can use Variant multidimensional arrays to store your collections. Variants can store any data type making it very versatile.
Const Name as Integer = 0
Const Age as Integer = 1
Const moreStuff as Integer = 2
Dim myStructure as Variant
Redim myStructure(0 to 2, 0 to n)
myStructure(Name, 0) = "Blah"
myStructure(Age, 0) = 33
myStructure(moreStuff, 0) = "test"
Note: You can only expand the last dimension of a multidimensional array in VBA and preserve the values, so make sure that it is for the dimension you want to scale.
That is the basic data structure and you can develop a class or functions to wrap everything up to suit your needs.
You may also want to look into the Scripting.Dictionary object, though I have read that it is considered unsafe. It is more dynamic than using Type definitions, and unlike Collection, it gives you access to the keys.
Here's a sample:
Public Function TestDictionary()
Dim d As New Scripting.Dictionary 'need reference to Microsoft Scripting Runtime
'Dim d As Object
'Set d = CreateObject("Scripting.Dictionary")
Dim k As Variant
d.Add "Banana", "Yellow"
d.Add "Apple", "Red"
d.Add "Grape", "Green"
For Each k In d.Keys
Debug.Print k; ": "; d.Item(k)
Next k
End Function
The Collection class is an option.
Dim col As New Collection
col.Add("blah", "name")
col.Add(33, "age")
col.Add("test", "moreStuff")
This gives you the most flexibility. However it isn't very efficient and the Collection class has no way to get a list of keys.
Related
I am trying to build a collection with a new key on top of an older collection. I've looked at other, similar topics but they all had particular issues that I couldn't relate to mine. My collection issue requires that I take an object from an older collection (Col_0), split it into a twin object with slightly different property values, and then place both into a new collection with a key based on one of the altered properties. My problem is that the altered twin overwrites the original inside the new collection. Here is a brief example:
Public Sub FillOutput(ID As String, Col_0 As Collection)
'---Obj is a class scope object not defined here.
'---ID is the item key for items in Col_0 collection
Dim Col_1 As Collection
Set Obj_0 = Col_0.Item(ID)
'---Now I have the right item from the collection
Loop_Count = 0
Set Col_1 = New Collection
While Loop_Count < 2
If Loop_Count = 0 Then
Obj_0.Color = "Red"
Col_1.Add Obj_0, CStr(Obj_0.Color)
ElseIf Loop_Count = 1 Then
Obj_0.Color = "Blue"
Col_1.Add Obj_0, CStr(Obj_0.Color)
End If
Loop_Count = Loop_Count + 1
Wend 'Loop_count
End If
End Sub
I've tried a number of things such as creating an interim object that gets initialized and set to nothing inside the loop. As below:
ElseIf Loop_Count = 1 Then
Set pObj = New clsObjTable
pObj = Obj_0
pObj.Color = "Blue"
Col_1.Add pObj, CStr(pObj.Color)
Set pObj = Nothing
End If
Still doesn't work. My final collection keeps getting overwritten such that both items are "Blue". I have a suspicion that its about keys getting transferred and screwed up but I just don't know enough here. Any help or guidance here would be much appreciated!
Collections do not actually contain copies of objects, they only contain references to them. If you change your object after adding it to the collection, the object referenced in your collection has the changes too.
The only way to change this behavior, is to manually copy the object. But that's not an easy thing to do. Read this question on cloning objects, but note that you can't just clone built-in objects.
You could try to create a helper function to move all relevant properties from your object to a fresh copy of an object of the same object type, and then return the copy, when struggling with a built-in Access object. But some objects have read-only or private properties, and are thus unclonable.
At the moment all my arrays are implemented in the vba code. This works fine
Dim COST As String
Dim GAT As String
Dim OND As String
COST = "C:\Users\update\COST.xlsb"
GAT = "C:\Users\BACKUP\GAT.xlsb"
OND = "C:\Users\BACKUP\OND.xlsb"
MyArray = Array(COST, GAT, OND)
However I would like to select the above arrays from a textbox via a form
Below code works but I have to implement the file path
MyArray = Array(Forms![LAYOUT_F]![Update_F])
with file path
MyArray = Array("C:\Users\BACKUP\" & Forms![LAYOUT_F]![Update_F])
Using the above will require me to be file name specific and doesn't take into account arrays plus the file paths are different
Is there a way of selecting declared arrays from a textbox
I would use a multi-line textbox, where the user e.g. enters
C:\Users\update\COST.xlsb
C:\Users\BACKUP\GAT.xlsb
and then use the Split() function on the line breaks:
MyArray = Split(Me!myTextbox, vbCrLf)
But I my have misunderstood your question - not sure what you mean by "selecting declared arrays from a textbox".
When using a Linq-to-SQL class, how can I make a simple copy of an entity and save it?
My entity has a guid for a unique ID that gets automatically generated in the SQL Server.
I don't require a "deep clone".
I tried to use some clone methods that are out there but I couldn't figure out how to get everything serialized that needed to be serialized (got stuck on the DataContext not being serializable).
Can I just get an entity, detach it from the DataContext, null out the unique ID and InsertOnSubmit in a new DataContext? If so, how would I do this?
VB.net code preferred but not required.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
UPDATE:
Public Shared Function ReIssue(RequestID As Guid) As Guid
Dim req As Request
Dim new_req As Request
Using dc1 As New MBDataContext()
req = (From r In dc1.Requests Where r.ID = RequestID).Single()
End Using
new_req = req
new_req.ID = Guid.Empty
new_req.CreateDate = Nothing
Using dc2 As New MBDataContext()
dc2.Requests.InsertOnSubmit(new_req)
dc2.SubmitChanges()
End Using
End Function
I get an error: An attempt has been made to Attach or Add an entity that is not new, perhaps having been loaded from another DataContext. This is not supported.
on this line: dc2.Requests.InsertOnSubmit(new_req)
Nulling out the unique id and then calling InsertOnSubmit is the right way to go. Some things you have to consider though:
What is the type of the id? Is it an int? A Guid? Is it nullable? If it is nullable, make sure to set it to null, if it is an int, then to 0, or a Guid, then to Guid.Empty.
Does the type have a timestamp of some kind? If so, then you have to reset/set it to null as well, depending on the type.
Once you've done that, you can call InsertOnSubmit and then SubmitChanges and the change should take place.
Note, if you are doing this for a large number of records, you are better off writing a stored procedure which will perform the insert into the table using a select from the other table. It will be much faster that way (you won't be loading the data from the database into memory then pushing it back, inserting the records one at a time).
This method seems to have worked perfectly.
Making the final code look like this:
Public Shared Function ReIssue(RequestID As Guid) As Guid
Using dc As New MBDataContext()
Dim req As Request
req = (From r In dc.Requests Where r.ID = RequestID).Single()
Dim new_req As Request = DirectCast(Entity.Copy(req, New Request()), Request)
dc.Requests.InsertOnSubmit(new_req)
dc.SubmitChanges()
req.ActiveRequestParentID = new_req.ID
dc.SubmitChanges()
Return new_req.ID
End Using
End Function
Public NotInheritable Class Entity
Private Sub New()
End Sub
Public Shared Function Copy(source As Object, destination As Object) As Object
Dim sourceProps As System.Reflection.PropertyInfo() = source.[GetType]().GetProperties()
Dim destinationProps As System.Reflection.PropertyInfo() = destination.[GetType]().GetProperties()
For Each sourceProp As System.Reflection.PropertyInfo In sourceProps
Dim column As ColumnAttribute = TryCast(Attribute.GetCustomAttribute(sourceProp, GetType(ColumnAttribute)), ColumnAttribute)
If column IsNot Nothing AndAlso Not column.IsPrimaryKey Then
For Each destinationProp As System.Reflection.PropertyInfo In destinationProps
If sourceProp.Name = destinationProp.Name AndAlso destinationProp.CanWrite Then
destinationProp.SetValue(destination, sourceProp.GetValue(source, Nothing), Nothing)
Exit For
End If
Next
End If
Next
Return destination
End Function
End Class
I want to initialize a class with data coming from a MySql db. Some fields can be null:
Dim dr As MySqlDataReader = ...
Dim item As New Item(dr.GetInt16(0), dr.GetString(1), dr.GetString(2))
Suppose the last two fields could be NULL In the db, so that calling GetString on that field causes an exception.
I could certainly write code to test for NULLs before I get each field:
dim field1 as String
if ( dr.IsDbNull(1) )
field1 = Nothing ' or even ""
else
field1 = dr.GetString(1)
But if you have many fields this is an "ifs" nightmare.
To this purpose I rewrote the IIf VB function to make it more typed, thus to avoid casts:
Namespace Util
Public Shared Function IIf(Of T)(ByVal condition As Boolean, ByVal iftrue As T, ByVal iffalse As T) As T
If condition Then Return iftrue Else Return iffalse
End Function
So that I could write something like:
Dim item As New Item(
dr.GetInt16(0),
Util.IIf(dr.IsDbNull(1), "", dr.GetString(1),
Util.IIf(dr.IsDbNull(2), "", dr.GetString(2))
The typed IIf works well in other cases, but unfortunately it doesn't in this instance, because being it a normal function and not a language keyword, each inpout parameter is evaluated during the call, and when the field is NULL the exception is raised.
Can you think of an elegant if-less solution?
First off, I'd recommend you to use an ORM mapper - there are very few cases nowadays when you have to do manual "mapping".
If this is one of these cases, I'd recommend you to use field names instead of indexes while accessing Data Reader.
And to answer your original question: try extension methods. Sorry for C#, but VB.NET syntax drives me nuts:
public static class DbDataReaderExtensions
{
public static T GetField<T>(this DbDataReader dbDataReader, string fieldName,
T defaultValue)
{
if(dbDataReader.IsDBNull(fieldName))
return defaultValue;
return (T)dbDataReader[fieldName];
}
}
Thanks.
I have looked at many ORMs and I don't like them for one reason or another, so I decided to call plain stored procedures to get data. Can you advice something powerful yet simple?
You're right about to use field names, it is safer, even if a little bit slower.
I had just arrived to the same conclusion with the method, but what I still don't like is the type conversion:
Public Shared Function IfNull(Of T)(ByVal dr As MySqlDataReader, ByVal index As Integer, ByVal _default As T) As T
If dr.IsDBNull(index) Then
Return _default
Else
Return CType(dr.GetValue(index), T)
End If
End Function
I would like to do something more elegant to get the "real" data type from the reader.
With FileInfo I can find the file name of a DataContext .dbml file.
Provided I declare:
Dim DataModel = New AttributeMappingSource().GetModel(GetType(NorthwindDataContext))
With System.Data.LINQ.Mapping I can find the name of all Tables and furthermore their Columns and relationships.
All this thanks to the excellent post from Jomo Fisher here: LINQ to SQL Trick: Get all Table [and Column] Names: http://blogs.msdn.com/jomo_fisher/archive/2007/07/30/linq-to-sql-trick-get-all-table-names.aspx
But how can I achieve same result without explicitly knowing the DataContext Object?
I mean how can i "replace" this:
GetType(NorthwindDataContext))
With:
dim myDCFile as String = "Northwind.dbml"
Dim DataModel = .../... GetType(myDCFile))
You could try something like this:
Assembly dataAssembly = Assembly.Load("Your.Data.Assembly");
Type dataContextType = dataAssembly.GetTypes()
.FirstOrDefault(x => x.IsSubclassOf(typeof(DataContext)));
You would basically load your data assembly and search for the first data type that is a subtype of DataContext.
You could then pass that into your method:
Dim DataModel = New AttributeMappingSource().GetModel(dataContextType)
Marc