For VB6 applications it is common to rely on OS or Kernel API Calls. Some of those APIs might
need you to send data back and for the native API.
Marshalling in .NET can be complicated and bothersome. I have published several posts about
interop. But it usually depends on adding several marshalling attributes and even tricks specially for
fixed strings.
So I decided to provide a more a simpler approach for conversion. In this approach you just need to things:
1. Your VB6 types or structs will be mapped to .NET classes
2. All VB6 type or struct fields will be mapped to public fields
3. An attribute must be used on those fields to indicate the field length, for arrays or strings.
4. Extension methods .AsString() .SetFromString and .GetClassLength will handle all the complexities of setting the struct fields.
Let’s see an example:
Type EmployeeRecord
FirstName As String * 5
LastName As String * 5
End Type
That vb6 type will be mapped in .NET following this approach to:
public class EmployeeRecord
{
[FixedLength(5)]
public string FirstName = "Mau";
[FixedLength(5)]
public string LastName = "Rojas";
}
You can then simple use that class in .NET
var emp = new EmployeeRecord {FirstName="Mauricio",LastName="Rojas"} ;
var str = emp.AsString();
//This str value will be "MauriRojas" the helper extension methods
// .AsString and .SetFromString will handle setting the internal class fields
All that is very good but how is this used in Marshalling?? Well very simple. Let’s say you have a Dll called foo.dll
with a function foo that receives an EmployeeRecord:
[DllImport("foo.dll")]
public static extern int foo(IntPtr Struct);
Then if you want to call that function you will do something like:
var emp = new EmployeeRecord { FirstName="Ann",LastName="Smith"};
string str = emp.AsString();
var ptr = IntPtr.Zero;
ptr = Marshal.StringToBSTR(str);
//or
ptr = Marshal.StringToHGlobalAnsi(str);
//or
ptr = Marshal.StringToHGlobalAuto(str);
//or
ptr = Marshal.StringToHGlobalUni(str);
//And call the native function
foo(ptr);
If the function modifies the structure and you want to reflect those changes then you will do something like:
str = Marshal.PtrToStringAnsi(ptr,typeof(EmployeeRecord).GetClassLength())
emp.SetFromString(str);
This solution can also be applied for more complex structures. For example:
public class EmployeeRecord
{
[FixedLength(5)]
public string FirstName = "Mau";
[FixedLength(5)]
public string LastName = "Rojas";
}
public class Record1
{
public int field1;
[FixedLength(10)]
public string field2 = "";
public EmployeeRecord rec = new EmployeeRecord();
}
public class GeneralInfo
{
public int field1;
[ArrayLength(5)]
[FixedLength(2)]
public String[] countrycodes = { "cr","es","mx","pa","ni"};
[FixedLength(2)]
public EmployeeRecord[] employees;
}
If you want to try it out this is the link to the CODE
If you were in VB6 HelpContextID will be familiar for you (http://msdn.microsoft.com/en-us/library/aa267690(v=vs.60).aspx). In those sweet VB6 days all you had to do was:
Private Sub Form_Load ()
App.HelpFile = "VB.HLP"
Frame1.HelpContextID = 21004
Text1.HelpContextID = 21001
Form1.HelpContextID = 21005
End Sub
And each time you pressed the F1 button your application will have opened the .hlp file and show you the Help Topic corresponding to that ID. After migration from VB6 to WinForms Help you now have the HelpProvider.SetHelpKeyword http://msdn.microsoft.com/en-us/library/system.windows.forms.helpprovider.sethelpkeyword.aspx And you had to do something like:
internal System.Windows.Forms.HelpProvider HelpProvider1;
...
HelpProvider1.HelpNamespace = "sample.chm";
HelpProvider1.SetHelpKeyword(TextBox1, "1007.html");
HelpProvider1.SetHelpNavigator(TextBox1, HelpNavigator.Topic);
HelpProvider1.SetHelpKeyword(ListBox1, "1006.html");
HelpProvider1.SetHelpNavigator(ListBox1, HelpNavigator.Topic);
And all that seems nice. But, what can you do when you cross over to SilverlightjQuery15205164761650376022_1357918518660? Well, in general there are several systems that allow you to author your help files in html or convert your .hlp or .chm files to html, but how do you link your components to that help system in order to provide context-sensitive help???? Ok. So one of the possible solutions is very very simple. In general, the solution that I will show in this post is this: 1) First implement an attached property for adding a HelpKeyword to Silverlight components 2) Set the helpkeyword in the desired components 3) Provide logic that will open the appropiate help file. Ok. So let's implement a Silverlight Attached property. An attached propery is like adding a new property to your controls. This new attached property will be called Helpkeyword
using System;
using System.Windows.Shapes;
namespace System.Windows.Controls
{
public class HelpProvider
{
public static readonly DependencyProperty HelpKeyword =
DependencyProperty.RegisterAttached("HelpKeyword", typeof(string), typeof(HelpProvider), new PropertyMetadata(null));
public static void SetHelpKeyword(UIElement element, string keyword)
{
element.SetValue(HelpKeyword, keyword);
}
public static string GetHelpKeyword(UIElement element)
{
return (string)element.GetValue(HelpKeyword);
}
}
}
Ok. So once we have the attached property we have to use it, and set it on the code: To set it on the code we must add a namespace:
<UserControl x:Class="SilverlightApplication.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
...
xmlns:help="clr-namespace:System.Windows.Controls"
mc:Ignorable="d"
....>
And apply the attribute to components
<Button help:HelpProvider.HelpKeyword="helpforbutton1" Content="Button" ... />
<TextBox help:HelpProvider.HelpKeyword="helpfortext1" Height="47" ... />
So that almost everything, now we just need to trigger the appropiate logic, to do that we will add a KeyUp handler to the top most element, in this example a grid. NOTE: if Silverlight is running on the browser F1 is not an option. I just used F2 here as an example.
<Grid x:Name="LayoutRoot" Background="White" Height="205" KeyUp="LayoutRoot_KeyUp">
<Button help:HelpProvider.HelpKeyword="helpforbutton1" ... />
<TextBox help:HelpProvider.HelpKeyword="helpfortext1" ... />
</Grid>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Browser;
namespace SilverlightApplication
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
}
private void LayoutRoot_KeyUp(object sender, KeyEventArgs e)
{
//check for the specific key. For now use F2 as the Help Shortcut
if (e.Key==Key.F2) {
var uielement = FocusManager.GetFocusedElement() as UIElement;
if (uielement!=null)
{
var keyword = HelpProvider.GetHelpKeyword(uielement);
var host = HtmlPage.Document.DocumentUri.Host;
var port = HtmlPage.Document.DocumentUri.Port;
var url = string.Format("http://{0}:{1}/help/{2}.html", host,port,keyword);
HtmlPage.Window.Navigate(new Uri(url),"_blank");
}
} // else ignore the keystroke
}
}
}
This property can be used on the IDE:
On code
var uielement = FocusManager.GetFocusedElement() as UIElement;
if (uielement!=null) {
var keyword = HelpProvider.GetHelpKeyword(uielement);
}
This is an image of the application running.
And you can download the code from: CODE
If you have any questions or would like more info on Silverlight migration check www.silverlightmigration.com
In VB6 when you have an ActiveX Library it was very important to use
the BinaryCompatibility setting to make sure that your applications did not break after a change.
So let’s first introduce what is binary compatibility and how to accomplish that in .NET.
Binary Compatibility allows to make changes to your components or COM classes without recompiling
every application you've made that uses the component.
And why do you need it. Why compatibility breaks.
On lets see.
An ActiveX Control or DLL expose Public interfaces.
Those interfaces have all of the properties, methods, events, etc. that you've marked as Public.
In other words, everything you've added that shows in Intellisense while working outside of your component.
Now let's say you have create a class, with two Methods Method1 and Method2
When you compile, VB generates all the COM infraestructure you need for your component.
It defines a CoClass and an interface and an entry for each method.
For a vb class with two methods:
Sub Method1()
End Sub
Sub Method2()
End Sub
It will produce a typelib like:
// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: <could not determine filename>
[
uuid(8ABA2C0C-7CCA-40CD-A944-56707566634A),
version(1.0)
]
library Project1
{
// TLib : // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
importlib("stdole2.tlb");
// Forward declare all types defined in this typelib
interface _Class1;
[
odl,
uuid(6B86684C-B3DD-4680-BF95-8DEE2C17AF5B),
version(1.0),
hidden,
dual,
nonextensible,
oleautomation
]
interface _Class1 : IDispatch {
[id(0x60030000)]
HRESULT Method1();
[id(0x60030001)]
HRESULT Method2();
};
[
uuid(C71C7AB0-552A-4D5D-A9FB-AF33830A697E),
version(1.0)
]
coclass Class1 {
[default] interface _Class1;
};
};
As you can see in the typelib there are IDs associated to each coclass, interface and
methods. Those IDs are the ones use when you generate the .exe file for your application.
Now if you modify your Class to:
Sub Method3()
End Sub
Sub Method4()
End Sub
Sub Method1()
End Sub
Sub Method2()
End Sub
and you use No Compatibility the typelib after your changes will be:
// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: <could not determine filename>
[
uuid(FE5C56C2-E03A-4DC0-994D-B68543C72A46),
version(1.0)
]
library Project1
{
// TLib : // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
importlib("stdole2.tlb");
// Forward declare all types defined in this typelib
interface _Class1;
[
odl,
uuid(A3032E1E-52FE-42E0-98FF-84A9DD4FD8C3),
version(1.0),
hidden,
dual,
nonextensible,
oleautomation
]
interface _Class1 : IDispatch {
[id(0x60030000)]
HRESULT Method3();
[id(0x60030001)]
HRESULT Method4();
[id(0x60030002)]
HRESULT Method1();
[id(0x60030003)]
HRESULT Method2();
};
[
uuid(72721504-CC56-4BB9-9447-C7193FE8C02D),
version(1.0)
]
coclass Class1 {
[default] interface _Class1;
};
};
As you can see, now the ids for the methods, CoClass are different, so your applications will return errors like: Error 430 (Automation error, the component dies horribly) or Error 429 (can't create the object at all)
But if you instead used BinaryCompatibility then the typelib for your class will be:
// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: <could not determine filename>
[
uuid(8ABA2C0C-7CCA-40CD-A944-56707566634A),
version(1.1)
]
library Project1
{
// TLib : // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
importlib("stdole2.tlb");
// Forward declare all types defined in this typelib
interface _Class1;
[
odl,
uuid(6E9C59C3-82D7-444C-92FB-01B49D91A2FF),
version(1.1),
hidden,
dual,
nonextensible,
oleautomation
]
interface _Class1 : IDispatch {
[id(0x60030002)]
HRESULT Method3();
[id(0x60030003)]
HRESULT Method4();
[id(0x60030000)]
HRESULT Method1();
[id(0x60030001)]
HRESULT Method2();
};
[
uuid(C71C7AB0-552A-4D5D-A9FB-AF33830A697E),
version(1.1)
]
coclass Class1 {
[default] interface _Class1;
};
typedef [uuid(6B86684C-B3DD-4680-BF95-8DEE2C17AF5B), version(1.0), public]
_Class1 Class1___v0;
};
If you compare now the two typelibs you can see the Method1 and Method2 keep the same ids.
For each version a typedef is generated that will point to the last version. For example adding a Method5 will add new entry like:
typedef [uuid(6B86684C-B3DD-4680-BF95-8DEE2C17AF5B), version(1.0), public]
_Class1 Class1___v0;
typedef [uuid(6E9C59C3-82D7-444C-92FB-01B49D91A2FF), version(1.1), public]
_Class1 Class1___v1;
Well that is what binary compatibility does. Now how to achieve binary compatibility in .NET
Binary Compatibility in .NET
Achieving binary compatibility in .NET is really easy. You just need to give more information to
make explicit how your typelib information will be. I will follow an approach as the one I already explained in this post:
http://blogs.artinsoft.net/mrojas/archive/2010/06/23/exposing-c-classes-thru-interop.aspx
Lets take our previous example:
using System;
using System.Runtime.InteropServices;
namespace InteropExamples
{
public class Class1
{
public void Method3()
{
}
public void Method4()
{
}
public void Method1()
{
}
public void Method2()
{
}
public void Method5()
{
}
}
}
In previous posts I had recommended using partial classes and using interfaces to explicitly specify what you what to be seen in COM. This means you start up with something like:
public partial class Class1
{
public void Method3()
{
}
public void Method4()
{
}
public void Method1()
{
}
public void Method2()
{
}
}
[ComVisible(true)]
public interface _Class1
{
void Method3();
void Method4();
void Method1();
void Method2();
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(_Class1))]
partial class Class1 : _Class1
{
#region _Class1 Members
void _Class1.Method3()
{
Method3();
}
void _Class1.Method4()
{
Method4();
}
void _Class1.Method1()
{
Method1();
}
void _Class1.Method2()
{
Method2();
}
#endregion
}
Now to make this code binary compatible then you have to make sure that the tlb file generated for your class is almost identical to that generated before. To acomplish that we must make sure that we your methods, interfaces and classes have the same guids and ids. Lets see how:
using System;
using System.Runtime.InteropServices;
namespace InteropExamples
{
public partial class Class1
{
public void Method3()
{
System.Windows.Forms.MessageBox.Show("3 N");
}
public void Method4()
{
System.Windows.Forms.MessageBox.Show("4 N");
}
public void Method5()
{
System.Windows.Forms.MessageBox.Show("5 N");
}
public void Method1()
{
System.Windows.Forms.MessageBox.Show("1 N");
}
public void Method2()
{
System.Windows.Forms.MessageBox.Show("2 N");
}
}
[ComVisible(true)] //This to make the interface Visible for COM
[TypeLibType((TypeLibTypeFlags)((short)TypeLibTypeFlags.FHidden |
(short)TypeLibTypeFlags.FDual |
(short)TypeLibTypeFlags.FNonExtensible |
(short)TypeLibTypeFlags.FOleAutomation))] //This to use the same flags as in previous tlb
[Guid("9BAFD76D-8E6B-439C-8B6D-37260BFA3317")] //This is to make the class have the guid
public interface _Class1
{
[DispId(0x60030000)]
void Method1();
[DispId(0x60030001)]
void Method2();
[DispId(0x60030002)]
void Method3();
[DispId(0x60030003)]
void Method4();
[DispId(0x60030004)]
void Method5();
}
[ComVisible(true)] //This to make the class Visible for COM
[ClassInterface(ClassInterfaceType.None)] //This is to make sure that we have control on interface generation
[ComDefaultInterface(typeof(_Class1))] //To set default interface
[ProgId("Project1.Class1")] //To set ProgId
[Guid("C71C7AB0-552A-4D5D-A9FB-AF33830A697E")] //Maintain same Guid.
partial class Class1 : _Class1, Class1___v0, Class1___v1
{
#region _Class1 Members
void _Class1.Method3()
{
Method3();
}
void _Class1.Method4()
{
Method4();
}
void _Class1.Method1()
{
Method1();
}
void _Class1.Method2()
{
Method2();
}
#endregion
#region Class1___v0 Members
void Class1___v0.Method1()
{
Method1();
}
void Class1___v0.Method2()
{
Method2();
}
void Class1___v0.Method3()
{
Method3();
}
void Class1___v0.Method4()
{
Method4();
}
void Class1___v0.Method5()
{
Method5();
}
#endregion
#region Class1___v1 Members
void Class1___v1.Method1()
{
Method1();
}
void Class1___v1.Method2()
{
Method2();
}
void Class1___v1.Method3()
{
Method3();
}
void Class1___v1.Method4()
{
Method4();
}
void Class1___v1.Method5()
{
Method5();
}
#endregion
}
//This is to keep compatibility with old versions
//we cannot generate a typedef so we will need to add all of the versions
//for BinaryCompatibility
[ComVisible(true)]
[Guid("6B86684C-B3DD-4680-BF95-8DEE2C17AF5B")]
[TypeLibType(TypeLibTypeFlags.FHidden)]
public interface Class1___v0
{
[DispId(0x60030000)]
void Method1();
[DispId(0x60030001)]
void Method2();
[DispId(0x60030002)]
void Method3();
[DispId(0x60030003)]
void Method4();
[DispId(0x60030004)]
void Method5();
}
//This is to keep compatibility with old versions
//we cannot generate a typedef so we will need to add all of the versions
//for BinaryCompatibility
[ComVisible(true)]
[Guid("4A7A3317-BF13-443E-9DB0-2C5EA21F00CA")]
[TypeLibType(TypeLibTypeFlags.FHidden)]
public interface Class1___v1
{
[DispId(0x60030000)]
void Method1();
[DispId(0x60030001)]
void Method2();
[DispId(0x60030002)]
void Method3();
[DispId(0x60030003)]
void Method4();
[DispId(0x60030004)]
void Method5();
}
}
Sadly in .NET you cannot use Interface Inheritance in COM. If there is interface inheritance YOU HAVE TO IMPLEMENT each interface. In the case of code that comes from VB6. VB6 just uses typedefs, so you really don’t know which methods belong to each version. So in the end all versions have all methods.
The other alternative to this method, is just to implement last version. And after generating the tlb, decompile it to an .IDL file add the typedefs and recompiled it. I explained something similar in this post:http://blogs.artinsoft.net/mrojas/archive/2010/05/17/interop-remove-prefix-from-c-enums-for-com.aspx
Ok. I hope this helps you to have an more clear idea of what Binary Compatibility is and how to do it in .NET. I am attaching some sample code. It show an ActiveX library that uses BinaryCompatibility and three version on an aplications that uses the different versions. And also a .NET class library that is equivalent to the VB6 one. HERE
Enjoy.
AutoCAD 2010 will not be supporting VBA.
Quoting
“If you utilize VBA macros in your work environment, they will no longer work unless the VBA module is installed on your system. “
“When you run a command that requires VBA, a message dialog box will be displayed stating that VBA is no longer installed with AutoCAD and directing you to a website where you can download the VBA module. “
And also you can see that Autodesk states: “Autodesk is evaluating how long VBA will be supported in Autodesk products in the future. Though supported in the AutoCAD 2010-based products, it may or may not be supported in future releases. Therefore, it is strongly recommended that VB developers develop all new code using VB .NET.”
VBA does not support 64bit systems in a native way.
But If you want some advice from the VB migration experts or help on your migration project from VBA to VB.NET or C# you can us contact Artinsoft Migration Services.
We build the VB Upgrade Wizard that shipped with Visual Studio and have been doing VB migrations for years.
We have seen a situation that can be a little annoying.
I usually find that situation in ASP.NET projects, but it can also happen in Winforms.
The issue is like this:
- You have a VStudio Solution
- You have added all the needed references
- All your code compiles
BUT
When you run the application it terminates at a certain point with a
FileNotFound exception indicating that the assembly blablabla or one of its
dependencies could not be loaded.
Whats hapenning!!!
Do this:
1) Look at the bin\debug or bin\release and make sure that the assembly is in those directories.
2) If the assembly is not there, then go to the project references in the solution explorer, right click and select properties and set the Copy Local Setting
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.
Here is some examples of how to determine the WeekNumber of a given Date
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
object index = DateTime.Now;
int res = 0;
//0 First day of year
res = System.Globalization.CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(
Convert.ToDateTime(index), System.Globalization.CalendarWeekRule.FirstDay, System.Globalization.DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek);
//1 (Default) First four day week from Sunday
res = System.Globalization.CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(
Convert.ToDateTime(index), System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Sunday);
//2 First four day week from StartOfWeek
res = System.Globalization.CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(
Convert.ToDateTime(index), System.Globalization.CalendarWeekRule.FirstFourDayWeek, System.Globalization.DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek);
//3 First full week from Sunday
res = System.Globalization.CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(
Convert.ToDateTime(index), System.Globalization.CalendarWeekRule.FirstFullWeek, DayOfWeek.Sunday);
//4 First full week from StartOfWeek
res = System.Globalization.CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(
Convert.ToDateTime(index), System.Globalization.CalendarWeekRule.FirstFullWeek, System.Globalization.DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek);
}
}
}
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.
This post shows a way to calculate the number of weeks.
Remember that this calculation is culture-dependant
For example the GetWeekOfYear methods requires a criteria to determine
how to determine the first week and which day to consider as FirstDayOfWeek for more info see here:
CalendarWeekRule.FirstDay
Supported by the .NET Compact Framework.
Indicates that the first week of the year starts on the first day of the year and ends before the following designated first day of the week. The value is 0.
CalendarWeekRule.FirstFourDayWeek
Indicates that the first week of the year is the first week with four or more days before the designated first day of the week. The value is 2.
CalendarWeekRule.FirstFullWeek
Indicates that the first week of the year begins on the first occurrence of the designated first day of the week on or after the first day of the year. The value is 1.
Sample Code
Dim x As Date
Dim currentCulture As System.Globalization.CultureInfo
currentCulture = CultureInfo.CurrentCulture
Dim weekNum = currentCulture.Calendar.GetWeekOfYear(x, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday)
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;
In the VB6 world it was very common to use “Resizer” OCXs that you just put on your forms and that they handle all the proportional resizing needed if you changed your form.
It was very simple and easy.
However when I started looking for something like that in .NET it was quite hard to find. So finally I got to ComponentSource and found some alternatives and I evaluated them:
Component | Vendor | Pricing | URL |
Component One Sizer For .NET | Component ONE | I think you have to buy the Component One Studio. That’s $800 ($1000 with more support) and I think is one license per developer, runtime-free | http://www.componentone.com/SuperProducts/StudioWinForms/ |
.NET Forms Resize | Softgroup | 380$ per developer from ComponentSource if you buy directly from them you can get the component for 180$ Each copy is licensed on a per developer basis. A developer may use the license on multiple CPUs as long as they are not used concurrently nor used by another developer. Run-time royalty free. | http://www.soft-group.it/net/index.php?c=3a&id=11
|
Resize OCX/.NET | Larcom and Young | 95$-100$ per license one license per developer, runtime-free | http://www.lyoung.com/ |
All of them seem to work. So it really up to your company preferences, budget and the level of support you desire. I haven't made tests like how it behaves if you have thirdparty components or activex ??? maybe I'll review that later.
Most of our clients come from a medium-size to a big enterprise level. In these scenarios is very common to have different department using different technologies to solve their business needs.
These different technologies can be on a very homogeneous platform like .NET where you can easily interact between your VB.NET and C# assemblies, or they could be on differente technologies like ASP, Classic VB, C++, or Powerbuilder.
This post is about PowerBuilder, and in order to interact with PowerBuilder I think the easiest way is to expose your assemblies thru COM Interop.
So if there is some .NET functionality that you want to expose to PowerBuilder you just need to expose that functionality with a class in a ClassLibrary project with COM attributes.
Let’s begin with a simple program to show how to comunicate Powerbuilder with C#.
NOTE: If you don’t have Powerbuilder you can get a trial version from: http://www.sybase.com/detail?id=1052162
- Open Microsoft Visual Studio
- On the File Menu, choose the New option, and in the File submenu choose Project….
Figure 1. Visual Studio File Menu. Choosing the option for a new project
- When you choose that option a dialog window will shown with the available options for new projects. In the option for C# Projects choose “Class Library”
Figure 2. New Project dialog window
You must introduce the new project name, location and solution name. Type something like ClassLibrary1, D:\PowerBuilder, ClassLibrary1.
1. When you finish creating your project you will have a code file called Class1.cs.
2. Change that code for something like :
using System;
using System.Collections.Generic;
using System.Text;
namespace SimpleClass
{
public class Class1
{
public int AddTenToParameter(int param1)
{
return param1 + 10;
}
public void SayHi()
{
System.Windows.Forms.MessageBox.Show("Hello World!");
}
public String GiveMeDate()
{
return DateTime.Now.ToLongDateString();
}
}
}
This will allow to test things like parameter passing, using different return types like strings or integers. But Before you continue you must add a reference to System.Windows.Forms to be able to use the MessageBox.
Figure 3. Adding a reference
Figure 3. Reference to System.Windows.Forms
3. Right click on the solution file and select properties:
Figure 4. Option to change project properties
4. Select the Register for COM Interop checkbox
Figure 5. Project properties window
5. Return to Class1.cs code file
6. Add an using statement after the existing using lines on Class1.cs file:
using System.Runtime.InteropServices;
7. Add the following attributes to the class:
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]
[ProgId("ClassLibrary1.Class1")]
Note: the ProgId is very important, because these value will be use in PB to comunicate with the this code
8. Now you must edit the AssemblyInfo.cs
Figure 6. AssemblyInfo.cs File
Now make sure to establish the COM settings in this file with statements like the following:
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(true)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("69efac5b-d887-40f4-a7e9-2721ac3c1598")]
The Guid is also very important, because this is used to differentiate this component and it must be unique.
To generate a new GUID you can got to the Tools Menu and choose the option Create GUID
Figure 7. Option menu to create a GUID
In the Create Guid dialog box, choose the fourth option and press Copy to put the contents on the Clipboard. Later, copy that value in the GUID attribute but remove the “{“ y “}”.
Now we are set. You only need to build the solution and the DLL. The build process with register the COM component.
If you will use the component on another computer you need to create an instalation program.
Using your program from Powerbuilder
Using your program from Powerbuilder is very easy. You just need code like the following:
Figure 8. PB Code to call a C# class thru COM
When you execute this program you will have 3 messageboxes :
- Hello World!
- 30
- Monday, March 02, 2009 (this message will change depending of the day, locale and regional settings)
Creating an instalation program
- Right click the solution and in the context menu choose Add and then new project.
Figure 9. Context Menu to add a new project
On the dialog box for Add New projec, look for the Other Project Types section and the choose Setup Project.
Figure 10. Creating a setup project
In this dialog bos indicate the name and location of the setup project. For example Setup1 and D:\Powerbuilder\ClassLibrary1.
Later, add a project to the setup program. To do that rigth click on the setup project and select Add, and in the submenu choose Project Output.
Figure 11. Adding a project to the setup project.
A dialog box will be shown with a combo that allow you to select the proyects in the solution. Choose ClassLibrary1 and press OK.
Figure 12. Adding project output to the setup project.
When you build this instalation program two files will be produced:
Release
D:\PowerBuilder\ClassLibrary1\Setup1\Release\Setup.exe
D:\PowerBuilder\ClassLibrary1\Setup1\Release\Setup1.msi
Debug
D:\PowerBuilder\ClassLibrary1\Setup1\Debug\Setup.exe
D:\PowerBuilder\ClassLibrary1\Setup1\Debug\Setup1.msi
When you run the instalation program, this program will handle the instalation of the .NET component and the COM registration.
My good friend Jafet, was recently dealing with an interesting bug in Visual Studio.
He created a simple Visual Studio Project that had just an interface that he exposed thru COM. He’s intention was to compile the Class Library and use the TLB file in other projects, but after building the solution he found out that there was no TLB file generated.
After some tests he finally found out that Visual Studio does not generates a TLB if there are not classes in the Project. So your options are, add a dummy class to your project or invoke the TLB tool from the command line.
:)
My first language ever was GW-Basic. I was a teenager doing in summer work at a company that made polls about hair products and things like that. At that time I didn’t had a computer. They were tooooo expensive.
And I found a manual for GW-Basic. I stole it, to make a copy, my boss found out and he fired me :(. But then I had the power!! jeje. With my cousin Esteban we started out first programs. And we got our first sale :) :) :). It was the typical POS software for a small video rental company.
Ahhh, and that was just the beginning.
So, I really appreciate basic, with all its strengths and weaknesses. I do most of my development now in C#, I think due to my C++, java years, and know I am more used to its syntax, and I really prefer C#. But I consider Basic a real treasure.
Well recently while digging in the Microsoft Dev Labs I discovered SmallBasic. I was just browsing for some new stuff and I am glad to have found this jewel.
Artinsoft (the company I work for) is a company that has devoted a lot of time in the research of automatic software migration to other platforms.Not for nothing one of our flapship products is Visual Basic Upgrade Companion, so basic in any form is always a delight for me. (I will soon post a brief article about bringing VBScript back to life in VB.NET just wait and see)
Ok. Back to SmallBasic.
First of all. The interface is great!!!
It has a cool intellisense guide and you can even extend it :)
And they even resurrected LOGO!
I just hope to write a cool Turtle Graphics routine soon!
:)
Most people migrating their application want to move ahead and take advantage of new technologies and new operating systems.
So if you had a VB6 application and you migrated it with us to .NET we will recommend and automate the process to use ADO.NET.
Why?
You can still use ODBC but i will list some compelling reasons:
* There a very fast ADO.NET drivers available. Using ODBC implies addind an interop overhead that can affect performance.
* Some vendors do not support and/or certify the use of ODBC drivers for .NET. So in those cases if you use ODBC your are on your own.
During my consulting experience I have seen several problems using ODBC drivers ranging from just poor performance, problems with some SQL statements, stored procedures calls, database specific features or complete system inestability.
* and also problems running in 64-bit.
This last one is very concerning. If you made all the effort to migrate an application to .NET and run it on for example on a Windows 2003 64 bit server it wont be able to use your 32-bit ODBC drivers unless you go to the the Build tab, and set Platform Target to "x86".
This is very sad because your application cannot take advantage of all the 64 bit resources.
If you are lucky enough you might find a 64 bit version of your ODBC driver but I will really recommend going straigth to 64-bit and use ADO.NET. And that's exactly what we can really help you to do specially in our version 2.2 of the VBUC.
This post describes an an interesting workaround that you can use to support the migration of ActiveX Documents with the Artinsoft Visual Basic Upgrade Companion which is one of the Artinsoft \ Mobilize.NET tools you can use to modernize your Visual Basic, Windows Forms and PowerBuilder applications.
Currently the Visual Basic Upgrade Companion does not allow you to process ActiveX Document directly, but there is a workaround: in general ActiveX Document are something really close to an User Control which is a element that is migrated automatically by the Visual Basic Upgrade Companion.
This post provides a link to a tool (DOWNLOAD TOOL) that can fix your VB6 projects, so the Visual Basic Upgrade Companion processes them. To run the tool:
1) Open the command prompt
2) Go to the Folder where the .vbp file is located
3) Execute a command line command like:
FixUserDocuments Project1.vbp
This will generate a new project called Project1_modified.vbp. Migrate this new project and now UserDocuments will be supported.
First Some History
VB6 allows you to create UserDocuments, which can be embedded inside an ActiveX container. The most common one is Internet Explorer. After compilation, the document is contained in a Visual Basic Document file (.VBD) and the server is contained in either an .EXE or .DLL file. During development, the project is in a .DOB file, which is a plain text file containing the definitions of the project’s controls, source code, and so on.
If an ActiveX document project contains graphical elements that cannot be stored in text format, they will be kept in a .DOX file. The .DOB and .DOX files in an ActiveX document project are parallel to the .FRM and .FRX files of a regular Visual Basic executable project.
The trick to support ActiveX documents is that in general they are very similar to UserControls, and .NET UserControls can also be hosted in a WebBrowser. The following command line tool can be used to update your VB6 projects. It will generate a new solution where UserDocuments will be defined as UserControls.
If you have an ActiveX document like the following:
Then after running the tool you will have an Project like the following:
So after you have upgraded the projet with the Fixing tool, open the Visual Basic Upgrade Companion and migrate your project.
After migration you will get something like this:
To use your migrated code embedded in a Web Browser copy the generated assemblies and .pdb to the directory you will publish:
Next create an .HTM page. For example UserDocument1.htm
The contents of that page should be something like the following:
<html>
<body>
<p>ActiveX Demo<br> <br></body>
<object id="UserDocument1"
classid="http:<AssemblyFileName>#<QualifiedName of Object>"
height="500" width="500" VIEWASTEXT>
</object>
<br><br>
</html>
For example:
<html>
<body>
<p>ActiveX Demo<br> <br></body>
<object id="UserDocument1"
classid="http:Project1.dll#Project1.UserDocument1"
height="500" width="500" VIEWASTEXT>
</object>
<br><br>
</html>
|
Now all that is left is to publish the output directory.
To publish your WinForms user control follow these steps.
- Create a Virtual Directory:
- A Wizard to create a Virtual Directory will appear.
Click Next
Name the directory as you want. For example Project1. Click Next
Select the location of your files. Click the Browse button to open a dialog box where you can select your files location. Click Next
Check the read and run scripts checks and click next
Now Click Finish
- Properties for the Virtual Directory will look like this:
NOTE: to see this dialog right click over the virtual directory
- Now just browse to the address lets say http:\\localhost\Project1\UserDocument1.htm
And that should be all! :)
The colors are different because of the Host configuration however a simple CSS like:
<style>
body {background-color: gray;}
</style>
Can make the desired change:
Notice that there will be security limitations, for example for thinks like MessageBoxes.
You can allow restricted operations by setting your site as a restricted site:
For example:
Restrictions
The constraints for this solution include:
* This solutions requires Windows operating system on the client side
* Internet Explorer 6.0 is the only browser that provides support for this type of hosting
* It requires .NET runtime to be installed on the client machine.
* It also requires Windows 2000 and IIS 5.0 or above on the server side
Due to all of the above constraints, it might be beneficial to detect the capabilities of the client machine and then deliver content that is appropriate to them. For example, since forms controls hosted in IE require the presence of the .NET runtime on the client machine, we can write code to check if the client machine has the .NET runtime installed. You can do this by checking the value of the Request.Browser.ClrVersion property. If the client machine has .NET installed, this property will return the version number; otherwise it will return 0.0.
Adding a script like:
<script>
if ((navigator.userAgent.indexOf(".NET CLR")>-1))
{
//alert ("CLR available " +navigator.userAgent);
}
else
alert(".NET SDK/Runtime is not available for us from within " + "your web browser or your web browser is not supported." + " Please check with http://msdn.microsoft.com/net/ for " + "appropriate .NET runtime for your machine.");
</script>
Will help with that.
References:
ActiveX Documents Definitions:
http://www.aivosto.com/visdev/vdmbvis58.html
Hosting .NET Controls in IE
http://www.15seconds.com/issue/030610.htm
During migratio of a simple project, we found an interesting migration details.
The solution has a project with two Forms. Form1 and Form2. Form1 has a command button and in the Click for that command button it performs a code like UnLoad Form2.
But it could happen that Form2 has not been loaded but in VB6 it is not a problem. In .NET the code will be something like form2.Close() and it could cause problems.
A possible fix is to add some flag that indicates if the form was instanciated and the call the event.
Just more details about scripting
Using the MS Scripting Object
The MS Scripting Object can be used in .NET applications. But it has several limitations.
The main limitation it has is that all scripted objects must be exposed thru pure COM. The scripting object is a COM component that know nothing about .NET
In general you could do something like the following to expose a component thru COM:
[System.Runtime.InteropServices.ComVisible(true)]
public partial class frmTestVBScript : Form
{
//Rest of code
}
NOTE: you can use that code to do a simple exposure of the form to COM Interop. However to provide a full exposure of a graphical component like a form or user control you should use the Interop Form ToolKit from Microsoft http://msdn.microsoft.com/en-us/vbasic/bb419144.aspx
To expose an object in COM. But most of the properties and methods in a System.Windows.Forms.Form class, use native types instead of COM types.
As you could see in the Backcolor property example:
public int MyBackColor
{
get { return System.Drawing.ColorTranslator.ToOle(this.BackColor); }
set { this.BackColor = System.Drawing.ColorTranslator.FromOle(value); }
}
Issues:
- The problem with properties such as those is that System.Drawing.Color is not COM exposable.
- Your script will expect an object exposing COM-compatible properties.
- Another problem with that is that there might be some name collision.
Using Forms
In general to use your scripts without a lot of modification to your scripts you should do something like this:
- Your forms must mimic the interfaces exposed by VB6 forms. To do that you can use a tool like OLE2View and take a look at the interfaces in VB6.OLB
- Using those interfaces create an interface in C#
- Make your forms implement that interface.
- If your customers have forms that they expose thru com then if those forms add new functionality do this:
- Create a new interface, that extends the basic one you have and
I’m attaching an application showing how to to this.
Performing a CreateObject and Connecting to the Database
The CreateObject command can still be used. To allow compatibility the .NET components must expose the same ProgIds that the used.
ADODB can still be used, and probably RDO and ADO (these last two I haven’t tried a lot)
So I tried a simple script like the following to illustrate this:
Sub ConnectToDB 'declare the variable that will hold new connection object Dim Connection 'create an ADO connection object Set Connection=CreateObject("ADODB.Connection") 'declare the variable that will hold the connection string Dim ConnectionString 'define connection string, specify database driver and location of the database ConnectionString = "Driver={SQL Server};Server=MROJAS\SQLEXPRESS;Database=database1;TrustedConnection=YES" 'open the connection to the database Connection.Open ConnectionString MsgBox "Success Connect. Now lets try to get data" 'declare the variable that will hold our new object Dim Recordset 'create an ADO recordset object Set Recordset=CreateObject("ADODB.Recordset") 'declare the variable that will hold the SQL statement Dim SQL SQL="SELECT * FROM Employees" 'Open the recordset object executing the SQL statement and return records Recordset.Open SQL, Connection 'first of all determine whether there are any records If Recordset.EOF Then MsgBox "No records returned." Else 'if there are records then loop through the fields Do While NOT Recordset.Eof MsgBox Recordset("EmployeeName") & " -- " & Recordset("Salary") Recordset.MoveNext Loop
End If MsgBox "This is the END!" End Sub |
I tested this code with the sample application I’m attaching. Just paste the code, press Add Code, then type ConnectToDB and executeStatement
I’m attaching an application showing how to do this. Look at extended form. Your users will have to make their forms extend the VBForm interface to expose their methods.
Using Events
Event handling has some issues.
All events have to be renamed (at least this is my current experience, I have to investigate further, but the .NET support for COM Events does a binding with the class names I think there’s a workaround for this but I still have not the time to test it).
In general you must create an interface with all events, rename then (in my sample I just renamed them to <Event>2) and then you can use this events.
You must also add handlers for .NET events to raise the COM events.
#region "Events"
public delegate void Click2EventHandler();
public delegate void DblClick2EventHandler();
public delegate void GotFocus2EventHandler();
public event Click2EventHandler Click2;
public event DblClick2EventHandler DblClick2;
public event GotFocus2EventHandler GotFocus2;
public void HookEvents()
{
this.Click += new EventHandler(SimpleForm_Click);
this.DoubleClick += new EventHandler(SimpleForm_DoubleClick);
this.GotFocus += new EventHandler(SimpleForm_GotFocus);
}
void SimpleForm_Click(object sender, EventArgs e)
{
if (this.Click2 != null)
{
try
{
Click2();
}
catch { }
}
}
void SimpleForm_DoubleClick(object sender, EventArgs e)
{
if (this.DblClick2 != null)
{
try
{
DblClick2();
}
catch { }
}
}
void SimpleForm_GotFocus(object sender, EventArgs e)
{
if (this.GotFocus2 != null)
{
try
{
GotFocus2();
}
catch { }
}
}
#endregion
Alternative solutions
Sadly there isn’t currently a nice solution for scripting in .NET. Some people have done some work to implement something like VBScript in .NET (including myself as a personal project but not mature enough I would like your feedback there to know if you will be interesting in a managed version of VBScript) but currently the most mature solution I have seen is Script.NET. This implementation is a true interpreter. http://www.codeplex.com/scriptdotnet Also microsoft is working in a DLR (Dynamic Languages Runtime, this is the runtime that I’m using for my pet project of VBScript)
The problem with some of the other solutions is that they allow you to use a .NET language like CSharp or VB.NET or Jscript.NET and compile it. But the problem with that is that this process generates a new assembly that is then loaded in the running application domain of the .NET Virtual machine. Once an assembly is loaded it cannot be unloaded. So if you compile and load a lot of script you will consume your memory. There are some solutions for this memory consumption issues but they require other changes to your code.
Using other alternatives (unless you used a .NET implementation of VBScript which currently there isn’t a mature one) will require updating all your user scripts. Most of the new scripts are variants of the Javascript language.
Migration tools for VBScript
No. There aren’t a lot of tools for this task. But you can use http://slingfive.com/pages/code/scriptConverter/
Download the code from: http://blogs.artinsoft.net/public_img/ScriptingIssues.zip