Login
 

Atalasoft Imaging SDK Development Blog

Document Imaging and Developer Commentary

Blog Home RSS Feed Old Archive Atalasoft.com

Dynamically Testing an ActiveX Control from C# and NUnit


Posted by Spike McLarty
February 06, 2012 Comments



EZTwainX DemoI spent most of last week on web archeology, puzzling out how to unit test an ActiveX control, entirely dynamically, from C# inside the NUnit framework.

The EZTwainX ActiveX control is a control I created a few years ago at Dosadi, first and foremost a wrapper for the TWAIN scanning API, allowing a web application to scan from a user’s local scanner. Secondarily EZTwainX is an image container, able to collect, display, modify, print and export images as base64 strings to Javascript, ready for upload.

When Atalasoft hired me, they purchased EZTwainX and incorporated it into some of their offerings. And one of the things Atalasoft does a lot of is unit-testing, so EZTwainX needed a solid unit-test.  (At this point the TWAIN/scanner geeks might be wondering “Wait… like, automated tests of TWAIN scanning?” – Yes, that too. A good subject for a future blog.)

What I wanted was a C# class that would carry out a series of automated tests of a freshly-built EZTwainX.cab on a dedicated build-server. It would have been simpler (I imagine) if I had been willing to add the control as a reference to the test assembly, or import a type library, or any similar compile-time cheat. But I didn’t want to do that. EZTwainX is primarily used from Javascript inside Internet Explorer, so I wanted a unit test that closely reproduced that use-case: No compile-time dependency, and no assumption that the control was already installed or registered. So I set out, by stages, to load and test the control from C# (.NET 3.5) entirely dynamically, at run-time, depending only on the .cab file and the human-readable documented API of the component.  …OK, and I knew its GUID.

Parts of the project were not hard. Once the control was instantiated, getting & setting properties and calling methods was tedious and error-prone but relatively straightforward and fairly well documented on the web – basically InvokeMember is your best friend.

Two things, however, sent me into the weeds for days:

A. Instantiating the control purely from an ocx file inside a .cab

B. Capturing events fired by the control.

In this post I’ll explain how I instantiated the control. In an upcoming post I’ll explain what I did to capture events.

A. Instantiating an ActiveX dynamically from a .cab

Despite my best efforts, I failed to instantiate EZTwainX directly from the .cab although there were hints it should have been possible. Not for me. To instantiate the control, takes four steps:

1. Unpack the eztwainx.cab file into eztwainx.ocx and eztwainx.manifest.ocx, placed alongside the running unit test assembly.

2. Set up an activation context that points to the control on disk and push it onto the activation context stack.
What, you’ve never heard of these? Me neither. All part of Side-by-side Assemblies or SxS.  Overlappingly discussed under the term Registration-Free COM, also here.

3. Create the control within that activation context by getting the Type from the GUID and then using System.Activator.CreateInstance

4. Pop the activation context back off the stack and destroy it.

In detail:

1. Unpack the .cab

Using a helper function, I use the built-in ‘expand’ command to unpack the .cab file into the directory alongside the executing unit test assembly.

// Expand the EZTwainX.cab into the folder this test assembly is running from:

int exitCode = runCommand(

"expand.exe",

"\"" + Path.Combine(solutionDir, @"WebControls\Resources\WebCapture\eztwainx.cab") + "\"" +

      " -F:* "+

      "\"" + targetDir + "\"");

Assert.IsTrue((exitCode==0||exitCode==1), "exit code of expand of eztwainx.cab");

// We allow exitCode 1 because that's what expand returns when a file

// already exists and can't be overwritten e.g. when the file is in use.

solutionDir = the root directory of the solution that contains the .cab file as a binary resource.
targetDir = the directory containing the executing unit test assembly.
Note the quotes inserted around the paths, in case there are spaces in them.

2. Establish an Activation Context

Good reference: Registration-Free Activation of COM Components: A Walkthrough by Steve White and Leslie Muller (MSDN)

Activating an ActiveX purely from files on disk involves an application manifest and an activation context – both items poorly documented even by Microsoft standards.  This is part of Side-by-Side assemblies, meant to be a solution to “DLL Hell”.  The application manifest is a file associated with the application (so in our case, the unit test class running within NUnit) that “describes and identifies the shared and private side-by-side assemblies that an application should bind to at run time”.  Using the application manifest, we can tell Windows to load and use a particular DLL or COM component from a particular location when the application needs it – overriding anything in the registry and preventing any default searching.  The application just loads and uses the specific versioned file.  Here’s the manifest used by our unit test, the result of considerable Googling and trial-and-error:

Application Manifest

<?xml version="1.0" encoding="utf-8"?>

<assembly xsi:schemaLocation="urn:schemas-microsoft-com:asm.v1 assembly.adaptive.xsd"

          manifestVersion="1.0"

          xmlns:asmv1="urn:schemas-microsoft-com:asm.v1"

          xmlns:asmv2="urn:schemas-microsoft-com:asm.v2"

          xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"

          xmlns="urn:schemas-microsoft-com:asm.v1"

          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

  <assemblyIdentity name="client.exe" version="1.0.0.0" processorArchitecture="x86" type="win32" />

  <dependency>

    <dependentAssembly  asmv2:codebase="EZTwainX.ocx.manifest">

      <assemblyIdentity name="EZTwainX.ocx" version="1.100.0.0" type="win32" />

    </dependentAssembly>

  </dependency>

</assembly>

It’s mostly boilerplate. The variable parts for us are:

<assemblyIdentity name="client.exe" version="1.0.0.0" processorArchitecture="x86" type="win32" />

This describes the application or client – who is going to be using these external assemblies – and is half lies but Windows doesn’t seem to care. The “x86” and “win32” are true: Because the ActiveX is a 32-bit native-code Windows DLL, the application must be a win32 app and must run in x86 mode.

<dependentAssembly asmv2:codebase="EZTwainX.ocx.manifest">
<assemblyIdentity name="EZTwainX.ocx" version="1.100.0.0" type="win32" />
</dependentAssembly>

This tells Windows about the one assembly of interest to us, which is used by our test application.  It contains the name of the control’s manifest, the name of the DLL containing the control, and the version and platform of the control as required by the application.  I have to admit I don’t remember, but I believe the codebase value is interpreted relative to the directory containing the application manifest.  In other words codebase=”foo.manifest” means foo.manifest in the same folder as this document - the application manifest.

ActivationContext Helper Class

The whole activation-context-with-manifest thing is handled by this little helper class.
Yes, yes, it’s a hack.

class ActivationContext

{

 

    /// <summary>

    /// Create an instance of a COM object given the GUID of its class

    /// and a filepath of a client manifest (AKA an application manifest.)

    /// </summary>

    /// <param name="guid">GUID = CLSID of the COM object, {NNNN...NNN}</param>

    /// <param name="manifest">full path of manifest to activate, should list the

    /// desired COM class as a dependentAssembly.</param>

    /// <returns>An instance of the specified COM class, or null.</returns>

    static public object CreateInstanceWithManifest(Guid guid, string manifest)

    {

        object comob = null;

        ActivationContext.UsingManifestDo(manifest, delegate()

        {

            // Get the type object associated with the CLSID.

            Type T = Type.GetTypeFromCLSID(guid);

            // Create an instance of the type:

            comob = System.Activator.CreateInstance(T);

        });

        return comob;

    }

 

    public delegate void doSomething();

 

    static public void UsingManifestDo(string manifest, doSomething thingToDo)

    {

        UnsafeNativeMethods.ACTCTX context = new UnsafeNativeMethods.ACTCTX();

        context.cbSize = Marshal.SizeOf(typeof(UnsafeNativeMethods.ACTCTX));

        if (context.cbSize != 0x20)

        {

            throw new Exception("ACTCTX.cbSize is wrong");

        }

        context.lpSource = manifest;

 

        IntPtr hActCtx = UnsafeNativeMethods.CreateActCtx(ref context);

        if (hActCtx == (IntPtr)(-1))

        {

            throw new Win32Exception();

        }

        try // with valid hActCtx

        {

            IntPtr cookie = IntPtr.Zero;

            if (!UnsafeNativeMethods.ActivateActCtx(hActCtx, out cookie))

            {

                throw new Win32Exception();

            }

            try // with activated context

            {

                thingToDo();

            }

            finally

            {

                UnsafeNativeMethods.DeactivateActCtx(0, cookie);

            }

        }

        finally

        {

            UnsafeNativeMethods.ReleaseActCtx(hActCtx);

        }

    }

 

    [SuppressUnmanagedCodeSecurityAttribute]

    internal static class UnsafeNativeMethods

    {

        // Activation Context API Functions

        [DllImport("Kernel32.dll", SetLastError = true, EntryPoint = "CreateActCtxW")]

        internal extern static IntPtr CreateActCtx(ref ACTCTX actctx);

 

        [DllImport("Kernel32.dll", SetLastError = true)]

        [return: MarshalAs(UnmanagedType.Bool)]

        internal static extern bool ActivateActCtx(IntPtr hActCtx, out IntPtr lpCookie);

 

        [DllImport("kernel32.dll", SetLastError = true)]

        [return: MarshalAs(UnmanagedType.Bool)]

        internal static extern bool DeactivateActCtx(int dwFlags, IntPtr lpCookie);

 

        [DllImport("Kernel32.dll", SetLastError = true)]

        internal static extern void ReleaseActCtx(IntPtr hActCtx);

 

        // Activation context structure

        [StructLayout(LayoutKind.Sequential, Pack = 4, CharSet = CharSet.Unicode)]

        internal struct ACTCTX

        {

            public Int32 cbSize;

            public UInt32 dwFlags;

            public string lpSource;

            public UInt16 wProcessorArchitecture;

            public UInt16 wLangId;

            public string lpAssemblyDirectory;

            public string lpResourceName;

            public string lpApplicationName;

            public IntPtr hModule;

        }

 

    }

}

 

3. Create an Instance of the Control, in the Activation Context

Trivial once everything is set up right – see CreateInstanceWithManifest above.

4. Pop the Activation Context off the Context Stack and Destroy it

And the cleanup, which apparently can cause ugly runtime weirdness if not done properly, can be seen in the finally clauses of UsingManifestDo above.

Test SetUp

Once we have that ActivationContext helper class, setting up and running tests (other than event handling…) becomes pretty simple.

Here’s the function in the unit test class that uses that helper class to instantiate an EZTwainX object, using EZTwainX’s GUID and the application manifest file:

private object CreateEZTwainX()

{

    object eztwain = null;

    string manifest = System.IO.Path.Combine(targetDir, @"eztwainx.client.manifest");

    try

    {

        eztwain = Atalasoft.Tests.ActivationContext.CreateInstanceWithManifest(

            new Guid("74F4F118-91E6-4AFC-B8D2-04066781F239"), manifest);

    }

    catch (Exception)

    {

        Console.WriteLine("EZTwainX creation failed");

        throw;

    }

    return eztwain;

}

 

IC90098This function returns a .NET object known as an RCW or Runtime Callable Wrapper, which wraps around and mediates between .NET, and a COM object - in this case an instance of the EZTwainX control. Keep in mind this returned object has type System.Object. It is absolutely not a literal pointer to a COM object or COM interface. It holds, internally, some kind of pointer to the COM object, probably the IUnknown.  When you make calls using InvokeMember through the RCW they are translated into COM calls.  Normally events coming out of the COM object are translated back to .NET by the RCW, but I was unable to get that to work - we’ll get into that in Part B.

Finally: Actual Testing of Methods and Properties

Once we’ve created an EZTwainX control this way, making calls to methods and getting and setting properties is fairly straightforward.  For example, here’s the beginning of the SetUp method of our test class. This method is run by NUnit before running each designated test method:

[SetUp]

public void SetUp()

{

    // Per-test initialization.

    // Create an instance of the control:

    eztwain = CreateEZTwainX();

    EZTwainX = eztwain.GetType();

    string version = (string)EZTwainX.InvokeMember("VersionString", BindingFlags.GetProperty, null, eztwain, null);

    Console.WriteLine("EZTwainX VersionString='{0}'", version);

    // Extract sub-objects

    scan = EZTwainX.InvokeMember("Scan", BindingFlags.GetProperty, null, eztwain, null);

    Assert.IsNotNull(scan);

    IScan = scan.GetType();

 

To use InvokeMember, we need both the eztwain object and - kind of weirdly - its System.Type – kept around in the EZTwainX variable – which we can get any time with eztwain.GetType().

For example, the first call to InvokeMember above gets the value of the property named ‘VersionString’ of the object eztwain. We convert that to a .NET string and log it to the test output log.  This fails at runtime if the value of that property can’t be reasonably interpreted as a string.

By the way, the Assert.IsNotNull(scan) – that’s a call to the NUnit framework, to do an actual unit test thing. If scan is null at that point, the test immediately exceptions-out and fails, recording a semi-informative message in a build log and causing the entire build to be flagged as failing.  Lights begin flashing, sirens go off, Nerf launchers pop up and enter targeting mode – the joy of Continuous Integration.  I have to say, I’ve grown very fond of NUnit.

Here are examples of setting a property and calling a method respectively:

EZTwainX.InvokeMember("AppTitle", BindingFlags.SetProperty, null, eztwain, new object[] { "EZTwainX Tests" });

bool success = (bool)EZTwainX.InvokeMember("AcquireSingleImage", BindingFlags.InvokeMethod, null, eztwain, null);

 

Aside: It seems likely that the new dynamic type in Visual C# 2010 might obviate the whole EZTWainX.InvokeMember thing, allowing us to write e.g.

bool success = (bool)eztwain.AcquireSingleImage();

 

but… we’re still on .NET 3.5.

Conclusion

Whew!  That’s a lot. Let’s save Part B, capturing events in C# from a dynamically instantiated ActiveX control, for the next blog post.  Obviously if you just can’t wait for that, comment here, or put a watch on my stackoverflow question about this which I’ll be self-answering shortly…

Random Afterthoughts

The application manifest file, eztwainx.client.manifest, has to agree with the control’s manifest regarding the version number of the control.

It seemed to make life simpler to roll the control’s eztwainx.ocx.manifest file into the .cab archive, so that’s what we did.  That way when we unpack the .cab, that manifest ends up alongside the eztwain.ocx that it describes, which is where we need it.  We SCC the eztwainx.ocx.manifest as part of the EZTwainX control, so there’s a better chance we’ll remember to update it when we bump the control’s version ;-)

Somehow you need to get the application manifest file copied alongside the executing test assembly.  I do this by literally copying the file at runtime with file I/O operations. In your dev environment, you might find a simpler way.

Posted: 2/6/2012 4:26:04 PM by Spike McLarty | with 0 comments


Trackback URL: http://www.atalasoft.com/trackback/e3b197d9-6295-4e11-8026-b36a45cad4af/Dynamically-Testing-an-ActiveX-Control-from-C-and-NUnit.aspx?culture=en-US

Comments
Blog post currently doesn't have any comments.

Subscribe

Register to receive our monthly newsletter.
preload preload preload