Exposing C# Classes thru Interop

23. June 2010 05:38 by Mrojas in General  //  Tags: , , , , , , , , ,   //   Comments (0)

Either if you migrate your application from VB6 to C# or if you develop a new application in C# something you end up with cases where you need to use your classes in legacy apps. Some of them could have been written in VB6 or could even be VBA macros in Excel applications.

Exposing your .NET classes can be sometimes very easy (you can think is just a matter of putting a ComVisible tag) but in other occasions is not that simple. Specially if your legacy application is using a lot of Late Bound calls like in VBA, so you must make sure that the COM information that you are exposing for your class is exactly what you really want and need.

OK. So I will provide some guidelines or some steps you should follow to provide a consistent COM interface for your .NET Code.

1. First you have to add the [ComVisible(true)]  attribute. Don’t think that’s all. Even if in some cases that is enough is better if you take an strict control of want is being generated for your class. Ok Let’s use the following class as an example:

using System;
using System.Runtime.InteropServices;

namespace InteropExamples
{
    [ComVisible(true)]
    public class MyVerySimpleClass 
    {
        public Class2 CreateANewClass()
        {  return new Class2()     }

        public int GetMyLuckyNumber() { return 15; }
    }
public class Class2 { 
  }
}

// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: <could not determine filename>
[
  uuid(370E4AD4-073B-4984-8C7D-5ED027F7B1CA),
  version(1.0)
]
library ClassLibrary1
{
    // TLib :     // TLib : mscorlib.dll : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
    importlib("mscorlib.tlb");
    // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("stdole2.tlb");

    // Forward declare all types defined in this typelib
    interface _MyVerySimpleClass;

    [
      uuid(E03CCE68-2D55-3576-9DB6-019AAA667A5D),
      version(1.0),
        custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "InteropExamples.MyVerySimpleClass")
    ]
    coclass MyVerySimpleClass {
        [default] interface _MyVerySimpleClass;
        interface _Object;
    };

    [
      odl,
      uuid(D18BEEE1-4425-3AC7-891E-807EC2283731),
      hidden,
      dual,
      oleautomation,
        custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "InteropExamples.MyVerySimpleClass")   

    ]
    interface _MyVerySimpleClass : IDispatch {
    };
};

In this case your class will be expose using all defaults. That is, a progId that will be the <AssemblyName>.ClassName an interface _<ClassName> is generated and the class is exposed only for IDispatch, which would not provide class information if you add the tlb reference to a VB6 or VBA project.

And if you run this code in VB6 you will have a problem like type mismatch when you try to use the method x.CreateAClass because it is returning an object that is not exposed thru COM.

Private Sub Command1_Click()
    Dim x As Object
    Set x = CreateObject("InteropExamples.MyVerySimpleClass")
    MsgBox x.GetMyLuckyNumber
    MsgBox x.CreateAClass
End Sub

So my recommendation is to make explicit what you want to expose. Maybe you only need some of the methods to be exposed. Well that is step two.

2. Define a public, ComVisible(true) interface that will define the methods that you want to be exposed thru COM. Sometimes it is better to implement the interface explicitly. I even recommend using partial classes so you isolate the COM stuff from your normal class. If you class is very simple you can leave all COM stuff there.

    //It is better to have an interface, because
    //you are completely sure what you are exposing or not
    [ComVisible(true)]
    public interface _MyVerySimpleClass
    {
        int GetMyLuckyNumber();
    }

3. (Recommedation) This is not an obligatory step but I recommend using partial classes.

    //Using partial classes allow you to separate all the
    //COM plumbing and leave your .NET implementation simple
    public partial class MyVerySimpleClass 
    {
        public Class2 CreateAClass()
        {
            return new Class2();
        }

        public int GetMyLuckyNumber() { return 15; }
    }

3. Make sure your partial class has the following attributes:

[ComVisible(true)] <—This is obvious because you want to use your class in COM

[ClassInterface(ClassInterfaceType.None)] <—This is because your want to take charge or what will be generated in your Typelib (tlb)

[ComDefaultInterface(typeof(_MyVerySimpleClass))] <—This is to indicate the interface that holds your COM visible methods.

[ProgId("InteropExamples.MyVerySimpleClass")] <—To establish which will be the progId not have a generated one
[Guid("{029D468C-8BE6-498f-8A57-3B4B0306BA41}")] <—this is important specially if you are trying to accomplish binary compatibility

Optionally add this attribute [IDispatchImpl(IDispatchImplType.CompatibleImpl)] this is currently marked as an obsolete attribute but it still works and I have found scenarios, specially in some VBA applications where you need this attribute in order to make some late bound calls.

4. And Explicitly implement the interface methods. This is important because some of the return values or arguments might need convertions. For example what can you do if your method returns a DataSet and your Excel VBA script is expecting something like a Recordset (more on this on other posts).

So now you will have a class like:

    //Using partial classes allow you to separate all the
    //COM plumbing and leave your .NET implementation simple
    public partial class MyVerySimpleClass
    {
        public Class2 CreateAClass()
        {
            return new Class2();
        }

        public int GetMyLuckyNumber() { return 15; }
    }

    //It is better to have an interface, because
    //you are completely sure what you are exposing or not
    [ComVisible(true)]
    public interface _MyVerySimpleClass
    {
        int GetMyLuckyNumber();
    }

    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)] //This is to make sure that no automatic generation of COM methods is done
    [ComDefaultInterface(typeof(_MyVerySimpleClass))] //This to explicitly establish which is the default interface
    [ProgId("InteropExamples.MyVerySimpleClass")]
    [Guid("{029D468C-8BE6-498f-8A57-3B4B0306BA41}")]
    [IDispatchImpl(IDispatchImplType.CompatibleImpl)]
    partial class MyVerySimpleClass : _MyVerySimpleClass
    {
    
        #region _MyVerySimpleClass Members
        //Explicit implementation is better because it avoids messing your .NET
        //class specification. Sometimes when you expose thru COM you can have problem with
        //methods overloads. For example you have to have the same method name but differente 
        //return type. Or you have a collition with an existing member.
        int _MyVerySimpleClass.GetMyLuckyNumber()
        {
            return GetMyLuckyNumber();
        }

        #endregion
    }

And your TLB is now explicit and exposes ONLY what you really really want.

// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: <could not determine filename>

[
  uuid(370E4AD4-073B-4984-8C7D-5ED027F7B1CA),
  version(1.0)
]

library ClassLibrary1
{
   // TLib :     // TLib : mscorlib.dll : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
    importlib("mscorlib.tlb");
   // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("stdole2.tlb");

    // Forward declare all types defined in this typelib
    interface _MyVerySimpleClass;

    [
      odl,
      uuid(80D00C45-EE10-3D65-A5FF-42AB7D8F8A71),
      version(1.0),
      dual,
      oleautomation,
        custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "InteropExamples._MyVerySimpleClass")   

    ]
    interface _MyVerySimpleClass : IDispatch {
        [id(0x60020000)]
        HRESULT GetMyLuckyNumber([out, retval] long* pRetVal);
    };

    [
      uuid(029D468C-8BE6-498F-8A57-3B4B0306BA41),
      version(1.0),
        custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "InteropExamples.MyVerySimpleClass")
    ]

    coclass MyVerySimpleClass {
        interface _Object;
        [default] interface _MyVerySimpleClass;
    };
};

For more info about BinaryCompatibility see my other posts on Interop.

Bittable What????

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:

image

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:

image

 

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

Change CreateObject during Migration

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:

image

3. Create a new CustomMaps File and an an entry like the following:

 

image

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.
image 
 
image 
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.

Calling .NET from PowerBuilder

3. March 2009 08:14 by Mrojas in General  //  Tags: , , , , ,   //   Comments (0)

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

  1. Open Microsoft Visual Studio
  2. On the File Menu, choose the New option, and in the File submenu choose Project….

Visual Studio File Menu

Figure 1. Visual Studio File Menu. Choosing the option for a new project

  1. 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”

image

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.

 

Context Menu to add a reference

Figure 3. Adding a reference

 

Dialog with References

Figure 3. Reference to System.Windows.Forms

3. Right click on the solution file and select properties:

Project properties

Figure 4. Option to change project properties

4. Select the Register for COM Interop  checkbox

Register for COM

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

AssemblyInfo file

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

Create GUID menu option

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:

image

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

  1. Right click the solution and in the context menu choose Add and then new project.

Context Menu for Adding new project to solution

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.

Adding a 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.

 

image

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.

Add Project Output Dialog

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.

Debug XBAP using WinForms Host

26. January 2009 06:13 by Mrojas in General  //  Tags: , , , , ,   //   Comments (0)

Recently I had to deal with targeting an XBAP application that had some Windows Forms controls.

The problem is that those controls can only be used in a trusted environment. If you try to debug an XBAP with some Windows Forms Controls you will get an exception like:

Message: Cannot create instance of 'Page1' defined in assembly 'XBAPWithWinForms, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Exception has been thrown by the target of an invocation.  Error in markup file 'Page1.xaml' Line 1 Position 7.

It took me a while to found a solution, and it was thru Scott Landford Blog that I found a way around.

In short what he recommends to do is:

Change your settings to:

Start Action->Start external program = %windir%system32\PresentationHost.exe

  In my case (and the case of most people that is: c:\windows\system32\PresentationHost.exe)


Start Options->Command line arguments = -debug "c:\projects\myproject\bin\debug\MyProject.xbap" -debugSecurityZoneUrl "http://localhost:2022"

Copy the value from the Start URL after the –debug argument

Very import for using WinForms components you must run in FULL TRUST

fullTrust

 

Here is some XBAP code using a WinForms WebBrowser. They designer does not like it a lot but it works:

XBAPWithWinforms.zip

Migration to 64-bit: ODBC

5. December 2008 06:20 by Mrojas in General  //  Tags: , , , , , ,   //   Comments (0)

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.

 

TLBIMP SourceCode

30. September 2008 09:50 by Mrojas in General  //  Tags: , , , , , ,   //   Comments (0)

 

Have you ever wished to modify the way Visual Studio imported a COM Class. Well finally you can.

The Managed, Native, and COM Interop Team (wow what a name). It looks like the name of that goverment office in the Ironman movie.

Well this fine group of men, have release the source code of the TLBIMP tool. I'm more that happy for this.

I can know finally get why are some things imported the way they are.

http://www.codeplex.com/clrinterop

You can dowload also the P/Invoke assistant. This assistant has a library of signatures so you can invoke any Windows API.

 

Scripting your applications in .NET

14. August 2008 16:48 by Mrojas in General  //  Tags: , , , , ,   //   Comments (0)

In VB6 it was very simple to add scripting capabilities to your application.
Just by using the Microsoft Script Control Library
You can still use this library in .NET just as Roy Osherove' Bloc show in
http://weblogs.asp.net/rosherove/articles/dotnetscripting.aspx

However there are some minor details that must be taken care of:

* Objects must be exposed thru COM (Add the [ComVisible(true)] attribute to the class
* Add the ComVisible(true) attribute to the AssemblyInfo file
* Make these objects public
* Recommended (put your calls to Eval or ExecuteStatement inside try-catch blocks).

And here's an example:

using System;
using System.Windows.Forms;
 
namespace ScriptingDotNetTest
{
    [System.Runtime.InteropServices.ComVisible(true)]
    public partial class frmTestVBScript  : Form
    {
        public int MyBackColor
        {
            get { return System.Drawing.ColorTranslator.ToOle(this.BackColor); }
            set { this.BackColor = System.Drawing.ColorTranslator.FromOle(value); }
        }
 
        MSScriptControl.ScriptControl sc = new MSScriptControl.ScriptControl();
        private void RunScript(Object eventSender, EventArgs eventArgs)
        {
            try
            {
                sc.Language = "VbScript";
                sc.Reset();
                sc.AddObject("myform", this, true);
                sc.ExecuteStatement("myform.MyBackColor = vbRed");
            }
            catch 
            {
                MSScriptControl.IScriptControl iscriptControl = sc as MSScriptControl.IScriptControl;
                lblError.Text = "ERROR" + iscriptControl.Error.Description + " | Line of error: " + iscriptControl.Error.Line + " | Code error: " + iscriptControl.Error.Text;
            }
        }
 
        [STAThread]
        static void Main()
        {
            Application.Run(new frmTestVBScript());
        }
    }
}


TIP: If you don find the reference in the COM tab, just browse to c:\windows\system32\msscript.ocx