Now all we need to do is setup the VB6 code that we will use for migration. To do that follow these steps:
1. On the Source Code Explorer toolback click on the Add Files button:
2. Click the Add Folder button and select the folder with your VB6 files
3. After you select the folder, a list of files found will be shown. Just remove any unneccesary files. For example files like MSSCCPRJ.SCC should be removed. And press OK
Now you have to commit your code the Source Code Repository
4. On the Source code Explorer Right click on Source Control Folder (for this example is MergeExample) and select Check In Pending Changes..
5. Write an apropiate comment and press Check In
To create a new Team System Project in Visual Studio 2005, follow these steps:
1. Go to the File Menu. Select New…, and in the New… submenu select Team Project… to start a wizard.
2. The “Specify the Team Project Settings” of the New Team Project wizard appears. Type the Team Project name. For example type MergeExample and click Next
3. In the following page, select the Process Template for the Team Project. We recommend using the Artinsoft Migration Methodology – v0.8 or later. Click Next.
4. In the following page, type the Title for the Team Project Portal. Type MergeExample. The Team Project Portal provides useful information about your project. Click Next
5. In the following page you select your source control settings.
Select the first option “Create an empty source control folder”. Click Next
6. In the following page a summary of your Team Project settings is presented. Click Finish
7. In the following page the a process bar appears while the project is created. Just wait until the process is done. It normally takes between 3 and 5 minutes.
This post will show you the necessary steps to configure Microsoft Visual Studio 2005 to use a Team System Project. In this example we will use a Team Project created to illustrate Continuous Migration with VBUC and MS Team System 2005, called MergeExample.
1. Start Microsoft Visual Studio 2005
2. Go to the File Menu\Open\Team Project…
3. Dialog is presented, where you can choose your Team Foundation Server, and the Team Project to use. For these Continuous Migration example we’ll assume a project called MergeExample.
4. The Team Explorer is shown, with your selected project.
Expand the elements for the Team Project you previously selected.
5. We now have to map our local directories to bind them to the
source control system. On the expanded tree, double click on the
Source Control Node
6. The Source Code Explorer is shown:
7. Go to the Workspaces dropdown box and select Workspaces…
8. The Manage workspace dialog is shown.
Select the appropiate workspace and click Edit…
9. On the Edit Workspace dialog type a description for your source control and select the source control Folder we had created (for this example it is $/MergeExample) and the directory for your code (for this example it is C:\MergeExample).
NOTE: if a message box about a pending update is shown, just click ok.
Now you have successfully mapped your directories, and you can start setting up your VB6 code for continuous migration
Someone recently made me remind an old technology called DDE.
“Dynamic Data Exchange (DDE) is a technology for communication between multiple applications under Microsoft Windows or OS/2”
“The primary function of DDE is to allow Windows applications to share data. For example, a cell in Microsoft Excel could be linked to a value in another application and when the value changed, it would be automatically updated in the Excel spreadsheet. The data communication was established by a simple, three-segment model. Each program was known to DDE by its "application" name. Each application could further organize information by groups known as "topic" and each topic could serve up individual pieces of data as an "item". For example, if a user wanted to pull a value from Microsoft Excel which was contained in a spreadsheet called "Sheet1" in the cell in the first row and first column, the application would be "Excel", the topic "Sheet1" and the item "r1c1".
Note: In DDE, the application, topic and item are not case-sensitive.”
So in VB6 you can have something like:
Private Sub Form_Load()
Text1.LinkMode = 0
Text1.LinkTopic = "Excel|Sheet1"
Text1.LinkItem = "R1C1"
Text1.LinkMode = 1
End Sub
How can you do that in .NET. Is it possible in C#? Well I started looking around and found several forums explaining about all the API calls and I was just about to write my own solution when I found NDDE. This project hosted in CodePlex “provides a convenient and easy way to integrate .NET applications with legacy applications that use Dynamic Data Exchange (DDE)” :)
So this is a nice example of how to do the previous lines in C#:
//This class provides the infraestructure for DDE comunication
NDde.Client.DdeClient ddeClient_TextBox1 = null;
private void Form1_Load(object sender, EventArgs e)
{
//I initialize the DDEClient object. Application is Excel and Topic is Sheet1. I'm using the
//the TextBox as the syncronization object
ddeClient_TextBox1 = new NDde.Client.DdeClient("Excel", "Sheet1", textBox1);
//Connect to the DDE Server
ddeClient_TextBox1.Connect();
//Start the Advise Loop
ddeClient_TextBox1.StartAdvise("R1C1", 1, true, 60000);
//Setup the Advise Method
ddeClient_TextBox1.Advise += new EventHandler<NDde.Client.DdeAdviseEventArgs>(ddeClient_TextBox1_Advise);
//Setup a method to Poke the Server for TextBox cahnges
textBox1.TextChanged += new EventHandler(textBox1_TextChanged);
}
void textBox1_TextChanged(object sender, EventArgs e)
{
//Syncronous Poking the server
ddeClient_TextBox1.Poke("R1C1", textBox1.Text + "\0", 4000);
}
const string DDE_postFix = "\r\n\0";
void ddeClient_TextBox1_Advise(object sender, NDde.Client.DdeAdviseEventArgs e)
{
//Advise only if needed
if (e.Text.Length >=DDE_postFix.Length && textBox1.Text + DDE_postFix != e.Text)
textBox1.Text = e.Text.Substring(0,e.Text.Length-3);
}
NOTE: Remember that you need to download NDDE and add a reference to this library
This is very good library, you can also set up a lot of Async calls to even improve performance. I have even thought of making an extender as the ToolTip control to add LinkTopic, LinkMode and LinkItem properties for Winforms controls or provide extensions methods to make all the syntax easier, but that is for a future post. Good Luck.
How can I migrate property pages? Well that is a common question when migrating VB6 Activex controls.
Property Pages where commonly used in VB6 to provide a mechanism for your user controls to edit values.
.NET provides even more mechanisms for editing your control properties. You can provide an editor for each one of your component properties or you can provide a ComponentEditor for all the component, this is very similar to the VB6 concept.
In .NET the ComponentEditor can be actived in the designer selecting Properties from the context menu when you right click over the control.
This is from the MSDN documentation:
“A component editor is used to edit a component as a whole and can be used to implement a
user interface similar to that of the property pages. You associate a component editor with a
component by using the EditorAttribute attribute.” From: ComponentEditor Class
The VBUC does not process out of the box, your PropertyPages, but I developed a tool that can be
used so the VBUC can help you migrate those property pages. This tool will modify your VB6 project,
and VB6 PropertyPages source code to make those VB6 PropertyPages look like VB6 UserControls.
This will allow the VBUC migration tool to recover some of the VB6 PropertyPages code and appearance
and with some manual changes you can get your property pages to work again.
Use the following link to downlaod the tool: DOWNLOAD TOOL
So these are the steps to migrate a VB6 Project that has Property Pages with the VB6.
1) Make a backup copy of your source code.
2) Run the TOOL with your project file. For example if your project file is Project1.vbp then run the tool like this:
FixPropertyPages Project1.vbp
This will generate a new VB6 Project file called ModifiedProject1.vbp
3) Open the VBUC, and migrate the new project file ModifiedProject1.vbp
4) Open the migrated solution in Visual Studio.
5) All your property pages will be migrated to .NET UserControls. You might need to go thru some changes to make them completely functional. Remeber to add the [ToolboxItem(false)] to these property pages because they do not need to be visible in your toolbox.
6) Now, to associate those property pages with your UserControl do this:
6.1) Add a new code file to your migrated solution. We are going to create a ComponentEditor, that will hold all the pages and associate that to the migrated control. Lets say the control is named Control1 and the property pages are PropertyPage1 and PropertyPage2.
We will call the ComponentEditor ComponentEditorToAssociatePagesForMyControl.
In this ComponentEditor we will add an internal class for each PropertyPage. This class will inherit from ComponentEditorPage. We will call this internal classes Page1, and Page2. And we will associate those classes with the ComponentEditorToAssociatePagesForMyControl in the GetComponentEditorPages().
The resulting code will be like:
C#using System.Windows.Forms.Design;
using WindowsFormsApplication1;
using System.Drawing;
using System.ComponentModel;
[ToolboxItem(false)]
public class ComponentEditorToAssociatePagesForMyControl : WindowsFormsComponentEditor
{
// Methods
public override bool EditComponent(ITypeDescriptorContext context, object component)
{
return false;
}
class Page1 : ComponentEditorPage
{
// Methods
public Page1()
{
PropertyPage1ForControl1 page1 = new PropertyPage1ForControl1();
Size mysize = new Size(400, 250);
this.Size = mysize;
this.Text = "Page 1 for Control1";
this.Controls.Add(page1);
}
protected override void LoadComponent() { }
protected override void SaveComponent() { }
}
class Page2 : ComponentEditorPage
{
// Methods
public Page2()
{
PropertyPage2ForControl1 page2 = new PropertyPage2ForControl1();
Size mysize = new Size(400, 250);
this.Size = mysize;
this.Text = "Page 2 for Control1";
this.Controls.Add(page2);
}
protected override void LoadComponent() { }
protected override void SaveComponent() { }
}
protected override System.Type[] GetComponentEditorPages()
{
return new System.Type[] { typeof(Page1),typeof(Page2) };
}
protected override int GetInitialComponentEditorPageIndex()
{
return 0;
}
}
VB.NET
<ToolboxItem(False)> _
Public Class ComponentEditorToAssociatePagesForMyControl
Inherits WindowsFormsComponentEditor
' Methods
Public Overrides Function EditComponent(ByVal context As ITypeDescriptorContext, ByVal component As Object) As Boolean
Return False
End Function
Protected Overrides Function GetComponentEditorPages() As Type()
Return New Type() { GetType(Page1), GetType(Page2) }
End Function
Protected Overrides Function GetInitialComponentEditorPageIndex() As Integer
Return 0
End Function
' Nested Types
Private Class Page1
Inherits ComponentEditorPage
' Methods
Public Sub New()
Dim page1 As New PropertyPage1ForControl1
Dim mysize As New Size(400, 250)
MyBase.Size = mysize
Me.Text = "Page 1 for Control1"
MyBase.Controls.Add(page1)
End Sub
Protected Overrides Sub LoadComponent()
End Sub
Protected Overrides Sub SaveComponent()
End Sub
End Class
Private Class Page2
Inherits ComponentEditorPage
' Methods
Public Sub New()
Dim page2 As New PropertyPage2ForControl1
Dim mysize As New Size(400, 250)
MyBase.Size = mysize
Me.Text = "Page 2 for Control1"
MyBase.Controls.Add(page2)
End Sub
Protected Overrides Sub LoadComponent()
End Sub
Protected Overrides Sub SaveComponent()
End Sub
End Class
End Class
7) After creating the ComponentEditor you must associate the component Editor to your new component editors. This can be done with something like:
C#
[Editor(typeof(ComponentEditorToAssociatePagesForMyControl), typeof(ComponentEditor))]
public class Control1 : UserControl
|
VB.NET
<Editor(GetType(ComponentEditorToAssociatePagesForMyControl), GetType(ComponentEditor))> _
Public Class Control1
|
8) Now to use this property pages, go to the designer screen and open the context menu and select properties. And editor with your properties pages will appear :)
9) You still need to write some code for saving the property values that is something you have to add to the LoadComponent and SaveComponent methods of the internal classes in your ComponentEditor (ComponentEditorToAssociatePagesForMyControl in our previous example).
I hope this helps to get your code faster in .NET. I'm attaching a C# sample if you want to try it out.
Some time ago Jose Aguilar had blogged about the Interesting Behavior of TabIndex in Migrated Applications. As he explained at the time there are functional differences between the TabIndex behaviour in VB6
If you look at Figure1.
Figure 1. This image show a VB6 form, the TabIndex values and the way the form navigates when you press Tab.
If you migrate that form with the VBUC and activate the TabOrder option in View\TabOrder you will see something like:
As you can see by the 0.1 and 0.3 and 5.4 and 5.2 values. TabOrder in .NET is hierarquical. When you press tab you will navigate to the next control in the container, and when you get to the last in that container then you will switch to the next one in the following container. This is different from the VB6 world when you would have switched from 0.1 to 5.2.
How can we fix this without a lot of manual corrections. Well you can override the ProcessTabKey method to navigate controls following the tabIndex without taking into account the containers.
The code you will need to add is:
/// <summary>
/// holds a list of controls for tab navigation
/// </summary>
List<Control> controls = new List<Control>();
/// <summary>
/// Populates the list used for tab navigation
/// </summary>
/// <param name="c">Control to use to populate list</param>
protected void BuildOrder(Control c)
{
if (c.TabStop)
controls.Add(c);
if (c.Controls.Count > 0)
{
foreach (Control child in c.Controls)
BuildOrder(child);
}
}
/// <summary>
/// Transversers all form controls to populate a list ordered by TabIndex
/// that will be used to follow tabindex ignoring containers
/// </summary>
protected void BuildOrder()
{
if (controls.Count == 0)
{
foreach (Control c in this.Controls)
{
BuildOrder(c);
}
controls.Sort(
delegate(Control c1, Control c2) { return c1.TabIndex.CompareTo(c2.TabIndex); });
}
}
/// <summary>
/// Overrides default tabIndex behaviour
/// </summary>
/// <param name="forward"></param>
/// <returns></returns>
protected override bool ProcessTabKey(bool forward)
{
BuildOrder();
if (ActiveControl != null)
{
int index = controls.IndexOf(ActiveControl);
if (index != -1)
{
if (forward)
controls[(index + 1) % controls.Count].Select();
else
controls[index==0?controls.Count-1:index-1].Select();
return true;
}
else
return false;
}
else
return base.ProcessTabKey(forward);
}
After adding this code just run your project and it will fix the tabIndex issues.
Well recently Kingsley has point me to a lot of useful links to improve the ExtendedWebBrowser. However he found another detail. When in Javascript you do something like a:
window.open(‘url’,’window’,’width=200;height=300’);
Those width and height settings were not being considered in the new window. I researched for I while until I found this great link:
So basicly I follow the sugested code and added logic in my EventSink class:
public void WindowSetLeft(int Left)
{
///Should I calculate any diff?
_Browser.Parent.Left = Left;
}
public void WindowSetTop(int Top)
{
_Browser.Parent.Top = Top;
}
public void WindowSetWidth(int Width)
{
int diff = 0;
diff = _Browser.Parent.Width - _Browser.Width;
_Browser.Parent.Width = diff + Width;
}
public void WindowSetHeight(int Height)
{
int diff = 0;
diff = _Browser.Parent.Height - _Browser.Height;
_Browser.Parent.Height = diff + Height;
}
So now when the window opens it takes the specified width, heigth, left and top.
As always
HERE IS THE UPDATED CODE
As vb6 migration experts in our company we deal everyday with a lot of issues around Interop and serialization.
One important thing to note is the concept of “Bittable Types”. I’m not making up terms. Those terms actually exist. Just see this link in MSDN.
In a few words, a bittable type is a type that has the same representation in managed and unmanaged code.
Why in earth is that important at all?
Because if you are calling that great C++ DLL implemented some years ago that just works ok, you won’t be able to pass a NON-Bittable type because that DLL will expect a binary representation different from that in the .NET virtual machine.
This is also an issue in other scenarios like:
- Serializing content to files
- Sending messages through messaging mechanisms like named-pipes or sockets.
Well, we have just introduced the problem so now let’s think on a nice solution for this problem.
Well Bittable Types are:
The following types from the System namespace are blittable types:
So now let’s look at a couple of non-BITTABLE types
DateTime
To test this differences let’s make a small test in VB6 and write a Date value to a file:
Private Sub SaveDateToFile()
Open "C:\test1.bin" For Binary Access Write As #1
Dim d1 As Date
d1 = "1/1/2009"
Put #1, , d1
Close #1
End Sub
Now let’s make a quick program in Vb.NET
Sub Main()
Dim f As System.IO.FileStream = System.IO.File.Open("C:\test2.bin", IO.FileMode.Create, IO.FileAccess.Write)
Dim fw As New System.IO.BinaryWriter(f)
Dim d As Date
d = Convert.ToDateTime("1/1/2009")
Dim val As Long = d.ToBinary()
fw.Write(val)
fw.Close()
Main2()
End Sub
If we compare these files we will have:
So the values are obviously different. This is because VB6 Date are stores with the OLE Automation DateFormat
So let’s change the C# code for something like:
Sub Main2()
Dim f As System.IO.FileStream = System.IO.File.Open("C:\test3.bin", IO.FileMode.Create, IO.FileAccess.Write)
Dim fw As New System.IO.BinaryWriter(f)
Dim d As Date
d = Convert.ToDateTime("1/1/2009")
fw.Write(d.ToOADate())
fw.Close()
End Sub
And now when we compare the files we will have:
So to make your Date values compatible with VB6 format you must user the DateTime method .ToOADate. Now if you are calling a DLL that expects a Date value in the same format used by VB6 then you will have to do this:
Dim d As Date
d = Convert.ToDateTime("1/1/2009")
Dim handle As System.Runtime.InteropServices.GCHandle = System.Runtime.InteropServices.GCHandle.Alloc(d.ToOADate(), Runtime.InteropServices.GCHandleType.Pinned)
Dim memory_address As IntPtr = handle.AddrOfPinnedObject()
Try
APICall(memory_address)
Finally
d = DateTime.FromOADate(System.Runtime.InteropServices.Marshal.ReadInt64(memory_address))
handle.Free()
End Try
String
Most of the time you wont have to deal with String marshalling because adding marshaling tags to your API call solves most of the problems, but if you arent that luckyly then you might do something like:
IntPtr ptrToStringVar = System.Runtime.InteropServices.Marshal.StringToHGlobalAnsi(strVar);
try
{
APICall(ptrToStringVar);
}
finally
{
strVar = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(ptrToStringVar);
System.Runtime.InteropServices.Marshal.FreeHGlobal(ptrToStringVar);
}
NOTE: if you have an API that might return an string with /0 characters you must call the API with System.Runtime.InteropServices.Marshal.PtrToStringAnsi(ptrToStringVar,size), if you do that the Framework will take in consideration the size bytes at the ptrToStringVar memory address.
Double and Singles
At least between VB6 and VB.NET the double and single types follows the same format. Well, at least, that is the result of my tests.
Try it yourself, the following shows a simple test for double variables:
VB6
Private Sub SaveDoubleToFile()
Open "C:\test1.bin" For Binary Access Write As #1
Dim d1 As Double
d1 = 1.123
Put #1, , d1
Close #1
End Sub
Sub Main()
SaveDoubleToFile
End Sub
.NET
Module Module1
Sub Main()
Dim f As System.IO.FileStream = System.IO.File.Open("C:\test2.bin", IO.FileMode.Create, IO.FileAccess.Write)
Dim fw As New System.IO.BinaryWriter(f)
Dim d As Double
d = 1.123
fw.Write(d)
fw.Close()
End Sub
End Module
So you could make an api call in those cases with something like:
Dim handle As System.Runtime.InteropServices.GCHandle = System.Runtime.InteropServices.GCHandle.Alloc(d, System.Runtime.InteropServices.GCHandleType.Pinned)
Dim ptr As System.IntPtr = handle.AddrOfPinnedObject()
Try
APICall(ptr)
Finally
handle.Free()
End Try
One of our clients wanted to change the CreateObject function migration for a function of their own. So they wanted all cases like:
Dim x As Object
Set x = CreateObject("Excel.Application")
To be migrated to something like:
Excel.Application x = (Excel.Application) Utils.MyCreateObject("Excel.Application", "");
Our migratio vb6migration tool provides a new cool feature called CustomMaps. This feature allows you to provide some simple but useful changes to the way things get migrated.
For this case follow these steps:
1. Open the Visual Basic Upgrade Companion.
2. In the Tools Menu choose:
3. Create a new CustomMaps File and an an entry like the following:
Notice the Source name is VBA.Interaction.CreateObject. To find out this name you can look in your VB6 IDE, right click on the CreateObject and select goto Definition.
and for the target name just put the implementation that you what, for example you can write a function like:
class Utils
{
public static object MyCreateObject(string className,params object[] ignoreRestParams)
{
return Activator.CreateInstance(Type.GetType(className));
}
}
and set the SourceName to Utils.MyCreateObject (or NameSpace.Utils.MyCreateObject to use the fully qualified name). You just need to set the New Reference Name column because we will not change the definition of the function.
If you have your ASP.NET application for example in c:\inetpub\wwwroot\WebApplication1 and you want to programmatically get that path just use something like:
string AppPath = Request.PhysicalApplicationPath;