Introduction
ActiveX technology is very often used by 3rd party companies to distribute their API. Usually, it is represented as a Windows Forms control. Visual Studio makes it very easy to integrate such control into your Windows Forms application. But what if you want to borrow functionality from ActiveX control and use it in Console application or in a Class Library?
In this tip, I will show how to use ActiveX in back-end applications. Also, I will describe the most common issues that may happen with ActiveX and solutions for them.
Register/Unregister ActiveX
First of all, ActiveX component should be registered on the target computer. Otherwise, you will get the following exception:
System.Runtime.InteropServices.COMException (0x80040154): Class not registered (Exception from HRESULT: 0x80040154 (REGDB_E_CLASSNOTREG))
Usually, ActiveX component represented as one ".ocx" file and several ".dll" files. For installation - run the following command in CMD:
regsvr32 "path_to_activex\activex_name.ocx"
For uninstall - run the following command in CMD:
regsvr32 /u "path_to_activex\activex_name.ocx"
If you are not an Administrator in the system, you may receive the following errors:
The module "C:\[file being registered]" was loaded but the call to DllRegisterServer failed with error code 0x80040200.
The module "C:\[file being registered]" was loaded but the call to DllUnregisterServer failed with error code 0x80040200.?
This message may appear when registering or unregistering a file on Windows 7 when User Account Control (UAC) is enabled on the computer. User Account Control may limit or prevent certain tasks that the Windows User Account does not have permissions to.
If the security settings do not allow registering or unregistering a file from the Windows Run box, it is necessary to register or unregister the file from an elevated Windows Command Prompt. Please follow the steps below to register or unregister the necessary file from an Elevated Command Prompt:
- Close all programs.
- Click "Start > All Programs > Accessories".
- Right-click on "Command Prompt" and select "Run as administrator".
- Execute the necessary regsvr32 command to register or unregister the corresponding DLL or OCX file.
Using ActiveX in Class Library
Basically, ActiveX was designed to be used in GUI applications. That is why most of them require STA Thread, application message loop and many other stuff that is present in Windows Forms projects but absent in Class Library projects. Next, I will show how to avoid all this headache by creating a wrapper around ActiveX object.
First, we need to create Class Library project in Visual Studio. If ActiveX compiled as 32 bit application, make sure your project also compiles with x86 platform setting.
Next step is to add Windows Form to your project. Let's call it "SdkWrapperForm
". The only purpose of this form is to host our ActiveX control. So, you need to add it on the form: Toolbox->right-click->Choose Items...->COM Components. Select target ActiveX and press OK. New control will appear in Toolbox\General. Drag&Drop it on the form. Automatically, Visual Studio will add two DLLs to your project: AxInterop.activex_name
, Interop.activex_name
. These two DLLs should be distributed along with your project output DLL.
Now go to 'SdkWrapperForm.cs' and add the following code:
public partial class SdkWrapperForm : Form
{
public SdkWrapperForm()
{
InitializeComponent();
}
public activex_type SDK
{
get
{
return activex_obj;
}
}
}
This property allows us to access ActiveX object, that we placed on the form, from other classes.
Now, we are ready to write the main part of the wrapper. Add new class 'SdkWrapper
' to your project. Paste the following code:
public class SdkWrapper : IDisposable
{
#region Private members
private Thread _uiThread;
private SdkWrapperForm _form;
#endregion
#region Constructor
public SdkWrapper()
{
ManualResetEvent initFinished = new ManualResetEvent(false);
_uiThread = new Thread(() =>
{
_form = new SdkWrapperForm();
initFinished.Set();
while (true)
{
try
{
Application.Run();
}
catch (ThreadAbortException)
{
return;
}
catch (Exception ex)
{
}
}
}
);
_uiThread.SetApartmentState(ApartmentState.STA);
_uiThread.IsBackground = true;
_uiThread.Start();
initFinished.WaitOne(1000);
}
public void Dispose()
{
_uiThread.Abort();
_uiThread = null;
Application.Exit();
}
#endregion
}
This code creates an instance of our 'SdkWrapperForm
' in the thread that is marked with STA attribute and runs application message loop. Note that Dispose
should be called before application close.
Now, if we want to use some methods from ActiveX object, we need to create wrappers for them. Here are few examples:
public void AX_connect(string url, string username, string password)
{
if (_form.InvokeRequired)
{
_form.Invoke(new Action(() => { AX_connect(url, username, password); }));
}
else
{
_form.SDK.TS_connect(url, username, password);
}
}
public string AX_get_version()
{
if (_form.InvokeRequired)
{
return (string)_form.Invoke(new Func<string>(() => { return AX_get_version(); }));
}
else
{
return _form.SDK.AX_get_version();
}
}
public string AX_get_some_info(short id)
{
if (_form.InvokeRequired)
{
return (string)_form.Invoke(new Func<string>(() => { return AX_get_some_info(id); }));
}
else
{
return _form.SDK.AX_get_some_info(id);
}
}
As you can see, methods from ActiveX object should be called from the same thread that ActiveX was created.
Basically that's it! Class 'SdkWrapper
' may be used in any place of your code.
Points of Interest
There are few issues that I faced while integrating ActiveX component into my application.
First one related to incorrect disposing. I got the following error:
COM object that has been separated from its underlying RCW cannot be used.
at System.RuntimeType.InvokeDispMethod(String name, BindingFlags invokeAttr, Object target, Object[] args, Boolean[] byrefModifiers, Int32 culture, String[] namedParameters) at System.RuntimeType.InvokeMember(String name, BindingFlags bindingFlags, Binder binder, Object target, Object[] providedArgs, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParams) at System.RuntimeType.ForwardCallToInvokeMember(String memberName, BindingFlags flags, Object target, Int32[] aWrapperTypes, MessageData& msgData)
This issue happens if I call some methods from ActiveX when host form is already disposed.
Another issue that I faced with related to unmanaged resource allocation (I didn't dive into details what exactly happens there). My application crashed every 2 hours (+/- 15 mins) with the following exceptions that I took from Windows Event Log:
Application: my_app.exe Framework Version: v4.0.30319 Description: The process was terminated due to an unhandled exception. Exception Info: System.AccessViolationException Stack: at System.Windows.Forms.UnsafeNativeMethods.PeekMessage(MSG ByRef, System.Runtime.InteropServices.HandleRef, Int32, Int32, Int32) at System.Windows.Forms.Application+ ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.
FPushMessageLoop(IntPtr, Int32, Int32) at System.Windows.Forms.Application+ThreadContext.RunMessageLoopInner(Int32, System.Windows.Forms.ApplicationContext) at System.Windows.Forms.Application+ThreadContext.RunMessageLoop(Int32, System.Windows.Forms.ApplicationContext) at System.Windows.Forms.Application.Run() at my_app.SdkWrapper+<>c__DisplayClassf.<.ctor>b__6() at System.Threading.ThreadHelper.ThreadStart_Context(System.Object) at System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) at System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) at System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) at System.Threading.ThreadHelper.ThreadStart()
And like this:
Application: my_app.exe Framework Version: v4.0.30319 Description: The process was terminated due to an unhandled exception. Exception Info: System.Reflection.TargetInvocationException Stack: at System.RuntimeMethodHandle.InvokeMethod(System.Object, System.Object[], System.Signature, Boolean) at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(System.Object, System.Object[], System.Object[]) at System.Reflection.RuntimeMethodInfo.UnsafeInvoke(System.Object, System.Reflection.BindingFlags, System.Reflection.Binder, System.Object[], System.Globalization.CultureInfo) at System.Delegate.DynamicInvokeImpl(System.Object[]) at System.Delegate.DynamicInvoke(System.Object[]) at System.Windows.Forms.Control.InvokeMarshaledCallbackDo(ThreadMethodEntry) at System.Windows.Forms.Control.InvokeMarshaledCallbackHelper(System.Object) at System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) at System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) at System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) at System.Windows.Forms.Control.InvokeMarshaledCallback(ThreadMethodEntry) at System.Windows.Forms.Control.InvokeMarshaledCallbacks() at System.Windows.Forms.Control.WndProc(System.Windows.Forms.Message ByRef) at System.Windows.Forms.ScrollableControl.WndProc(System.Windows.Forms.Message ByRef) at System.Windows.Forms.ContainerControl.WndProc(System.Windows.Forms.Message ByRef) at System.Windows.Forms.Form.WndProc(System.Windows.Forms.Message ByRef) at my_app.MainForm.WndProc(System.Windows.Forms.Message ByRef) at System.Windows.Forms.Control+ControlNativeWindow.OnMessage(System.Windows.Forms.Message ByRef) at System.Windows.Forms.Control+ControlNativeWindow.WndProc(System.Windows.Forms.Message ByRef) at System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr) at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG ByRef) at System.Windows.Forms.Application+ ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.
FPushMessageLoop(IntPtr, Int32, Int32) at System.Windows.Forms.Application+ThreadContext.RunMessageLoopInner(Int32, System.Windows.Forms.ApplicationContext) at System.Windows.Forms.Application+ThreadContext.RunMessageLoop(Int32, System.Windows.Forms.ApplicationContext) at System.Windows.Forms.Application.Run(System.Windows.Forms.Form) at my_app.MainForm.Main(System.String[])
I found that it happens because my application creates and deletes ActiveX objects during work. After some amount of created objects, I got those exceptions. Probably, I didn't delete those objects fully and some unmanaged resources remain in memory. As a workaround, I created ActiveX object pool. The application first looks at an available object in the pool; if pool is empty - create a new object; if pool is not empty - re-use an existing object. Works like a charm for me since my app uses maximum 16 objects simultaneously.
History