title image

journal.nullschool.net


Archive for the 'Visual Basic' Category

LateCType: a strongly-typed latebound conversion operator

Thursday, August 4th, 2005

In my last post, I discussed how to use Microsoft.VisualBasic.CompilerServices.Conversions.ChangeType to perform latebound conversions. In this post, I will take its usage a step further and show how to define a strongly-typed but latebound conversion operator.

Conversion operators in VB (generally) perform conversions on compile-time types and give strongly-typed results. ChangeType , on the other hand, performs conversions on types determined at run-time and returns Object. If we blend these two concepts, we can achieve some surprisingly useful behavior:

Shared Function LateCType(Of T)(ByVal Value As Object) As T

Return CType(Conversions.ChangeType(Value, GetType(T)), T)

End Function

LateCType is a generic function that converts the parameter Value into an object of type T. The conversion is performed with ChangeType using T and Value’s type at run-time to resolve the conversion path. ChangeType returns an Object, so we must cast down to T to obtain a strongly-typed result. Because LateCType is generic, this one function can be used for any strongly-typed T that we require. For example:

Dim i As Integer = LateCType(Of Integer)(o)

Dim c As Class1 = LateCType(Of Class1)(o)

Dim q As List(Of String) = LateCType(Of List(Of String))(o)

In other words, this is a strongly-typed latebound conversion operator.

Why is this useful? Say I have a class ‘Base’ with a derived class ‘Derived’ and that both convert to String:

Class Base

Shared Narrowing Operator CType(ByVal Value As Base) As String

Return "mars"

End Operator

End Class

Class Derived : Inherits Base

Shared Narrowing Operator CType(ByVal Value As Derived) As String

Return "jupiter"

End Operator

End Class

Now if I have a variable of type Base and convert it to String using CType, class Base’s conversion operator will be called:

Dim b As Base = New Derived

Debug.WriteLine(CType(b, String))

This code snippet will print “mars” even though b contains an instance of Derived. Why? Because CType uses the source and target types as they are at compile time: Base and String. Therefore, it picks Base’s conversion operator. Using LateCType however, the results are different:

Debug.WriteLine(LateCType(Of String)(b))

This code snippet will print “jupiter” because b contains an instance of Derived at run-time, and it is this type which used by LateCType to resolve and perform the conversion. Note that the result is a strongly-typed String expression even though the conversion is latebound.

You might think the example above is not particularly compelling. I agree. So let’s now consider using LateCType in a different context. Generic type parameters seem to have very similar properties to LateCType: strongly-typed yet resolved at run-time. It’s no surprise then that LateCType is particularly well suited for doing conversions on generic type parameters.

For example, here’s a generic class which represents a space-delimited pair of values:

Public Class DelimitedPair(Of L, R)

Private m_Left As L

Private m_Right As R

Private Const Delimiter As Char = " "c

Shared Narrowing Operator CType(ByVal Value As String) As DelimitedPair(Of L, R)

'TODO: Error checking.

Dim Components As String() = Value.Split(Delimiter)

Dim Result As New DelimitedPair(Of L, R)

Result.m_Left = LateCType(Of L)(Components(0))

Result.m_Right = LateCType(Of R)(Components(1))

Return Result

End Operator

Shared Narrowing Operator CType(ByVal Value As DelimitedPair(Of L, R)) As String

'TODO: Error checking.

Return _

LateCType(Of String)(Value.m_Left) & _

Delimiter & _

LateCType(Of String)(Value.m_Right)

End Operator

Public Overrides Function ToString() As String

Return CStr(Me)

End Function

End Class

Notice the use of LateCType to cast to and from the type parameters L and R. Depending on the types used to instantiate this class, the appropriate conversion operators will be called (assuming of course that those types convert to/from String). For example:

Dim x As DelimitedPair(Of Double, Date)

x = CType("23.766 1/1/2005", DelimitedPair(Of Double, Date))

Debug.Print(CStr(x))

This outputs:

23.766 2005/01/01

So going back to the dataset example from the previous post, we can build columns with types such as DelimitedPair where the user-input will be validated and converted automatically even though the underlying types are quite complex:

Module Example1

Sub Main()

'First create a dataset to work with.

Dim Patients As New DataSet("Patients")

Dim ContactInfo As DataTable = Patients.Tables.Add("ContactInfo")

ContactInfo.Columns.Add("Age", GetType(Integer))

ContactInfo.Columns.Add("Weight", GetType(SqlTypes.SqlDouble))

ContactInfo.Columns.Add( _

"LastPayment", _

GetType(DelimitedPair(Of Date, Decimal)))

'Next, call a generalized function to add a row to the dataset.

AddRow(ContactInfo, "29", "63.4", "8/3/2005 1076.43")

'Lastly, print out the results.

For Each Value As Object In ContactInfo.Rows(0).ItemArray

Debug.WriteLine(Value.ToString & " : " & Value.GetType.ToString)

Next

End Sub

...

Running this code (see previous post) will give the result:

29 : System.Int32

63.4 : System.Data.SqlTypes.SqlDouble

2005/08/03 1076.43 : Example2+DelimitedPair`2[System.DateTime,System.Decimal]

Pretty cool. So the choice is up to us, the programmers, to use either early-bound or late-bound conversions where late-bound conversions seem particularly well suited for use on generic type parameters. Again, there is a performance penalty for doing late-bound conversions and you wouldn’t want to run them in a tight-loop. For user-driven input however, no problem.

Late-bound conversions with ChangeType

Friday, July 22nd, 2005

I was reading Paul Vick’s entries (1) (2) on dynamism, and it reminded me of a problem I encountered a short time ago.

Lately, I’ve been working with datasets and validation of user-supplied data. This data, entered as strings into bound controls, requires validation to make sure it converts to the underlying field types. Perhaps it’s my novice-level experience, but doing validation like this is a real pain.

The problem is that at some point the inputted string has to be converted to the underlying type U. To be robust, this conversion code requires as many unique code paths as there are possibilities for U. Looking only at the types allowed by the XSD designer, that’s 17 possibilities: Boolean, SByte, Byte, Short, UShort, Integer, UInteger, Long, ULong, Decimal, Single, Double, Date, Char, String, TimeSpan, and Guid. The corresponding conversion code paths need to be somewhere, whether written by hand or contained in a self-validating control. But consider that U could be one of the Sql types, such as SqlInt32, or one of the new Nullable types, such as Nullable(Of Integer), or even a user-defined type. The possibilities for U are theoretically limitless, and it’s unlikely that specialized, hand-written code or self-validating controls purchased from a vendor can handle data validation in these situations. Furthermore, I have yet to find self-validating controls that offer robust error handling capabilities, like the ability to display localized error messages or to throw richly-typed exceptions with detailed context information. In the end you’re left with the only option being a hand-written, and expensive, validation mechanism. So what to do?

There’s another place where these conversion code paths are written, and that’s in the Visual Basic runtime. Why not use them? They are contained in a “hidden” VB runtime function that can perform latebound conversions: Microsoft.VisualBasic.CompilerServices.Conversions.ChangeType. We will create generalized code using this function to do all type validation at runtime. In other words, we will defer all type validation to the latebinder. Consider the following code:

Module Example1
Sub Main()

'First create a dataset to work with.

Dim Patients As New DataSet("Patients")

Dim ContactInfo As DataTable = Patients.Tables.Add("ContactInfo")

ContactInfo.Columns.Add("Age", GetType(Integer))

ContactInfo.Columns.Add("Weight", GetType(SqlTypes.SqlDouble))

ContactInfo.Columns.Add("LastVisit", GetType(Nullable(Of Date)))

'Next, call a generalized function to add a row to the dataset.

AddRow(ContactInfo, "29", "63.4", "6/2/2005")

'Lastly, print out the results.

For Each Value As Object In ContactInfo.Rows(0).ItemArray

Debug.WriteLine(Value.ToString & " : " & Value.GetType.ToString)

Next

End Sub

Function AddRow( _

ByVal Table As DataTable, _

ByVal ParamArray Values As Object()) As DataRow

'This function adds any values to any dataset by performing

'the neccesary conversions to the underlying field types at runtime.

Dim Index As Integer = 0

Dim Count As Integer = Math.Min(Table.Columns.Count, Values.Length)

While Index < Count

'Convert each value to the underlying field type.

Values(Index) = _

CompilerServices.Conversions.ChangeType( _

Values(Index), _

Table.Columns(Index).DataType)

Index += 1

End While

'Add the row of converted values.

Return Table.Rows.Add(Values)

End Function

End Module

Compiling and running outputs:

29 : System.Int32

63.4 : System.Data.SqlTypes.SqlDouble

6/2/2005 12:00:00 AM : System.Nullable`1[System.DateTime]

This code creates a dataset with fields of type Integer, SqlDouble, and Nullable(Of Date). The user supplies the text “29″, “63.4″ and “6/2/2005″, and these strings are converted into the underlying types and added into the dataset. One function, AddRow, performs most of the work. It loops through each value, converting it to the appropriate column type. If the conversion fails an exception occurs. If it succeeds for all values, a row is inserted into the table with the correctly converted values. Notice that we can’t use a normal CType operator because it requires the type of the column to be known at compile-time. Also notice that column types such as SqlDouble and Nullable(Of Date) work correctly. These types rely on operator overloading for conversions, which means that Conversions.ChangeType is performing operator overload resolution at runtime and invoking the appropriate methods.

Actually, the .Net framework has a function which gets us partially towards a solution: System.Convert.ChangeType(value As Object, conversionType As System.Type). The documentation is quite vague, but this function also makes latebound conversions. Unfortunately, System.Convert.ChangeType supports only core conversions of the .Net world, such as along lines of inheritance and between intrinsics. It does not perform operator overload resolution and therefore does not offer a complete solution. For example, it could not handle SqlDouble or Nullable(Of Date) fields.

Why Visual Basic’s ChangeType is “hidden” in the CompilerServices namespace rather than exposed as a full fledged member of the language is an uninteresting story, but basically I lost that argument and it remains hidden and “unsupported”. Thankfully, it’s Public because the compiler needs to generate calls to it in certain scenarios (such as the copy-out conversion of an argument to a late-bound call), meaning it’s callable from user-code.

I’ve been reading in the documentation that handling the Format and Parse events of a Binding object allows custom data conversions between datasets and bound controls (see here if you have VS 2005 Beta2 installed). By using ChangeType, we need only one 1-line function to handle both of these events for all controls in the program. Add these lines to the end of Sub Main (assumes a form called InfoForm with a WeightTextBox control and a button control):

'Create a binding between the dataset and the TextBox control.

Dim InfoEntry As New InfoForm

Dim Binding As Windows.Forms.Binding = _

New Windows.Forms.Binding("Text", Patients, "ContactInfo.Weight")

'Hook-up the binding events with a generalized converter function.

AddHandler Binding.Format, AddressOf Convert

AddHandler Binding.Parse, AddressOf Convert

InfoEntry.WeightTextBox.DataBindings.Add(Binding)

'Display the form.

System.Windows.Forms.Application.Run(InfoEntry)

Now add this sub:

Sub Convert( _

ByVal Sender As Object, _

ByVal Cevent As Windows.Forms.ConvertEventArgs)

Debug.WriteLine("convert to " & Cevent.DesiredType.ToString)

'Use a latebound conversion to convert the incoming value into

'any desired type.

Cevent.Value = _

CompilerServices.Conversions.ChangeType( _

Cevent.Value, _

Cevent.DesiredType)

End Sub

That’s it. By using the example function above, any control can be bound to any data column (assuming the type conversion succeeds of course!). Run the code and enter some values in the textbox. You should get output similar to:

convert to System.String

convert to System.String

convert to System.Data.SqlTypes.SqlDouble

convert to System.String

convert to System.Data.SqlTypes.SqlDouble

convert to System.String

I only wish I was using Visual Studio 2005 at work. Code like this would make my life so much easier. Three words of caution:

  1. ChangeType only does type validation, not value validation such as checking for negative weight or other invalid values. You still have to write or purchase this code.
  2. ChangeType costs execution time because it is latebound. But keep in mind that we’re validating user-entered code here. The cost of calling ChangeType is miniscule compared to the time it takes the user to lift their finger from the Enter key.
  3. ChangeType is supposed to be “hidden”. It is not a first-class member of the Visual Basic language.

In my next post I will take the usage of ChangeType further and show how it can be applied to Generic type parameters to provide even more generalization.

useful IIf for Visual Basic 2005

Wednesday, June 8th, 2005

The IIf statement in Visual Basic is a pain when Option Strict is on. The reason is that the return value is Object and thus requires a cast back to the target type:

    Dim a As Integer = IIf(SomeCondition(), 10, 20)
    Error: Option Strict On disallows implicit conversions from 'System.Object' to 'Boolean'.

To fix the compile error, you need to add a cast:

    Dim a As Integer = CInt(IIf(SomeCondition(), 10, 20))

Even worse, use an enum:

    Dim b As Color = CType(IIf(SomeCondition(), Color.Red, Color.Blue), Color)

But by using generic methods and type inferencing, you can create a version of IIf that eliminates the need for the cast:

    Function IIf(Of T)(ByVal Expression As Boolean, ByVal TruePart As T, ByVal FalsePart As T) As T
        If Expression Then Return TruePart Else Return FalsePart
    End Function

Then the above examples could be written as:

    Dim a As Integer = IIf(SomeCondition(), 10, 20)
    Dim b As Color = IIf(SomeCondition(), Color.Red, Color.Blue)

Much nicer. And also much faster because we avoid boxing the parameters and unboxing the result. Now if only IIf could be massaged into a real ternary operator…