A .NET library to access Windows Vista and Windows 7 features

clock September 16, 2009 21:58

One of the reasons to migrate your code to .NET is create modern user interface that take advantage of the power of newer versions of Windows. On the other hand, the .NET Framework has been designed to work in the same way over a wide range of Windows versions, including versions that are about ten years old, and therefore it doesn't provide access to the most intriguing features of Vista and Windows 7.

The solution comes in a new library from Microsoft, named Windows API Code Pack for .NET Framework. Version 1.0 of this great tool has been just released, and already implements an impressive range of Windows features:

  • Windows 7 Taskbar Jump Lists, Icon Overlay, Progress Bar, Tabbed Thumbnails, and Thumbnail Toolbars.
  • Windows 7 Libraries, Known Folders, non-file system containers.
  • Windows Shell Search API support, a hierarchy of Shell Namespace entities, and Drag and Drop functionality for Shell Objects.
  • Explorer Browser Control.
  • Shell property system.
  • Windows Vista and Windows 7 Common File Dialogs, including custom controls.
  • Windows Vista and Windows 7 Task Dialogs.
  • Direct3D 11.0, Direct3D 10.1/10.0, DXGI 1.0/1.1, Direct2D 1.0, DirectWrite, Windows Imaging Component (WIC) APIs. (DirectWrite and WIC have partial support)
  • Sensor Platform APIs
  • Extended Linguistic Services APIs
  • Power Management APIs
  • Application Restart and Recovery APIs
  • Network List Manager APIs
  • Command Link control and System defined Shell icons.
  • Shell Search API support.
  • Drag and Drop functionality for Shell objects.
  • Support for Direct3D and Direct2D interoperability.
  • Support for Typography and Font enumeration DirectWrite APIs. 

The library comes with an extensive help file and all code samples are available in both VB.NET and C#. Last but not the least, the entire source code is provided.

A very subtle COM Interop bug

clock September 3, 2009 09:13

After migrating a piece of VB6 code to VB.NET, one of our customers found himself facing a weird problem. After stripping down all unneeded code, all boiled down to these statements

   ' open an ADODB connection and then a recordset
   Dim cn As New ADODB.Connection
   Dim rs As New ADODB.Recordset
"SELECT * FROM Products", cn, _

   ' insert a new record
   cn.Execute("INSERT Orders (OrderId) VALUES (1234)") ' <<< error!

This code - which works beautifully under VB6 - stops with the following error when it runs under VB.NET

fhloinsert error=Transaction cannot have multiple recordset with this cursor type. Change the cursor type, commit the transaction, or close one of the recordsets.

According to this Microsoft KB Article, this error message appears because a ForwardOnly-Readonly cursor can't live in a transaction with multiple recorsets. The problem is, the cn.Execute method doesn't create a FO-RO recordset (or any kind of recordset, for that matter). The ADODB documentation is clear on this point and, in fact, the VB6 code had worked fine for years.

When you see problems like this - a piece of VB6 code that accesses COM objects and that stops working when translated to VB.NET - then nearly always the problem lies in COM Interop.

It turned out that the cn.Execute does create a hidden recordset under .NET. The following code snippet proves this point:

   Dim rs As ADODB.Recordset = cn.Execute("INSERT Orders (OrderId) VALUES (1234)")
   Debug.WriteLine(rs IsNot Nothing)     ' <<< display "True"

Fortunately, the Execute method supports an option that allows you to specify that the SQL command doesn't produce a resultset. So you can avoid this very subtle problem by means of a minor fix to the original VB6 code or the migrated VB.NET code:

   cn.Execute("INSERT Orders (OrderId) VALUES (1234)", , ADODB.ExecuteOptionEnum.adExecuteNoRecords)

Notice that it is recommended that you *always* specify the adExecuteNoRecords in Execute methods that do not return a Recordset, even if your code doesn't live inside a transaction and therefore doesn't raise a runtime error. By doing so you avoid that a Recordset be created and later destroyed, which makes your code slightly faster.

Specifying the adExecuteNoRecords option was good VB6 programming practice that turns to be even better when you migrate the code to VB.NET.

How-to modify references to elements of a control array

clock September 3, 2009 04:43

VB.NET doesn't support control array, therefore both the Upgrade Wizard and VB Migration Partner generate one standard control whose name is obtained by appending the index to the control array name.

For example, if you have a control array named txtFields, then the txtFields(0) element  is converted into a control named txtFields_000, the txtFields(1) element is converted into a control named txtFields_001, and so forth. However, when the control is referenced in code, the VB6-like syntax is preserved. In our example, the generated code might contain elements like txtFields(0).ForeColor rather than txtFields_001.ForeColor. This is intentional, because it's the only way to create a control reference when the index is an expressoion rather than a constant.

One of our customers, however, asked whether it would be possible to generate code that references the actual .NET control whenever possibile (in other words, txtFields_000.ForeColor rather than txtFields(0).ForeColor). Personally I think that these references might be disorienting and prefer the kind of code that VB Migration Partner generates. At any rate, the answer is yes, VB Migration Partner allows you to tweak the generated code by means of the PostProcess pragma. This is how to achieve the desired effect:

'## PostProcess "txtFields\((?<index>\d)\)", "txtFields_00${index}"
'## PostProcess "txtFields\((?<index>\d\d)\)", "txtFields_0${index}"

where the second pragma is only necessary if the txtFields control array contains elements whose index is 10 or higher. These pragmas should be placed at the top of the form where the control array resides and have no effect if the control is accessed from another form.

If the form contains two or more control arrays, you still need only two pragmas. For example, if the form contains three control arrays named txtFields, lblFields, and chkOptions, you can fix control references with constant indexes using these pragmas:

'## PostProcess "(?<arr>txtFields|lblFields|chkOptions)\((?<index>\d)\)", "${arr}_00${index}"
'## PostProcess "(?<arr>txtFields|lblFields|chkOptions)\((?<index>\d\d)\)", "${arr}_0${index}"

Pay attention to orphaned ADODB objects

clock September 2, 2009 01:26

Don't take me wrong: COM Interop is a great piece of technology, and Microsoft developers made wonders with it. If you consider how different the COM and .NET worlds are, it's a miracle that they can communicate with each other in such a smooth way.

Actually, COM Interop is such a magic that it's easy to forget that all the COM objects that we manipulate from .NET aren't the "real" COM objects. Instead, they are .NET wrappers that redirect all calls to the actual COM object by means of COM Interop. This fact has many implications. Consider for example this VB6 code:

Function GetRecordCount(ByVal cn As ADODB.Connection, ByVal sql As String) As Integer
   Dim rs As New ADODB.Recordset
   rs.Open sql, cn
   GetRecordCount = rs.RecordCount
End Function

As you see, the Recordset object isn't closed explicitly, but it isn't a problem in VB6 because all COM objects are automatically closed when they are set to Nothing or go out of scope. However, if you translate this code to VB.NET you are in trouble, because nothing happens when the method exits. In other words, you'll have an open recordset hanging somewhere in memory and cause unexplainable errors later in the application. For example, you'll get an error when you'll later try to open another recordset on the same connection.

We know that some customers had to work around this issue by enabling the MARS option with SQL Server, which enables multiple active recordsets on any given connection. This might be a viable solution if you are working with SQL Server 2005 or 2008, but VB Migration Partner offers a better way to handle this case: use the AutoDispose pragmas.

In fact, when this pragma is used, all disposable objects are correctly disposed of when they go out of scope. In this case, VB Migration Partner emits the following code:

Function GetRecordCount(ByVal cn As ADODB.Connection, ByVal sql As String) As Integer
   Dim rs As New ADODB.Recordset
      rs.Open(sql, cn)
      GetRecordCount = rs.RecordCount
   End Try
End Function

where the SetNothing6 helper method takes care of orderly invoking the Close or Dispose method, if necessary.