Save a custom Class to a Access Table with VBA - vba

I have a really weird one that I'm not quite clear if it's possible. I have an Access database that I've built that stores documents basically. And in it, you are able to produce these documents in Excel. As it stands today, I have it storing values I want to put into a cell as a table value, such as a string. But I'd like to possibly store an attributed string so that I can also store formatting.
I have an idea of how I would make a custom class that would basically be an attributed string, but then I still have the problem that it would need to be an object that could be stored in an Access table.
I was thinking to make an OLEObject field in the table, and save it there, but it gives me an error when I try saving my custom class in that field.
Run-time error '438':
Object doesn't support this property or method
I tried making an object variable and then setting my custom class to that, but still same error.
Dim attStr As New AttributedStringClass
attStr.Value = "Test Test"
Dim oleObj As Object
Set oleObj = attStr
Dim rst As Recordset: Set rst = CurrentDb.OpenRecordset("tblTest")
rst.AddNew
rst("attributeString") = oleObj
rst.Update
AttributedStringClass
Option Compare Database
Option Explicit
Dim zValue As String
Property Get Value() As String
Value = zValue
End Property
Property Let Value(dValue As String)
zValue = dValue
End Property
I kept it really simple to test if I could store the custom class, just in case it wasn't possible.
Is what I'm trying to do even possible? Or am I barking up the wrong tree?
Thanks in advance!

Unfortunately, what you looking for is object serialization. .net supports serialization, and thus you can convert a object into XML, or these days the much tighter and shorter format used is JSON.
You could however, make your own serializer. So, you would have to take the custom class you make, and call a routine (passing the class object) to spit out all the values as text. Perhaps the format could be comma delimited, or I suppose even JSON format (but we don't have a good JSON serlizer/de-serlizer like we do in .net).
You then save the text in a standard memo column. You could then read/pull that data, and call a routine to de-serialize the text back into the object.
But, since you do know the class, you can expose each property, and use a for/each loop. this trick is outline here:
https://www.mrexcel.com/forum/excel-questions/466141-use-custom-class-each-loop.html
However, what I would simply do is make your class, add all the "members", and then simply add a routine called serialize, and de serializer.
So:
dim clsMyClass as new clsTour
clsMyClass.HotelName = "Greenwood Inn"
.etc. etc. etc.
' now get all of the values as a string
dim strCsv as string
strCsv = clsMyClass.Serlize
' now, the comma delimited strCsv as all the values of the class as a string
rstData!Cdata = strCsv
rstData.update
Now, at this point,the memo field is saved (as noted, xml, json, or csv format is fine).
To pull + load (de-serialize) the class, we now go:
dim rstData as DAO.Recordset
' code to load up reocord set
set rstData = currentdb.OpenRecordSet("Select * from tblTours where id =2")
strCsv = rstData!CData
dim clsMyClass as new clsTour
clsMyclass.Serialize = strCsv
' at this point, your class is now loaded with all the correct values.
eg:
msgbox "Hotel name = " & clsMyClass.HotelName
So, in the .net world, the idea of serializing a class ito a string, passing to a web service, and then on that end, they de-serialize the object back into a class/object.
In .net, this generating is built into the frame work. So, when you call a SOAP or these days more common a REST service, then the data is sent to you as xml (or json). On your end, you now call the de-serialize method, and you have the object now ready for use in your code. So, this idea of converting a class into some kind of "string" or something that can be saved as text, or pass (or pulled) from a web site is rather common these days.
So, your idea and question is rather normal, especially if you coming from any of the modern systems and frameworks that support serialization.
As noted, if your class only has say 5-10 values to save, then a simple method to serialize and de-serialize all values to/from a string from the values the class holds is not hard at all. But for complex objects, then of course one would want a development platform that supports this automatic. In .net, you can pass any object to a serializer, and it will spit back the xml (or json) string. Now that string can be saved, sent to a web site, or some program. And to get the object, you de-serialize that string back to the object for use in your code.
Do keep in mind that this whole concept only works well for a well defined class, and if the class is not dynamic, then the concept works well.

Related

How to handle object declaration in VBA (Error 91)

I'm stuck in VBA and I couldn't find a good answer in the other questions related to error 91. I want to create an object and store variables and arrays inside that object. I tried an approach like I would do in js:
Dim block As Object
...
Set block = Nothing
block.Name = "Unbekannter Prüfblock"
block.Awf = "Unbekannter Anwendungsfall"
block.Conditions = Array()
block.Checks = Array()
I use the "Set block = Nothing" because I will use it multiple times in a loop.
But all I get is error 91 - Object variable not set
How can I set the object?
Do I really have to declare everything in vba?
Isn't there a "stop annoying me with declaration notices" toggle? ;-)
Update
Thank you all so much for the detailed answers!
As suggested I created a class for "block" and also a class for "condition" and "check". Block for example:
Option Explicit
Public name As String
Public awf As String
Public conditions As Collection
Public checks As Collection
Then I use it inside my code like this:
Dim bl As Block
Dim co As Condition
Dim ce As Check
Set bl = New Block
bl.name = ws.Range("B" & i).value
bl.awf = ws.Range("B" & i).value
Set co = New Condition
co.attr = ws.Range("B" & i).value
co.value = ws.Range("C" & i).value
bl.conditions.Add co
VBA isn't Javascript; objects and their members cannot be created inline, they need a class definition.
When you make a member call against an object, you refer to it by name, and whenever that name refers to a null reference (Nothing) you'll get error 91.
To fix it, you need to ensure every member call is made against a valid object reference. Using the Set keyword you can assign such a reference, and to create a new instance of an object you can use the New keyword followed by the name of the class that defines the type you want a new instance of:
Dim Block As Object
Block.Something = 42 ' Error 91
Set Block = New SomeClass ' set reference
Block.Something = 42 ' OK
Note that because the object is declared As Object, every member call is late-bound (resolved at run-time); if the member doesn't exist (or if there's a typo), you'll get error 438 at run-time.
You can move this error to compile-time with early binding by using a more specific data type for the declaration:
Dim Block As SomeClass
Because the members of SomeClass are known at compile-time, the IDE will now provide you with a member completion list when you type up a member call, and typos will no longer be valid at compile-time: strive to remain in the early-bound realm whenever possible! Note: As Variant (explicit or not) is also similarly late-bound.
So we add a new class module and call it SomeClass and we add a couple of public fields:
Option Explicit
Public Name As String
Public Case As String
Public Condition As Variant
Public Check As Variant
And now you can create and consume a new instance of that class, and add instances of it to a collection to process later (note: you can't do that with a UDT/Type).
The VBIDE settings have an annoying option ("automatic syntax check", IIRC) that immediately pops a message box whenever there's a compilation error on the current line; uncheck it (invalid lines will appear in red, without a message box), but do have the "require variable declaration" setting checked: it will add Option Explicit to every module, and that will spare you from a number of easily avoidable run-time errors, moving them to compile-time.
In JS, you can add properties (together with values) on the fly to an object. That's not possible in VBA (and most other languages).
Your declaration Dim block As Object is defining a variable that is supposed to point to an Object. But it isn't pointing to anything yet, per default it is initialized with Nothing, which is, literally, nothing, and has neither properties nor methods, it's just a placeholder to signal "I don't point to anything yet". Furthermore, Object cannot be instantiated.
in VBA, you assign something to an object variable with Set (this is different to most other languages). If you want to create a new object, you use the keyword New.
However, before you do that, you need to know what kind of object (which class) you need. This can be an existing class, eg a Worksheet or a Range in Excel, or it can be an own defined class (by creating a new class module in your code). In any case, you need to define the properties and the methods of that class. Considering the most simple class module Class1 (of course you should think about a more usefull name):
Option Explicit
Public name as String
Public case as String
(I will not start to talk about private members and getter and setter).
You then write
Dim block As Class1
Set block = New Class1
block.name = "Unbekannter Prüfblock"
(But block.data = "Hello world" will not be possible as data is not a member of your class.)
Big advantage of this attempt is that the compiler can show you when you for example mistyped a property name before you even start your code (Debug->Compile). In JS, you will get either a runtime error or a new property on the fly - hard to find nasty bugs.
If you later need a new (empty) object, just create a new object using New. If the old object is not referenced anywhere else, the VBA runtime will take care that the memory is freed (so no need to write Set block = Nothing).
In case you really don't know the properties in advance (eg when you read XML or JSON files or a key-value list from an Excel sheet...), you can consider to use a Collection or a Dictionary. Plenty of examples on SO and elsewhere.
One remark to block.Condition = Array(). Arrays in VBA are not really dynamic, you cannot add or remove entries easily during runtime. In VBA, you have static and dynamic arrays:
Dim a(1 to 10) as String ' Static array with 10 members (at compile time)
Dim b() as String ' Dynamic array.
However, for dynamic members you need to define how many members you need before you write something into it, you use the ReDim statement for that. Useful if you can calculate the number of members in advance:
Redim b(1 to maxNames) ' Dynamic array, now with maxNames members (whatever maxNames is)
You can change the array size of a dynamic array with Redim Preserve, but that should be an exception (bad programming style, inperformant). Without Preserve, you will get a new array, but the former data is lost.
Redim Preserve b(1 to maxNames+10) ' Now with 10 more members.
If you really don't know the number of members and it can change often during runtime, again a Collection or a Dictionary can be the better alternative. Note that for example a Dictionary can itself a Dictionary as value, which allows to define Tree structures.
Regarding your issue adding to the collection:
You need to add this code to your class module "Block" - only then you can add objects to the collections
Private Sub Class_Initialize()
Set conditions = New Collection
set checks = new Collection
End Sub

Accessing an object over class

When reading an old project of mine I found something suspicious where I don't really understand why this part is working:
Public Shared Sub getXMLforProject(QueryString As String)
Dim linkStart As String = "http://example.org"
Dim linkEnd As String = "&tempMax=2000"
Dim target As String = linkStart & QueryString & linkEnd
'replaces parts that need encoding,
'groups(1) is the sign e.g. <= and groups(2) is the text that needs encoding
'groups(0) is the text of the full match (sign and encoding text)
target = rx.Replace(target, Function(m As Match) encodeURLString(m.Groups(1).Value) + encodeURLString(m.Groups(2).Value))
GUI.WebBrowser.Navigate(target)
Return True
End Sub
the respective path that seams suspicious to me is the line
GUI.WebBrowser.Navigate(target)
There is a class called GUI that realises the user interface, but in the file context there is no objects named "GUI" available, so the access must be done by using the class. How is it possible for this to work? Is there an implicit mechanism that redirects the call from the GUI-class to the GUI-object?
You are using VB.NET, it emulates the behavior of the Form class from earlier Visual Basic editions where using the type name was a legal way to refer to an instance of the class. Kinda necessary to give programmers a fighting chance to convert their VB6 projects. Underlying plumbing is the My.Forms object.
So, 99.9% odds are that the GUI class derives from System.Windows.Forms.Form. Especially given that it has a WebBrowser member. The Form is the host window for the browser.

VB6 map string to integer for headers

I'm trying to parse a CSV File into a VB6 application in order to update multiple records on a table on SQL with existing single record updating code already in the form. The CSV Files will have a header row whixh can be used to validate the information going into the correct place in the ADODB recordset. In C++ you can use a map to say like
map<String s, int x> column
column<"First Name", -1>
column<"Last Name",-1>
Then create a counter across the comma delimited values where if the third value is Last Name then the code could be written to change
column<"Last Name",-1> to column<"Last Name",3> and if x != -1 in any of the maps the file is valid for use, I would then loop through the remaining records and parse into a container using something similar to
strLastName = Array<column[3]>
to assign the record values to the correct variables. I am still very new to VB6, how can I accomplish something similar in VB6 and what containers should be used? So far I have
Public Sub GetImportValues()
On Error GoTo GetImportValues_Error:
Dim intFileNum As Integer
Open Path For Input As #intFileNum
Do Until EOF(intFileNum)
Line Input #intFileNum, vbCrLf
FunctionThatSavesInformationToSQL
Loop
Close #intFileNum
GetImportValues_Exit:
Exit Sub
GetImportValues_Error:
Err.Source = "frmMemberAdd.GetImportValues" & " | " & Err.Source
Err.Raise Err.Number, Err.Source, Err.Description
End Sub
with a dialog box returning the path as a string using App.path in a separate Function
*****************************************************Slight change to answer
The collection was on track for what I had asked but I did have to change it to dictionary because you cannot return items on a collection which kept me from comparing the items and changing the keys but dictionary can. Make sure if you use dictionary you switch the item and key.
If I understand your question correctly, you're trying to create a map (Dictionary<string, int> in C#). In VB6, you can use Collection for this purpose - it's roughly equivalent to C#'s Dictionary<string, object>. It uses String keys and stores all values as Variant. For example:
Dim oColl As Collection
Set oColl = New Collection
oColl.Add -1, "ColumnName"
Dim nColumnIndex As Long
'Get column index for column name.
nColumnIndex = oColl.Item("ColumnName")
If nColumnIndex = -1 Then
nColumnIndex = ...
'When you want to update a column index in the collection, you
'first have to remove the item and then add it back with the right
'index.
oColl.Remove "ColumnName"
oColl.Add nColumnIndex, "ColumnName"
End If
Edit 1:
One word of warning regarding VB6: you'll see many samples doing this:
Dim oObj As New SomeClass
It's ok to do this in VB.Net but don't ever do this in VB6. Declare and instantiate the object on separate statements because the single-statement form generates code where oObj is checked for Nothing and set to an instance before each use. This slows down your code (unnecessary checks) and creates hard-to-find bugs if you're using an instance that's supposed to be gone.
Always do this instead:
Dim oObj As SomeClass
Set oObj = New SomeClass
...
'Clean up the object when you're done with it. Remember, there's
'no garbage collection in COM / VB6, you have to manage object
'lifetimes.
Set oObj = Nothing
Also, use Long instead of Integer as much as you can - Long is a 32-bit integer, while Integer is only 16-bits. VB6 type names can be misleading frequently. Here's an old answer of mine with a bit more detail (not strictly related to your question but useful).
Alternatively, you can create a simplified wrapper around the .NET Dictionary class and expose it as a COM object: this would allow you to call it from VB6. This would likely be (somewhat) slower than Collection and it'd require the .NET Framework for your VB6 project to run.
Edit 2:
As #CMaster commented, Dictionary is available from the Microsoft Scripting Runtime library - you need to add a reference to it to use it (this is why I prefer Collection - it has no dependency). This answer has details about how to use it.

Report Viewer - Object With Nested List Objects

I have an existing class structure in place and want/need to use that as a data source for a series of reports using vb and 2005, (though we are almost ready to move to 2010, so if that will solve this ill move today!)
Using gotreportviewer sample for nested objects ive added a reportmanager class exposing a getdata method which ive populated with all my data and returned a list(of object). the data is there and correct at the point of databinding, i can add and reference top level properties, however not matter what syntax i try i cant reference the fields in nested classes/lists. I get various messages ranging from "#Error" in the ouput field to nothing, to wont compile.
my class structure is roughly this in short form:
Assembly0
Class ReportManager
TheData as List(Of Object)
New() 'that populates TheData from the class structure below
GetData() as List(of Object)
Assembly1
Class Test
aProperty1 as String
aProperty2 as Int
aProperty3 as String
aProperty4 as String
aProperty4 as List(of aType1)
Assembly2
Class AaType1
aProperty1 as String
aProperty2 as Int
aProperty3 as String
aProperty4 as String
aProperty4 as List(of aType2)
aProperty4 as List(of aType3)
aProperty4 as String
Assembly3
Class aType2
aProperty1 as Boolean
aProperty1 as String
you get the idea
and so on.....
in my main app
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' Create an instance of our ReportManager Class
Try
' trust assemblies used in get data
ReportViewer1.LocalReport.ExecuteReportInCurrentAppDomain(Assembly.GetExecutingAssembly().Evidence)
ReportViewer1.LocalReport.AddTrustedCodeModuleInCurrentAppDomain("assy1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1234")
ReportViewer1.LocalReport.AddTrustedCodeModuleInCurrentAppDomain("assy2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1234")
' etc through ALL dependant assemblies
' create datamanager, that will populate its TheData property
Dim reportMan As Data.Reporting.Manager = New Data.Reporting.Manager(18) ' test id sent
' this is the method from the gotreportviewer sample, which only allows you to reference top level properties, regardless of syntax used. i.e. =Fields!Prop.Value.SubProp
' doesnt work
'ReportViewer1.LocalReport.DataSources.Add(New ReportDataSource("DummyDataSource", reportMan.GetData))
'Me.ReportingDataBindingSource.DataSource = reportMan.GetData
' this is the only method I have found that allows me to reference an objects nested property and its fields.....?
Data = reportMan.GetData()
Me.ReportViewer1.ProcessingMode = Microsoft.Reporting.WinForms.ProcessingMode.Local
Me.ReportViewer1.LocalReport.DataSources.Add(New ReportDataSource("Data_Reporting_ReportingData", Data))
' fortnatley there is only ever one test in the list, HOWEVER there will be 4 specimens and n stages below that and so on..
Dim SpecimenData As SpecimenList = Data(0).Specimens
Me.ReportViewer1.LocalReport.DataSources.Add(New ReportDataSource("Tests_Specimen", SpecimenData))
' so this method is no good either. currently only a test its just returning the first specimen.
'Dim StageData As Tests.Stages = Data(0).Specimens(0).Stages
'Me.ReportViewer1.LocalReport.DataSources.Add(New ReportDataSource("Tests_Specimen", SpecimenData))
' render report
Me.ReportViewer1.RefreshReport()
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
End Sub
Fixes i found online/googling:
You must add "ExecuteReportInCurrentAppDomain",
done that no difference.
You must add
Assembly: AllowPartiallyTrustedCallers() to AssemblyInfo.vb, No difference.
You must strongly name you dependent assemblies, done that and it did get rid of
an error regarding a call being made in the "Code" property of the report
(for localization).
have tried the =Fields!Property.Value.SubProperty syntax and it DOESN'T work! no matter what variation I try.
' in the rdlc - this syntax works for a top level properties
=Sum(Fields!TestVersion.Value, "Data_Reporting_ReportingData")
' using the alternate method list in the above code this works
=First(Fields!Index.Value, "Tests_Specimen")
' but these dont for its child properties
=First(Fields!Specimens.Value.Index, "Data_Reporting_ReportingData")
=Fields!Specimens.Value.Index
=Fields!Specimens.Value.Index.Value
So does that mean I have no choice but to create something like
Dim SpecimenData As Tests.SpecimenList = Data(0).Specimens for every single nested object? Also for obvious reasons I'd rather not have to flatten the entire datastructure as it's massive.
I have tried everything I can find on this, not much out there and everything points back to the same three four articles/blog posts that just aren't working for me, their samples unmodified work, but none of them work when applied to nested lists or nested objects of inherited list types.
Does anyone have any sample code of using objects with nested lists that actually works? as none of the ones I could find online work with anything but the simplest of scenarios. i.e. one assembly, or one code file or no nested lists or simple/native types.
I've been at this the best part of A WEEK! I'm now bald and stressed please help.
Failing that can anyone suggest a thrid party vendor that does support this sort of thing?
does crystal? pebble?
Apologies for the wall of text...
Matma
I´m Looking for almost the same, except, that I have objects that have as property other objects, no List of objects, any way, you asked if Crystal Reports do this kind of thing, YES IT DOES, it´s a little bit difficult to do it, but it does.
I don´t know why it´s so difficult to work with that kind of think on now days. Because we are aways working with persistance Frameworks, like Entity Framework, and others, so, you do a hell of a job with a Persistance, and when you go to reports, you need to back to your DataBase model if you want easy work! So waste of time!
I Just have found, it´s possible to do it in report viewer, But it had a problem in visual studio 2010, it´s fixed in SP1 but you need to set all your classes that are used as nested objects as Serializable
Please read : http://wraithnath.blogspot.com.br/2011/04/reportviewer-object-datasource-nested.html

How do I update a single table of a DataSet using a TableAdapter, without hard-coding the table name?

This seems like a really basic thing that I'm doing, yet I'm tearing my hair out trying to make it work.
My situation is this: I have a project which contains a large number of lookup tables, and I have all of these lookup tables represented in a single typed DataSet, which contains TableAdapters for each lookup. I've designed an editor for these lookup tables, which should allow editing of one of these at a time. My front-end is written in VB and WinForms, the back-end is a SOAP web service; I can successfully pass the changes to the DataSet back to the web service, but can't find a way to use a TableAdapter to update the single table that has been changed.
What I'm trying to do is instantiate the appropriate TableAdapter for the updated DataTable by sending the name of the table back to the web service along with the DataSet, then referring to the TableAdapter with a dynamic name. The normal way to instantiate a TableAdapter is this:
Dim ta As New dsLookupsTableAdapters.tlkpMyTableTableAdapter
What I'd like to do is this, but of course it doesn't work:
strTableName = "tlkpMyTable"
Dim ta As New dsLookupsTableAdapters(strTableName & "TableAdapter")
Is there any way to achieve this, or am I taking the wrong approach altogether? My other alternative is to write separate code for each table, which I'd prefer to avoid!
You can use Activator to create an instance of your TableAdapter from its string name, just like you want:
object adapter = Activator.CreateInstance(Type.GetType("My.Namespace.MyDataSetTableAdapters." + myTable.Name + "TableAdapter"));
Then, because TableAdapters don't have a common interface, you should use reflection to call its Update method:
adapter.GetType().GetMethod("Update").Invoke(adapter, null);
http://msdn.microsoft.com/en-us/library/system.type.getmethod.aspx
This is from memory, but roughly close enough. You can also use GetProperty to get the connection property and set it as required.
Not sure I 100% understand, do you have a single DataTable in your DataSet, or one DataTable per lookup table?
Anyway, perhaps you could you this approach to filter by lookup table?
It's pretty easy to create types at runtime given the (string) type name.
Here's a self-contained VB class which illustrates one way to do it: use System.Activator.CreateInstance to create instances of types using a string representation of the type name. Then you can cast it to a DataAdapter base class and use it like any other DataAdapter.
Public Class dsLookupsTableAdapters
Public Function CreateInstance(ByVal strName As String) As Object
CreateInstance = Nothing
For Each a As System.Reflection.Assembly In System.AppDomain.CurrentDomain.GetAssemblies()
Try
Dim strAssemblyName As String() = a.FullName.Split(New Char() {","c})
Dim strNameTemp As String = strAssemblyName(0) & "." & strName
Dim instance As Object = System.Activator.CreateInstance(a.FullName, strNameTemp)
If instance IsNot Nothing Then
Dim handle As System.Runtime.Remoting.ObjectHandle
handle = CType(instance, System.Runtime.Remoting.ObjectHandle)
Dim o As Object = handle.Unwrap()
CreateInstance = o
Exit For
End If
Catch ex As System.Exception
Continue For ' ignore exception, means type isn't there
End Try
Next
End Function
Public Class tlkpMyTableTableAdapter
Inherits System.Data.Common.DataAdapter
End Class
Public Sub Test()
' define type name. note that, in this sample, tlkpMyTableTableAdapter is a nested
' class and dsLookupsTableAdapters is the containing class, hence the "+". If, however,
' dsLookupsTableAdapters is a namespace, replace the "+" with a "."
Dim typeName As String = "dsLookupsTableAdapters+tlkpMyTableTableAdapter"
Dim adapter As System.Data.Common.DataAdapter
Dim o As Object = CreateInstance(typeName)
adapter = CType(o, System.Data.Common.DataAdapter)
End Sub
End Class
If you are using VB.Net 2008, then use the tableadaptermanager (http://msdn.microsoft.com/en-us/library/bb384426.aspx). I think this would be much easier to code against :)
Wade