GetType does not exist a Reflection puzzle in VB - vb.net

This is a real basic schoolboy level error but I've not figured it out yet despite having used a fair amount of reflection in my generic updater class.
The puzzle is as follows: I want to get the type of a value that I've extracted in the code sample. Using that type I wish to compare it with another extracted value. To compare it correctly with Option Strict On I must directCast the extracted value to the type that the PropertyInfo says it is.
The code below simply illustrates my using it in Directcast or Ctype versions.
For Each p As PropertyInfo In _validProperties
Dim NewValue = p.GetValue(UpdateItem)
Dim x = CType(NewValue, p.PropertyType) 'Error p.PropertyType does not exist --How?
'Elided
Next
'Alternative approach
For Each p As PropertyInfo In _validProperties
Dim NewValue = p.GetValue(UpdateItem)
Dim x = DirectCast(NewValue, p.PropertyType) 'Error p.PropertyType does not exist --How?
'Elided
Next
I've tried explicitly putting the newly discovered type in it's own variable and it does exist.
This is an odd little trap I've had a few times and usually solved but this one is not budging. Therefore I'm fundementally misunderstanding how I need to use both the instance x.Gettype (OR GetType(Class) syntaxes).
Whatever I do, p.PropertyType does not exist.
Can anyone clear up my foggy mind on this please?
Edit: For now, Option Strict is off merely to allow this to work.
For Each p In _validProperties
Dim NewValue = p.GetValue(UpdateItem), OriginalValue = p.GetValue(originalItem)
If (p.PropertyType.Name.Contains("Nullable") AndAlso Not Nullable.Equals(NewValue, OriginalValue)) _
OrElse NewValue <> OriginalValue Then p.SetValue(originalItem, NewValue)
Next
All my tests pass so I'll live with it for now. I simply don't want the update to fire if there is no difference between the values on the properties in question.

Related

VB.Net - Can you access the expected data type within a function?

I was wondering if there is any way to access the expected data type within a function similar to an event arg. I am doubtful that this is possible, though it would be an excellent feature.
I frequently work with (old and disorganized)Mysql databases creating interfaces through VB.Net. Often I will have an optional field which contains a NULL value in the database. I am frequently dealing with errors due to NULL and dbnull values in passing data to and from the database.
To complicate things, I often am dealing with unexpected datatypes. I might have an integer zero, a double zero, an empty string, or a string zero.
So I spend a fair amount of code checking that each entry is of the expected type and or converting NULLs to zeros or empty strings depending on the case. I have written a function ncc(null catch convert) to speed up this process.
Public Function ncc(obj As Object, tp As Type) As Object 'Null Catch Convert Function...
My function works great, but I have to manually set the type every time I call the function. It would be so much easier if it were possible to access the expected type of the expression. Here is an example of what I mean.
Dim table as datatable
adapter.fill(table)
dim strinfo as string
dim intinfo as long
strinfo = ncc(table.Rows(0).Item(0),gettype(String)) 'here a string is expected
intinfo = ncc(table.Rows(0).Item(0),gettype(Long)) 'here a long is expected
It would be so much more efficient if it were possible to access the expected type directly from the function.
Something like this would be great:
Public Function ncc(obj As Object, optional tp As Type = nothing) As Object
If tp Is Nothing Then tp = gettype(ncc.expectedtype)
That way I do not have to hard code the type on each line.
strinfo = ncc(table.Rows(0).Item(0))
You can make the ncc function generic to simplify calling it:
Public Function ncc(Of T)(obj As T) As T
If DbNull.Value.Equals(obj) Then Return Nothing
Return Obj
End Function
This kind of function will be able to in some cases infer the type, but if there's any possibility of null you'll still want to include a type name (because DBNull will be the inferred type for those values). The advantage is not needing to call gettype() and so gaining a small degree of type safety:
strinfo = ncc(Of String)(table.Rows(0).Item(0))
But I think this has a small chance to blow up at run time if your argument is not implicitly convertible to the desired type. What you should be doing is adding functions to accept a full row and return a composed type. These functions can exist as static/shared members of the target type:
Shared Function FromDataRow(IDataRow row) As MyObject
And you call it for each row like this:
Dim record As MyObject = MyObject.FromDataRow(table.Rows(i))
But, you problem still exists.
What happens if the column in the database row is null?
then you DO NOT get a data type!
Worse yet? Assume the data column is null, do you want to return null into that variable anyway?
Why not specify a value FOR WHEN its null.
You can use "gettype" on the passed value, but if the data base column is null, then you can't determine the type, and you right back to having to type out the type you want as the 2nd parameter.
You could however, adopt a nz() function (like in VBA/Access).
So, this might be better:
Public Function ncc(obj As Object, Optional nullv As Object = Nothing) As Object
If obj Is Nothing OrElse IsDBNull(obj) Then
Return nullv
End If
Return obj
End Function
So, I don't care if the database column is null, or a number, for such numbers, I want 0.
So
dim MyInt as integer
Dim MyDouble As Double
MyInt = ncc(rstData.Rows(0).Item("ContactID"), 0)
MyDouble = ncc(rstData.Rows(0).Item("ContactID"), 0)
dim strAddress as string = ""
strAddress = ncc(rstData.Rows(0).Item("Address"), "")
Since in NEAR ALL cases, you need to deal with the null from the DB, then above not only works for all data types, but also gets you on the fly conversion.
I mean, you CAN declare variables such as integer to allow null values.
eg:
dim myIntValue as integer?
But, I not sure above would create more problems than it solves.
So,
You can't get exactly what you want, because a function never has knowledge of how it's going to be used. It's not guaranteed that it will be on the right-hand side of an assignment statement.
If you want to have knowledge of both sides, you either need to be assigning to a custom type (so that you can overload the assignment operator) or you need to use a Sub instead of an assignment.
You could do something like this (untested):
Public Sub Assign(Of T)(ByVal field As Object, ByRef destination As T,
Optional ByVal nullDefault As T = Nothing)
If TypeOf field Is DBNull Then
destination = nullDefault
Else
destination = CType(field, T)
End If
End Sub
I haven't tested this, so I'm not completely certain that the compiler would allow the conversion, but I think it would because field is type Object. Note that this would yield a runtime error if field is not convertible to T.
You could even consider putting on a constraint requiring T to be a value type, though I don't think that would be likely to work because you probably need to handling String which is a reference type (even though it basically acts like a value type).
Because the destination is an argument, you wouldn't ever need to specify the generic type argument, it would be inferred.

VBA - CallByName won't accept variant arguments

Solution: Just put brackets around Value in the CallByName statement to force evaluation of it.
Ex. CallByName MobClass, TargetData, vbLet, (Value)
Credit goes to Rory from the other post, which I will probably be deleting since it is no longer relevant and a possible duplicate.
I've spent a long time trying to figure out what was wrong with how I was using CallByName. I finally realized that its fourth argument (Args) will throw a type mismatch if the input is not either EXACTLY the same type as the input argument of what its calling or its hard-coded in.
(I don't even understand how, or why, it does this since VarType(Variant/Integer) = VarType(Integer))
So I either need a way to make it accept variant inputs or convert variables from Variant/Integer to Integer (or create a new variable) without a giant select case.
Edit: So my question wasn't clear so I'll explain it in more detail. I have a bunch of classes that I want to cycle through and call the Let property on. My simplified setup is:
Dim AllClasses as Collection
Sub SetAll(TargetProperty as String, Value as Variant)
For each ClassX in AllClasses
CallByName ClassX, TargetProperty, vbLet, Value
Next ClassX
End Sub
The problem is Value when it is initialized as Variant. The only time I can get it to not throw a type mismatch exception is when I initialize Value as the exact same type that the property wants, but I can't do that since the data types in the class vary.
Edit 2: I'm going to ask another question about the whole problem since no one seems to know much about CallByName
Edit 3: Here's a summary of what we have so far:
CallByName's fourth argument (Args) throws a type mismatch when trying to call the Let property on a class.
This only happens when the value trying to be assigned is stored in a Variant data type. It works perfectly if the variable is initialized to the same type the Let property is expecting OR if the value is hard-coded into the argument.
The Let property works fine on its own. It accepts Variant data types just fine.
My question is: Is there a way to stop this exception? I'm creating another post about other possible solutions to my overall problem.
The Problem is that you pass the properties-arguments by reference not by value, but you can't pass a reference to a different datatype (variant -> long) as the types don't match and it can't be converted as this would change the data type in the caller too. By using brackets, you force the argument to be passed by value and it can be casted as typeLong.
You can avoid this by using ByValin theProperty Letterinstead ofByRef(what is implicit used if not set). You are aware that by referencing a variable, changes made in the property change the callers value too?
Example:
Class PassExample
'Save as class module PassExample
Public Property Let PropByVal(ByVal NewVal As Long)
NewVal = 1
End Property
Public Property Let PropByRef(ByRef NewVal As Long)
NewVal = 1
End Property
Module with test sub:
'save as standard module
Sub test()
Dim v As Variant
v = 0
Dim ExampleInstance As New PassExample
CallByName ExampleInstance, "PropByVal", VbLet, v 'this works
CallByName ExampleInstance, "PropByRef", VbLet, v 'type mismatch
CallByName ExampleInstance, "PropByRef", VbLet, (v) 'this works as ByRef is changed to byval
Debug.Print v ' shows 0, not 1 as it should be if referenced
CallByName ExampleInstance, "PropByRef", VbLet, CVar(v) ' works too as it passes a function-result that can't be referenced
End Sub
Thanks to Rory and chris neilsen for providing the solution!

Cell ColumnType is NULL using Smartsheet API

I'm trying to update my SmartSheet API from v1 to v2 and am having some difficulty on the code below.
The code returns rows for the selected sheet, however the "ColumnType" property of all the Cell's within the rows are NULL.
I know to return this you have to specify it as an inclusion - which I believe I have.
Dim sheet As Sheet = smartsheet.SheetResources.GetSheet(curSheet.Id, New RowInclusion() {RowInclusion.COLUMN_TYPE, RowInclusion.COLUMNS}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing)
For Each Row As Row In sheet.Rows
If Row.ParentRowNumber Is Nothing Then
Dim i As Integer = 0
Dim colType As ColumnType
If Not Row.Cells(i).ColumnType = ColumnType.TEXT_NUMBER Then
'Do some stuff here...
End if
Next
Any help would be great.
Thanks,
Steve
The short answer is just get the latest SDK from https://github.com/smartsheet-platform/smartsheet-csharp-sdk/pull/60 and update your GetSheet to the following:
Dim sheet As Sheet = client.SheetResources.GetSheet(SHEETID, New SheetLevelInclusion() {SheetLevelInclusion.COLUMN_TYPE}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing)
Notice the use of SheetLevelInclusion rather than RowInclusion. You should be all set.
If you care about the details, the longer answer is... The GetSheet method doesn't accept an array/IEnumerable of RowInclusion as the second argument. It expects an array/IEnumerable of SheetLevelExclusion. In C#, the same invocation call would fail as C# imposes stricter type checking on the generic type parameter of IEnumerable. However, due to Visual Basic's leniency around implicit conversions between Enum types and its lenient conversions for arrays (and similar types like IEnumerable) it is possible to invoke a function with the "wrong" type of argument when the argument is an array/IEnumerable and the elements are Enums. In this case, Visual Basic is actually converting the RowInclusion values to their underlying numeric value (Enum is always implicitly or explicitly backed by an underlying numeric type) and converting those values to the SheetLevelExclusion value corresponding to the same underlying numeric value so that it can invoke the GetSheet method.
The other complication here is that the SDK didn't have COLUMN_TYPE as an available SheetLevelExclusion value. So the pull request/branch I linked to above adds that. In my simple test here that made it work.

Linq ToList does nothing

I have Option Strict and Option Infer both set "On".
This code works fine:
Dim tBoxes = From t In MainForm.Frame2.Controls.OfType(Of TextBox).ToList
tBoxes.ToList().ForEach(Sub(c) c.DataBindings.Clear())
Why can't I combine them into the one line below (I believe it's related to the fact that the first line above does not set tBoxes to a list but remains an IEnumerable even though I am calling ToList, why is this?)
Dim tBoxes = From t In MainForm.Frame2.Controls.OfType(Of TextBox).ToList.ForEach(Sub(c) c.DataBindings.Clear())
This code results in an error
Expression does not produce a value
This might seem like much ado about nothing but it's not just the reduction to one line, I'd like to understand what's going on here.
VB.NET 2010
The problem is not the ToList call, but List.ForEach Method which is Sub, hence does not have a result and cannot be assigned to a variable.
If you want to use a single line, remove Dim tBoxes =.
Update In fact there is another problem in the above code.
Dim tBoxes = From t In MainForm.Frame2.Controls.OfType(Of TextBox).ToList
is equivalent to
Dim tBoxList = MainForm.Frame2.Controls.OfType(Of TextBox).ToList
Dim tBoxes = From t in tBoxList
so obviously tBoxes is IEnumerable<TextBox>.
Since the from t In .. part is unnecessary in this case, the "oneliner" should be something like this
MainForm.Frame2.Controls.OfType(Of TextBox).ToList.ForEach(Sub(c) c.DataBindings.Clear())
If you really need a query part, to avoid such confusions, don't forget to enclose it in (..) before calling ToList or other methods like Count, Any etc., like this
(from t In MainForm.Frame2.Controls.OfType(Of TextBox)).ToList.ForEach(Sub(c) c.DataBindings.Clear())
Small description but enough to understand
From t In MainForm.Frame2.Controls.OfType(Of TextBox) 'Filter all object of type text box
.ToList 'Convert IEnemerable(Of TextBox) to a IList type.
.ForEach(Sub(c) c.DataBindings.Clear())' Iterate through list and remove bindg of each text box
Issue is that .ForEach does not return any value so that there is nothing to assign the tBoxes object that you have created. It is just like a void method or Sub in VB.net.

Function in VB that doesn't have a return type

I am not much familiar with Visual Basic 6.0 and am not having a VB compiler installed, but I was looking at some VB code for some debugging and saw this:
Private Function IsFieldDeleted(oLayoutField As Object)
Dim oColl As Collection
Set oColl = GetFieldIdsForField(oLayoutField)
IsFieldDeleted = (oColl.Count = 0)
Set oColl = Nothing
End Function
In other functions I see they define the return type with an "As" for example "As Boolean" but this one does not have an "As" :D and then how they have used it is like this:
If Not IsFieldDeleted(oRptField.GetUCMRLayoutField) Then
Call oCollection.Add(oRptField, oRptField.ObjectKeyString)
Call AddToNewLineSeperatedString(sCaseFldDescMsg, oFld.FieldDescription)
End If
How is this working? Is it just like rewriting it and saying that the function returns an integer and compare the return type to be either 0 or 1? Or are there some other hidden tips in there?
When no type is specified, in VB.NET it assumes Object for the return type. In VB6, it assumes Variant. In VB.NET you can make things much more obvious by turning Option Strict On, but I don't believe that option was available in VB6.
The value that is returned, in reality, is still typed as a Boolean, but you are viewing the returned value as a Variant. So, to do it "properly", you really ought to cast the return value like this:
If Not CBool(IsFieldDeleted(oRptField.GetUCMRLayoutField)) Then
....
End If
Calling CBool casts the value to a Boolean instead of a Variant. This is unnecessary, though, since VB will use late-binding to determine the type of the return value is a boolean.
The best thing to do in this case is to change the function to As Boolean. Doing so will not break any existing code since that's all it ever returned anyway. However, if it's a public member in a DLL, that would break compatibility.