How to make Listbox.List keep type information? - vba

If I affect a full array to a ListBox, using ListBox1.List = [{1;2;3;4}] for example, the Listbox's .List items keep their correct type (here numbers), but if I use ListBox1.AddItem 5 or Listbox1.List(5) = 6 to set an individual item the type is automatically changed to String.
Sample Code:
Private Sub UserForm_Initialize()
ListBox1.List = [{1;2;3;4}]
ListBox1.AddItem 5
ListBox1.AddItem
ListBox1.List(5) = 6
End Sub
Later on, when comparing values, I get wrong results because numbers are not equals to text (5 <>"5").
Is there any easy (1) way to ensure the type of the list items is not converted to String?
(1) I know I can explicitly make the conversion to String, but I rather keep my values as numbers instead of "numbers-strored-as-text" in the listbox

I guess that will be impossible when using AddItem. According to https://learn.microsoft.com/en-us/office/vba/api/access.listbox.additem, the first parameter is a string, so everything you pass will be converted to a string.
Probably your best bet is to collect all items in an array and assign the array using ListBox1.List. Or you have to live with numbers stored as string...
Update
I mixed the pages up - link to the Access page was wrong.
Anyhow, Documentation is rather poor. It says "valid object" but doesn't define what that means. At least it is not possible to add a object - that throws a type mismatch.
Also, the documentation states that a Variant is returned, but it seems thatis not true - when I try to get the result, the compiler throws an error.
As a conclusion, I assume that AddItem converts everything to a string (and throws an error if that fails). So I still assume that you have to build up an array and assign it to List if you want to have real numbers.

Related

excel VBA - refurn variable length array from function

I want to emulate the results of the builtin "text-to-fields" in a UDF function.
I need to do this because my original data comes from a web query, and I need to use the results on a separate page and plot those results.
For plotting, I need missing values to parse to empty cells, since that is the only option for excel graphs to show missing values as gaps.
You cannot do that with the builtin, because of two limitations;
1) It cannot target the parsed fields onto another sheet.
2) Trying to copy the data values to the destination sheet to parse them there fails, because text-to-fields parses the referencing expression, instead of the value it references.
3) I cannot parse on the original data sheet, and then copy the parsed fields to the target sheet, because no expression can copy an empty cell, it gets converted into a zero value. (after all, an expression resulting in an empty cell would erase itself!)
So I need a DIY field parser, and in any case using a formula is better for my overall needs than having to macro-ize the builtin function (even if it would work).
My fields look like this:
calm
S 10
S 10 G 20
And I want them to parse just as a text-to-fields would, which would give numerics for numbers, strings for text, and empty for missing fields (i.e. shorter readings.)
So I used this code;
Function Explode(texte As String, Optional ByVal delimiter As String = " ") _
As String()
' mimic the text-to-fields,
' but allow inter-sheet references
Explode = Split(texte, delimiter)
End Function
But to use it, I have to pre-define the function calling cell as part of an array, which is fixed size, and I don't know how to have this return a variable number of parsed fields into a fixed size array. What I want from the sample data above is:
But what I get instead is:
Note that empty cells must be empty - not just look empty (not "" strings).
Edit:
I suspect that I may have to instead create a sub which sets the values of the parsed fields and clears the remainder of cells for missing fields (I always have a maximum of four fields) instead of returning them, but am not very VBA proficient. For example, something that gets two cell references, one for the source reference, another for the target list of parsed fields. Then call that from a function which I can embed in the sheet. Side-effect based programming...
You could make your function return a fixed array size using Redim Preserve.
{=Explode($A$1,50,",")}
Function Explode(texte As String, ArraySize As Integer, Optional ByVal delimiter As String = " ") _
As String()
Explode = Split(texte, delimiter)
ReDim Preserve Explode(ArraySize- 1)
End Function

How to properly read a single Excel cell

I've been looking up in Google in these past days but I still can't find a good answer to this one.
Currently, this is how I do it:
For Each cell In ws.Range(fromCol, toCol)
If IsNothing(cell.Value) Then Exit For
valueList.Push(cell.Value.ToString())
Next
But when it reads a cell whose assumed data type is Time, it returns a Double value. I try to parse that value but it's not the same as expected.
How can I properly read a single Excel cell with an assumed type of Time?
As per the comment suggesting the article,
.Text is a bad idea in the sense that it will give you just the displayed string in that cell. If you use TypeName on the cell's content, for example, it will always return string regardless of any datatypes the content might be. However, if you use .Value, it will return the actual datatype name of the content of that cell.
This can prove useful to you if you're using TypeName in a comparison for instance. It saves you using conversion functions.
Take this example:
Sub Test()
Range("A1") = "True"
Debug.print(TypeName(Range("A1").Value))
Debug.print(TypeName(Range("A1").Text))
End Sub
This Output is:
Boolean
String

ListBox or Combobox ADODB imported Decimal items invisible in the list

I have encountered this problem several times already and have been able to work around it till now. Also the almighty search engines didn't help me.
The problem is that when I have populated a listbox or combobox from a ADODB recordset all Decimal data elements are not visible in the box, for example with the following (conn is a ADODB connection):
Private Sub GetFilteredRecords()
Dim strSQL As String
Dim arr As Variant
'create the SQL
strSQL = "SELECT * FROM vwStandard_Fee2"
'execute the SQL and fill the rs ( rsFiltered )
Set rsFiltered = conn.Execute(strSQL)
'Apply recordset to the listbox on the form
If Not (rsFiltered.EOF = True And rsFiltered.BOF = True) Then
arr = rsFiltered.GetRows()
With lbDeeper
.ColumnCount = rsFiltered.Fields.Count
.List = TransposeArray(arr)
End With
With cbDeeper
.ColumnCount = rsFiltered.Fields.Count
.List = TransposeArray(arr)
End With
End If
End Sub
Above contains 6 columns of Ids (all show Type = Variant/Decimal), of which the containing values are all not "shown" for some strange reason. Only the String and Date columns are shown normally, the Decimals are there but empty!
Here some snippets:
Now in case of a combo box I can get one column's value shown if their column the BoundColumn when I select that listitem, but only in the value fo the combobox (so still not in the list).
My initial workaround was to convert them into String values before adding to the Listbox/Combobox, in this case however I want to directly link the query result to the Box.List without looking at the details. And thus I am looking for a solution in stead of a work around.
In short: my numerical field items are invisible BY DEFAULT for some strange reason. Workaround was to make the items String values. I am now looking for a solution for this bug/problem instead:
What is causing this?
How to solve it?
So all string data is appearing? And,only numerics don't appear?
Then you may want to convert your numerics to strings and pass it to your list, combo boxes.
Which you have already done I noticed.
Now for any reason if your max number of rows and length of array/recorders row count doesn't match it could also cause an issue. However it seems your setting rows of combobox using recordset row count. Instead of using an array can you try to iterate over the recordset to populate the combobox? yes this is not performance friendly, buy guess what we need it to work without bugs before optimizing. ;-)
Have you bound your combobox to the recordset? Can you confirm if your array is single dimension and it has data to feed to the box?
You may try to populate the listbox using a saved query in the DB to if the issue still persists.
However, list boxes and combo boxes based on SQL statements are slower than
list boxes and combo boxes based on saved queries.
So can you try the following to set rowsource property? Make sure to test on both number,and test columns. As well as on old combo box and new one.
Rowsource->build query->
sqlview copy to rowsource property box->
delete or don't save that above built query since you already have SQL statement.
Just wanted you to try out possibilities to narrow down the issue.
UPDATING ANSWER WITH MOST POSSIBLE ISSUE AND SOLUTIONS
As per my comments, they mainly given assuming you had issues populating listbox/combobox
I forgot to ask something very very important, have you declared
Option Base 1 to make sure to avoid losing one of the array's column
values if you are dumping 2D array...? because you do not have any
explicit declartion for the array you are using to dump data into the
listbox.......... :)
Make sure your Listbox is enabled to show multi column data.
*So you have three choices, *
Option Base 1
ReDim your array and do looping to fill it and dump it into .list.
Since ReDim array need you to anyway loop through, you may just as well
use the recorset iself to add the data.
You seem to have a dimension issue with the array which is not declared but transposed from recordset and then to listbox/combobox. So your undeclared array is not populating multi-columns properly. That could be the reason it works when you declare array proeprly.......
Infact in your comment you have said so,
When I create an array in my code and populate it in my code (entry by
entry) it will show without any problem – K_B 14 mins ago
OK after going through various possible causes it seems to be the case that:
VBA has no Decimal Variant Type of its own.
VBA can handle Decimal from within a variable declared as Variant (thus becoming a Variant/Decimal)
This normally doesn't stop your program from working, but in Controls like Listbox and Combobox the Type Variant/Decimal is not interpretable and thus wont draw that specific entry.
For example populate a listbox called lbHigher with this:
Private Sub ListBoxProblem()
Dim tempArray(2, 2) As Variant
tempArray(0, 0) = "A"
tempArray(0, 1) = 1
tempArray(0, 2) = 1.1
tempArray(1, 0) = "B"
tempArray(1, 1) = CStr(CDec(5.2))
tempArray(1, 2) = 2.3
tempArray(2, 0) = "C"
tempArray(2, 1) = DateSerial(2012, 12, 13)
tempArray(2, 2) = 100
tempArray(3, 0) = "D"
tempArray(3, 1) = -1
tempArray(3, 2) = CDec(5.2)
lbHigher.ColumnCount = 3
lbHigher.List = tempArray
End Sub
Everything works fine except for the CDec(5.2). The CStr(CDec(5.2)) works fine as well as VBA will first have converted the Decimal to String before the Listbox gets to get it.
So either: Dont let the SQL generate any Decimal output OR convert any Decimal output to Single/Double/String/Integer/Long in VBA before handing it to the Listbox.

SSIS custom script: loop over columns to concatenate values

I'm trying to create a custom script in SSIS 2008 that will loop over the selected input columns and concatenate them so they can be used to create a SHA1 hash. I'm aware of the available custom components but I'm not able to install them on our system at work.
Whilst the example posed here appears to work fine http://www.sqlservercentral.com/articles/Integration+Services+(SSIS)/69766/ when I've tested this selected only a few and not all columns I get odd results. The script only seems to work if columns selected are in sequential order. Even when they are in order, after so many records or perhaps the next buffer different MD5 hashes are generated despite the rows being exactly the same throughout my test data.
I've tried to adapt the code from the previous link along with these articles but have had no joy thus far.
http://msdn.microsoft.com/en-us/library/ms136020.aspx
http://agilebi.com/jwelch/2007/06/03/xml-transformations-part-2/
As a starting point this works fine to display the column names that I have selected to be used as inputs
Public Overrides Sub Input0_ProcessInputRow(ByVal Row As Input0Buffer)
For Each inputColumn As IDTSInputColumn100 In Me.ComponentMetaData.InputCollection(0).InputColumnCollection
MsgBox(inputColumn.Name)
Next
End Sub
Building on this I try to get the values using the code below:
Public Overrides Sub Input0_ProcessInputRow(ByVal Row As Input0Buffer)
Dim column As IDTSInputColumn100
Dim rowType As Type = Row.GetType()
Dim columnValue As PropertyInfo
Dim testString As String = ""
For Each column In Me.ComponentMetaData.InputCollection(0).InputColumnCollection
columnValue = rowType.GetProperty(column.Name)
testString += columnValue.GetValue(Row, Nothing).ToString()
Next
MsgBox(testString)
End Sub
Unfortunately this does not work and I receive the following error:
I'm sure what I am trying to do is easily achievable though my limited knowledge of VB.net and in particular VB.net in SSIS, I'm struggling. I could define the column names individually as shown here http://timlaqua.com/2012/02/slowly-changing-dimensions-with-md5-hashes-in-ssis/ though I'd like to try out a dynamic method.
Your problem is trying to run ToString() on a NULL value from your database.
Try Convert.ToString(columnValue) instead, it just returns an empty string.
The input columns are not guaranteed to be in the same order each time. So you'll end up getting a different hash any time the metadata in the dataflow changes. I went through the same pain when writing exactly the same script.
Every answer on the net I've found states to build a custom component to be able to do this. No need. I relied on SSIS to generate the indexes to column names when it builds the base classes each time the script component is opened. The caveat is that any time the metadata of the data flow changes, the indexes may change and need to be updated by re-opening and closing the SSIS script component.
You will need to override ProcessInput() to get store a reference to PipelineBuffer, which isn't exposed in ProcessInputRow, where you actually need to use it to access the columns by their index rather than by name.
The list of names and associated indexes are stored in ComponentMetaData.InputCollection[0].InputColumnCollection, which needs to be iterated over and sorted to guarantee same HASH every time.
PS. I posted the answer last year but it vanished, probably because it was in C# rather than VB (kind of irrelevant in SSIS). You can find the code with all ugly details here https://gist.github.com/danieljarolim/e89ff5b41b12383c60c7#file-ssis_sha1-cs

Array in VB .Net

I have an array Newstr(20) When I fill it, I need to know how many of its indexes has been filled ? and find out the values.
I need to know how many of its indexes has been filled ?
Arrays don't keep that information. They only know how many spots you allocated. You have to track how many you assigned yourself. More than that, if you're working with a collection where you don't know how many items there will be, arrays are really the wrong choice in the first place. You should use a List(Of T) instead.
You could populate the array with a known string, then test for that string to see how many elements in your array are filled.
I would - however - suggest using an array list. You can get the number of elements added to the list from the Count property. This is the MSDN entry for Array Lists.
In order to find which of the elements have been filled, you can use a LINQ construction like this:
Dim input() = New String() {"abc", "def", "ghi", "", Nothing}
Dim output = input.Where(Function(i) Not String.IsNullOrEmpty(i)).ToArray
When you run this code, the output array will contain "abc", "def" and "ghi".
You can modify the selector of the Where to suit your preference if you're coding for a different type of array.
For instance the selector for Integer? will be:
input.Where(Function(i) (Not i Is Nothing) Or (i <> 0)).ToArray
Of course, you'll have to be coding in .NET 3.5+ in order to get access to LINQ.