Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Callback Functions and .NET C# COM Components

0.00/5 (No votes)
1 Feb 2008 1  
How to make your C# component callback a Perl subroutine

Introduction

Recently, my co-author had a requirement where he needed to convert a C# class library into a COM compatible component which could be called from either a C++/C#/Perl/Scripting environment. There was already a lot of information on how to implement this, but he had a somewhat more involved requirement. The requirement was that his Perl script needed to call a C# function (say Analyze()). The Analyze() function would typically take a long time to process, and the client needed to be updated regularly with the progress counter. This would be best solved using Callbacks.

However, there was scanty information available in a ready-to-use form, which could explain how to achieve this.

The article below explains how to handle multiple .NET events in a Perl environment.

Background

This article is an attempt to explain the concept of Event Sources and Event Sinks as applicable to a COM client.

The code contains a C# COM Server, and a sample Perl Client which explains in the simplest manner possible, the concept of tapping the power of callback functions.

This article does the above by showing how to use the Win32::OLE module present in Perl, and using it to trap events generated in the .NET component.

Using the Code

To use the code attached, there are a few steps that need to be performed.

  1. Unzip the CSharp_and_Scripting_New_src.zip.
  2. Browse to the CSharp_and_Scripting_New\CallBack Server\CallBack folder.
  3. Run register.bat
    1. In case the register.bat script does not work for you, open the project CallBack.sln in Visual Studio 2005. Go to the Tools Menu -> Visual Studio 2005 Command Prompt.
    2. Change directory in the command window to the CSharp_and_Scripting_New\CallBack Server\CallBack\bin\debug folder.
    3. Type regasm CallBack.dll /tlb:Callback.tlb
    4. type gacutil -i CallBack.dll
  4. Now that the .NET C# COM server is registered in your system, you can run the CSharp_and_Scripting_New\CallBack Client\Sample1.pl to view the output as shown below:

    CSharp_and_Scripting_New_Snapshot.png

Description

COM Server Code

The COM server has been written in C#. C# does not have any direct concept of function pointers. Instead, it defines something called delegates which is a safe form of a reference to a function. Now to tap the power of delegates, let us define some delegates as shown below in our class DotNetEventSender:

// No need to show this delegate to COM
[ComVisible(false)]
public delegate void MyEventTarget(string msg);
[ComVisible(false)]
public delegate void MyEventTarget2(int nTimerCounter);

public event MyEventTarget TheEvent;
public event MyEventTarget2 TheEvent2;  

As you can see above, we've declared two delegates of different types and associated two events with these delegates. Now let us declare an interface which our class will expose:

//This interface defines purely the events.
//DotNetEventSender should implement this interface with the
//ComSourceInterfaces() attribute to become an Event Source.
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]

public interface CallBackEventInterface
{
    [DispId(1)] void TheEvent(string msg); 
    [DispId(2)] void TheEvent2(int nTimerCounter);
}  

Our class will then expose this interface as shown below:

//Identifies these interfaces that are exposed as COM event sources 
//for the attributed class.
[ComSourceInterfaces(typeof(CallBackEventInterface))]

//Tells the compiler not to generate an interface automatically and that we are 
//implementing our own interface (IDotNetEventSender)
[ClassInterface(ClassInterfaceType.None)]
public class DotNetEventSender:IDotNetEventSender
{ ... 

Notice the attribute tag:

[ComSourceInterfaces(typeof(CallBackEventInterface))] 

This tells the compiler that we are interested in exposing an event source for our class. The event sink will be then implemented in the Perl Client in a package which is expressly consuming (handling) all events generated by our C# server.

Now, it's not much use exposing an Event source, if we don't also implement some server functions that could be called by the Perl client. For example, the Perl client would call the function Run() which will start the timer in the C# server. This timer would then intermittently fire the event sources, which could then be handled in the Perl client.

So, let's now implement some class functions in the C# server.

private int m_nLoopCounter=0;
public void Run()
{
        Console.Out.WriteLine("Inside Run");
        //Note that the event handlers will be specified in the client
        //(e.g. Perl Script)     
 
        //start a timer which call the Events every one second.
        System.Timers.Timer progress = new System.Timers.Timer(1000);
        progress.Elapsed += new ElapsedEventHandler(TimerFunction);           
        progress.Start();
}

private void TimerFunction(object source, ElapsedEventArgs e)
{
        m_nLoopCounter++;
        TheEvent("Hello, World!");
        TheEvent2(m_nLoopCounter);
} 

Perl Client Code

In the Perl client code, the basic step is to create an object of the C# server as shown below:

my $TM = Win32::OLE->new('CallBack.DotNetEventSender');

Once this is done, we want to inform the C# server that we are interested in receiving notifications when events of interest are generated on the server. We do this by stating:

Win32::OLE->WithEvents($TM, 'MyEvents');

Here, we are informing the C# server that all events should be forwarded to the package MyEvents. This package will define subroutines with names that are exactly the same as the events in the C# server.

package MyEvents;

#The name of the subroutine should be the exactly the same as the name of the
#event in the C# component.

sub TheEvent 
{
        my ($obj,$args) = @_;
        print "TheEvent() : ".$args."\n";
}

sub TheEvent2 
{
    my ($obj,$args) = @_;
    print "TheEvent2(): Timer Fired Count: ".$args."\n";
} 

Now, it's not much use defining all the above code if we don't run a loop, which will keep the Perl client waiting for event messages to arrive. To do this, we add the code to the very end of the main package as shown below:

#keeps the Perl client active, processing the event messages as they
#occur repeatedly. 

Win32::OLE->MessageLoop(); 

That's it, folks! Just run the sample1.pl to see the above logic in action. Happy 'Interop'ing! :-)

History

  • 1st February, 2008: Initial post

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here