Support Statement: Upgrading a COM Component property to a WinForms Static property

Overview

This is an example of a COM upgrade.  In this case we want to upgrade SysInfoLib.SysInfo.ScrollBarSize to System.Windows.Forms.SystemInformation.VerticalScrollBarWidth.

Suppose your VB6 has this:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
SysInfo control on a Form
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
>1301    Begin SysInfoLib.SysInfo SysInfo1 
 1302       Left            =   13920
 1303       Top             =   0
 1304       _ExtentX        =   1005
 1305       _ExtentY        =   1005
 1306       _Version        =   393216
 1307    End
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Logic to use SysInfo control to get ScrollBarSizwe
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 8014 Private Function XSizePartsUpDown()
...
>8020         .Width = SysInfo1.ScrollBarSize
...
 8024     End With
 8025 End Function

And suppose the desired translation should use System.Windows.Forms.SystemInformation.VerticalScrollBarWidth instead.  

We wish to change a control instance property declared in a COM interface description file (IDF) to a static readonly property declared in a .NET framework class.

Here is the property in the COM IDF
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
proj\idf\FromIdl\SYSINFO.OCX.xml
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   17    <class id="ISysInfo" parent="IDispatch">
...
>  26       <property id="ScrollBarSize" type="Single" status="Out"/>
...
   32    </class>

And, here is the replacement property in WinForms:

      //
      // Summary:
      //     Gets the default width, in pixels, of the vertical scroll bar.
      //
      // Returns:
      //     The default width, in pixels, of the vertical scroll bar.
      public static int VerticalScrollBarWidth { get; }

We want the migration to simplify how the application gets ScrollBarSize information.  This will require several steps:

1) Remove the SysInfo library reference form the csproj file.
2) Remove all references to control class instances.
3) Replace ScrollBarSize property references with System.Windows.Forms.SystemInformation.VerticalScrollBarWidth
4) Avoid unnecessary casts and numeric conversions.

The following sections describe the step by step process to implement the migration.

Create a COM RefactorLibrary file

The recommended way to migrate a COM API is to use a RefactorLibrary file. A RefactorLibrary is a set of migration rules that direct how the translator interprets and rewrites code relating to a COM API. Here is the stub RefactorLibrary file to get us started:

   <RefactorLibrary>
   <Refactor id="[SYSINFO.OCX]">
   ... rules will go here ...
   </Refactor>
   </RefactorLibrary>

The translator can load RefactorLibraries automatically based on their name (mig.SysInfo.ocx.xml) or explicitly using a registry-migfile command. I prefer the explicit approach as it provides more more control and better documentation. The recommended convention for a RefactorLibrary file name is libfile.description.Refactor.xml. In this case, the file name will be named SysInfo.ocx.WinForms.Refactor.xml and its location will be in the proj\usr folder.

Activate the RefactorLibrary.

The following command tells the translator to load the RefactorLibrary whenever it detects a translation using SysInfo.ocx.

<Registry type="MigFile" Source="SYSINFO.OCX" Target="SYSINFO.OCX.WinForms.Refactor" />

This registry command must be in effect before the SysInfo.ocx reference is detected by the translation process. Typically this will be done by placing it in your translation script before the <Compile> command.

References to SysInfo.ocx cause gmStudio to load the SysInfo.ocx.xml interface description. If there is a MigFile registered, then gmStudio will also load the RefactorLibrary and modify the SysInfo API description information.  Here is an excerpt from the translation log:

Loading reference:[SYSINFO.OCX] ...\proj\idf\FromIdl\SYSINFO.OCX.xml
Loading reference:[SYSINFO.OCX.WinForms.Refactor] ...\proj\usr\SYSINFO.OCX.WinForms.Refactor.xml

Add migration rules to the RefactorLibrary

The migration details are specified by adding rules to the RefactorLibrary.

proj\usr\SYSINFO.OCX.WinForms.Refactor.xml

<Refactor id="[SYSINFO.OCX]">
   
Rule 1)   <Migrate location="DoNotDeclace" libType="Internal" /> 

Rule 2)   <Migrate id="SysInfo" role="define" migName="SysInfo" />
   
Rule 3)   <Migrate id="ISysInfo.ScrollBarSize" type="Integer" refType="Method" migPattern="System.Windows.Forms.SystemInformation.VerticalScrollBarWidth" nPram="1"/>
      
Rule 4)   <migClass id="NetControl.SysInfo" migName="Remove.SysInfo" parent="SysInfo">
              <!-- Suppress designer code -->
          </migClass>
</Refactor>

Rule 1) The location="DoNotDeclace" and libType="Internal" rules suppress the COM API reference in the .NET project file. The migName may be modified to simplify removing the Control from the designer code later.

Rule 2) The role="define" with a migName tells the tool to treat instances of SysInfo declared in the form property bag as simple declarations. This suppresses adding the instances to the form's Control collection in Designer code.

Rule 3) This tells the tool to migrate references to property ScrollBarSize as System.Windows.Forms.SystemInformation.VerticalScrollBarWidth:

   NOT THAT: .Width = SysInfo1.ScrollBarSize
   BUT THIS: .Width = System.Windows.Forms.SystemInformation.VerticalScrollBarWidth   

There are three parts to this Rule 3 and all of the attributes of the migrate command are needed to make this transformation precisely.

1) change "ScrollBarSize" to "System.Windows.Forms.SystemInformation.VerticalScrollBarWidth" (migPattern = "...")
2) remove "SysInfo1." (nPram='1' and refType='method')
3) change the type from Single to Integer (type="Integer")

Rule 4) A netControl migClass is used to specify the properties to author fro initializing each control instance in Designer code. An empty migClass suppresses this authoring process.

Clean Up

With the above rules, the translation is nearly complete. However there is still a problem:

ERRNUM: CS0246: The type or namespace name 'SysInfoLib' could not be found.
    private SysInfoLib.SysInfo SysInfo1;
...
    this.SysInfo1 = new SysInfoLib.SysInfo();

Recall, we removed the library reference so this namespace is no longer available. These declarations are no longer needed and they must be removed. This can be done using and Author/Fix:

<Author>
   <Fix host="%SrcName%" name="PostEdit">
   <Replace status="active" name="Remove SysInfoLib Type">
      <OldBlock><![CDATA[SysInfoLib]]></OldBlock>
   </Replace>
   </Fix>
</Author>

A migName (e.g. migName="Remove.SysInfoLib") can make this edit more precise by giving SysInfoLib a unique name in the translations

Consolidating the migration rules with a ScriptRules file

The SysInfoLib.SysInfo.ScrollBarSize migration has several moving parts, and the recommended approach is to organize them with a ScriptRules file:

proj\usr\SysInfo.ocx.Rules.xml

<ScriptRules>
<!--
Description: ScriptRules to migrate SyssInfoLib to .NET
-->
<ScriptRule id="SYSINFO.OCX" Condition="%TaskTag%=~(upg)">

<Option Condition="%SrcName%=~'... list of projects that should use this migration ...'">
   <Registry type="MigFile" Source="SYSINFO.OCX" Target="SYSINFO.OCX.WinForms.Refactor" />
</Option>

<Author Condition="%SrcName%=~'... list of projects that should use this migration ...'">
   <Fix host="%SrcName%" name="PostEdit">
   <Replace status="active" name="Remove SysInfoLib Type">
      <OldBlock><![CDATA[Remove.SysInfoLib]]></OldBlock>
   </Replace>
   </Fix>
</Author>

</ScriptRule>
</ScriptRules>

proj\usr\SYSINFO.OCX.WinForms.Refactor.xml

<RefactorLibrary>
<!-- 
Description: Map SYSINFO.OCX to .NET 
1) Remove the library reference
2) Remove all references to control instances
3) Replace property reference with call to System.Windows.Forms.SystemInformation.VerticalScrollBarWidth
-->
<Refactor id="[SYSINFO.OCX]">
   
   <Migrate location="DoNotDeclace" libType="Internal" migName="Remove.SysInfoLib"/> 

   <Migrate id="SysInfo" role="define" migName="SysInfo" />
   
   <Migrate id="ISysInfo.ScrollBarSize" type="Integer" refType="Method" migPattern="System.Windows.Forms.SystemInformation.VerticalScrollBarWidth" nPram="1"/>
      
   <migClass id="NetControl.SysInfo" migName="SysInfo" parent="SysInfo">
   </migClass>

</Refactor>
</RefactorLibrary>

These two files can be applied to your translation with a ScriptRule command in the translation script:

<gmBasic>
...
<ScriptRule id="SYSINFO.OCX" FileName="..\usr\SysInfo.ocx.Rules.xml" />

<Compile Project="%SrcPath%" Resx="%ResxFolder%"/>

...

Alternative Approach: Stub Implementation

The example above is know as COM replacement and it involves reworking the application code.  An alternative COM upgrade strategy is Stub Implementation. In this approach, the upgrade team implements the interface defined by the COM Stub Framework files generated for the COM API based on usage in the application. For example, the stub file generated for this discussion looks like this:

proj\deploy\SysInfoLib\SysInfoLib.cs (generated by the GlobalStubs process during an Incremental Upgrade)

using System;
using VBNET = Microsoft.VisualBasic;

namespace SysInfoLib
{
   public class SysInfo : System.Windows.Forms.Control
   {
      public float ScrollBarSize
      {
         get
         {
            if (System.Diagnostics.Process.GetCurrentProcess().ProcessName != "devenv") throw new System.NotImplementedException("STUB_TODO: stubbed SysInfoLib.ISysInfo.ScrollBarSize.Get");
            return 0.0F;
         }
      }
   }
...
}

Notice that the default stub file is generated to allow a build in the early stages of a project before you have settled the upgrade details for your COM APIs.  Notices that the default stubs will throw a NotImplemented exception – unless you are just looking at the code in the forms VS Designer.

With the stub implementation strategy, the upgrade team can can simply implement the body of the stub with code providing the expected service. In this case, a reasonable implementation is very simple:

NOT THAT: if (System.Diagnostics.Process.GetCurrentProcess().ProcessName != "devenv") throw new System.NotImplementedException("STUB_TODO: stubbed SysInfoLib.ISysInfo.ScrollBarSize.Get");
BUT THIS: return System.Windows.Forms.SystemInformation.VerticalScrollBarWidth;

The edit can may be done with a Fix@FileFilter command at the end of the GlobalStubs script:

proj\usr\GlobalStubs.xml

<gmBasic>
<!--
Description: gmStudio Template Translation Script for generating GlobalStub framework projects
-->

<GlobalStubs>
...
</GlobalStubs>

<Fix FileFilter="%WorkFolder%\PartsWatch-GlobalStubs-%TaskTag%-csh.bnd" name="FixFile">
   <Replace name="Update stub framework csproj files to reference gmRTL Assembly; gcd only">
   <OldBlock><![CDATA[
            if (System.Diagnostics.Process.GetCurrentProcess().ProcessName != "devenv") throw new System.NotImplementedException("STUB_TODO: stubbed SysInfoLib.ISysInfo.ScrollBarSize.Get");
            return 0.0F;
   ]]></OldBlock>
   <NewBlock><![CDATA[
            return System.Windows.Forms.SystemInformation.VerticalScrollBarWidth;
   ]]></NewBlock>
   </Replace>
</Fix>

In this example, the stub implementation is much easier than the COM replacement.  In many cases, stub implementation will provide time savings during the project, but it may have a higher cost of ownership if the resulting application code does not follow accepted coding standards.  The decision for choosing a COM upgrade strategy should be handled on a case by case basis.  

See also:

Custom COM Replacement