vb.netreflectionformat

Cannot use Format on reflected object's property ToString


I can format an Int32, I1, as currency - it displays: I1 = $31,742.

But I cannot format an Int32 property of a class whose PropertyInfo and whose value and whose format is determined by reflection. Inside GetPropertyString and inside debugger, Value has a .GetType of Int32, which is correct. But, I get an exception on the line "Dim Value2 As String = Value.ToString(HandlerFormatProvider)". The exception is:

System.InvalidCastException
  HResult=0x80004002
  Message=Conversion from string "C0" to type 'Integer' is not valid.
  Source=Microsoft.VisualBasic
  StackTrace:
   at Microsoft.VisualBasic.CompilerServices.Conversions.ToInteger(String Value)
   at PropertyInfoType.Module1.GetPropertyString[T](T TObject) in C:\Data\VBS.NET\PropertyInfoType\PropertyInfoType\Module1.vb:line 36
   at PropertyInfoType.Module1.Main() in C:\Data\VBS.NET\PropertyInfoType\PropertyInfoType\Module1.vb:line 9

I am very confused as to why, based on the exception, that statement appears to be trying to convert "C0" to an Int32??? The program that replicates the problem is as follows:

How do I format the reflected property value with a reflected format string? Obviously, I want to use the IPropertyHandler Interrface on many classes.

Module Module1

   Sub Main()
      Dim I1 As Int32 = 31742
      Dim P1 As String = "C0"
      Console.WriteLine("I1 = " & I1.ToString(P1) & ".")

      Dim F1 As Foo = New Foo(4217)
      Dim PropertyHandlerValue As String = GetPropertyString(Of Foo)(F1)
      Console.WriteLine("PropertyHandlerValue = " & PropertyHandlerValue.ToString & ".")

      Console.ReadLine()
   End Sub

   Public Function GetPropertyString(Of T)(ByVal TObject As T) As String
      GetPropertyString = Nothing
      If TypeOf TObject Is IPropertyHandler Then
         Dim SourceObject As IPropertyHandler = CType(TObject, IPropertyHandler)

         Dim HandlerPropertyName As String = SourceObject.PropertyHandlerName
         Dim HandlerFormatProvider As String = SourceObject.PropertyHandlerFormatProvider

         Dim SourcePropertyInfo As System.Reflection.PropertyInfo = GetType(T).GetProperty(HandlerPropertyName)

         If SourcePropertyInfo IsNot Nothing Then
            Dim SourcePropertyValue As Object = SourcePropertyInfo.GetValue(SourceObject)
            If String.IsNullOrEmpty(HandlerFormatProvider) Then
               GetPropertyString = SourcePropertyValue.ToString
            Else
               Dim Value As Object = System.Convert.ChangeType(SourcePropertyValue, SourcePropertyInfo.PropertyType)
               Dim Value2 As String = Value.ToString(HandlerFormatProvider)
               GetPropertyString = Value2
            End If
         End If
      Else
         Throw New System.NotImplementedException("The TObject passed in does not implement IPropertyHandler.")
      End If
   End Function

   Public Interface IPropertyHandler
      ReadOnly Property PropertyHandlerName As String
      ReadOnly Property PropertyHandlerFormatProvider As String
   End Interface

   Public Class Foo
      Implements IPropertyHandler
      Private m_SomeInt32 As Int32 = 0
      Private m_PropertyHandlerName As String = "SomeInt32"
      Private m_PropertyHandlerFormatProvider As String = "C0"
      'Private m_PropertyHandlerFormatProvider As String = ""
      Public Sub New(ByVal SomeInt32 As Int32)
         m_SomeInt32 = SomeInt32
      End Sub
      Public Property SomeInt32 As Int32
         Get
            Return m_SomeInt32
         End Get
         Set(value As Int32)
            m_SomeInt32 = value
         End Set
      End Property
      Public ReadOnly Property PropertyHandlerName As String Implements IPropertyHandler.PropertyHandlerName
         Get
            Return m_PropertyHandlerName
         End Get
      End Property

      Public ReadOnly Property PropertyHandlerFormatProvider As String Implements IPropertyHandler.PropertyHandlerFormatProvider
         Get
            Return m_PropertyHandlerFormatProvider
         End Get
      End Property
   End Class

End Module

Solution

  • It seems as though you are trying to check whether SourcePropertyValue has a ToString() method that supports customized formatting, and if so, use it. But if not, use the default ToString(). If that's what you are doing, the correct way to do it in .NET is to check to see whether SourcePropertyValue implements IFormattable:

    The IFormattable interface converts an object to its string representation based on a format string and a format provider.

    All .NET primitive types (other that string) implement this interface, so if you rewrite your code to use the interface when possible, your code should work correctly without needing to worry about runtime errors due to late binding failures:

    If SourcePropertyInfo IsNot Nothing Then
        Dim SourcePropertyValue As Object = SourcePropertyInfo.GetValue(SourceObject)
        Dim FormattableValue as IFormattable = TryCast(SourcePropertyValue, IFormattable)
        If FormattableValue Is Nothing Or String.IsNullOrEmpty(HandlerFormatProvider) Then
            GetPropertyString = IF(SourcePropertyValue, "").ToString()
        Else
            GetPropertyString = FormattableValue.ToString(HandlerFormatProvider, CultureInfo.CurrentCulture)
        End If
    End If
    

    Demo fiddle #1 here.


    As an aside, you seem to have made your debugging more difficult by not setting Option Strict On. If you were to have enabled it, you would have seen that the following line:

    Dim Value2 As String = Value.ToString(HandlerFormatProvider)
    

    has a compilation error:

    Option Strict On disallows implicit conversions from 'String' to 'Integer'.
    

    The compilation error is... not very helpful, however Value is declared to be of type System.Object whose ToString() method does not have an overload that takes a format string -- so I would expect to see some sort of compilation error there.

    Demo fiddle #2 here.

    The fact that the code fails to compile in strict mode means that its runtime behavior in non-strict mode will depend on the vagaries of VB.NET's late binding logic. Sometimes that works nicely for you, but sometimes not. And when it doesn't work, it can be very hard to reason out why; your code is a good example of this very problem, I'm still not sure exactly why you are getting the precise exception you see.

    For comparison, my fixed code above compiles with Option Strict On so will have no unexpected late-binding surprises.

    So, unless you need late binding for some reason (e.g. because you are working with COM objects), I'd recommend to always enable Option Strict On by default precisely because it allows you to find errors at compile time rather than runtime. And the earlier you find an error, the cheaper it will be to fix. For further discussion, see:


    Update I was eventually able to understand why you were getting the Conversion from string "C0" to type 'Integer' is not valid. error when using Option Strict Off. If I change the call to ToString() to pass in a CultureInfo like so:

    Dim Value2 As String = Value.ToString(HandlerFormatProvider, CultureInfo.CurrentCulture)
    

    Then I get a compilation error even with Option Strict Off:

     Too many arguments to 'Public Overloads ReadOnly Default Property Chars(index As Integer) As Char'.
    

    What that tells me is that VB.NET's late-binding code interpreted

    Value.ToString(HandlerFormatProvider)
    

    To be

    Value.ToString()(Convert.ToInt32(HandlerFormatProvider))
    

    I.e. a call to Object.ToString() to get a string, then an index into the string to get a specific character at the index given by HandlerFormatProvider when cast to an integer. Clearly not what you were hoping, which seems to have been CType(Value, Int32).ToString(HandlerFormatProvider). Just goes to show why Option Strict On can be easier to work with despite being, well, strict.

    Demo fiddle #3 here.