Programmers love a clean slate. Although it would be nice if we could throw away all the code we’ve ever written and start over, this typically isn’t a viable option for most companies. Over the past decade, many development organizations have made a substantial investment in developing and purchasing COM components and ActiveX controls. Microsoft has made a commitment to ensure that these legacy components are usable from within .NET applications, and (perhaps less important) that .NET components are easily callable from COM.
This chapter describes the support .NET provides for importing ActiveX controls and COM components into your application, exposing .NET classes to COM-based applications, and making direct calls to Win32 APIs. You’ll also learn about C# pointers and keywords for accessing memory directly; this may be crucial in some applications.
ActiveX controls are COM components typically dropped into a form, which might or might not have a user interface. When Microsoft developed the OCX standard, which allowed developers to build ActiveX controls in VB and use them with C++ (and vice versa), the ActiveX control revolution began. Over the past few years, thousands of such controls have been developed, sold, and used. They are small, easy to work with, and an effective example of binary reuse. Importing ActiveX controls into .NET is surprisingly easy, considering how different COM objects are from .NET objects. Visual Studio 2008 is able to import ActiveX controls automagically. As an alternative to using Visual Studio, Microsoft has developed a command-line utility, Aximp
, which will create the assemblies necessary for the control to be used in a .NET application.
To demonstrate the ability to use classic ActiveX controls in a .NET application, you’ll first develop a simple four-function calculator as an ActiveX control and then invoke that ActiveX control from within a C# application. You’ll build the control in VB 6, and test it in a VB 6 application. If you don’t have VB 6 or don’t want to bother creating the control, you can download the control from http://www.JesseLiberty.com. (Click on the Book site, then click on Books. Navigate to this book, and click on the source code.) Once the control is working in the standard Windows environment, you’ll import it into your Windows Forms application.
To create the control, open VB 6 and choose ActiveX control as the new project type. Make the project form as small as possible because this control will not have a user interface. Right-click UserControl1
and choose Properties. Rename it Calculator
in the Properties window. Click the Project in the Project Explorer, and in the Properties window, rename it CalcControl
. Immediately save the project and name both the file and the project CalcControl
, as shown in Figure 23-1.
You can add the four calculator functions by right-clicking the CalcControl
form, selecting View Code from the pop-up menu, and typing the VB code shown in Example 23-1.
Public Function _ Add(left As Double, right As Double) _ As Double Add = left + right End Function Public Function _ Subtract(left As Double, right As Double) _ As Double Subtract = left − right End Function Public Function _ Multiply(left As Double, right As Double) _ As Double Multiply = left * right End Function Public Function _ Divide(left As Double, right As Double) _ As Double Divide = left / right End Function
This is the entire code for the control. Compile this to the CalcControl.ocx file by choosing File 艉 Make CalcControl.ocx on the VB 6 menu bar. Open a second project in VB as a standard executable (EXE). Name the form TestForm
, and name the project CalcTest
. Save the file and project as CalcTest
. Add the ActiveX control as a component by pressing Ctrl-T and choosing CalcControl
from the Controls tab, shown in Figure 23-2.
This action puts a new control on the toolbox, as shown circled in Figure 23-3.
Drag the new control onto the form TestForm
and name it CalcControl
. Note that the new control will not be visible. This control has no user interface. Add two text boxes, four buttons, and one label, as shown in Figure 23-4.
Name the buttons btnAdd
, btnSubtract
, btnMultiply
, and btnDivide
. All that is left is for you to implement methods for handling the button-click events of the calculator buttons.
Each time a button is clicked, you want to get the values in the two text boxes, cast them to double
(as required by CalcControl
) using the VB 6 CDbl
function, invoke a CalcControl
function, and print the result in the label control. Example 23-2 provides the complete source code.
Private Sub btnAdd_Click( ) Label1.Caption = _ calcControl.Add(CDbl(Text1.Text), _ CDbl(Text2.Text)) End Sub Private Sub btnDivide_Click( ) Label1.Caption = _ calcControl.Divide(CDbl(Text1.Text), _ CDbl(Text2.Text)) End Sub Private Sub btnMultiply_Click( ) Label1.Caption = _ calcControl.Multiply(CDbl(Text1.Text), _ CDbl(Text2.Text)) End Sub Private Sub btnSubtract_Click( ) Label1.Caption = _ calcControl.Subtract(CDbl(Text1.Text), _ CDbl(Text2.Text)) End Sub
Now that you’ve shown that the CalcControl
ActiveX control is working, you can copy the CalcControl.ocx file to your .NET development environment. Once you have copied it, remember that the CalcControl.ocx file requires that you register it using Regsvr32
(if you are running Vista, you’ll need to do this as an administrator):
Regsvr32 CalcControl.ocx
You’re now ready to build a test program in .NET to use the calculator.
To get started, create a Visual C# Windows Forms application in Visual Studio 2008, name the application InteropTest
, and design a form (such as the TestForm
form you created in VB in the preceding section) by dragging and dropping controls onto it. Name the form TestForm
. Figure 23-5 shows a complete sample form.
There are two ways to import an ActiveX control into the Visual Studio 2008 development environment: you can use the Visual Studio 2008 tools themselves, or you can import the control manually using the Aximp
utility that ships with the .NET Framework SDK. To use Visual Studio 2008, right-click on the toolbox and add a tab named COM
. Then right-click again and select Choose Items. This will bring up the Choose Toolbox Items dialog box. Select the COM Components tab, as shown in Figure 23-6.
Alternatively, you can open a command box and import the control manually using the Aximp.exe
utility, as shown in Figure 23-7.
Aximp.exe
takes one argument, the ActiveX control you want to import (CalcControl.ocx). It produces three files:
A .NET Windows control
A proxy .NET class library
A debug file
Once this is done, you can return to the Choose Toolbox Items window, but this time, select .NET Framework Components. You can now browse to the location at which the .NET Windows control AxCalcControl.dll was generated and import that file into the toolbox, as shown in Figure 23-8.
Once imported, the control appears on the toolbox menu, as shown in Figure 23-9.
Now, you can drag this control onto your Windows Form and make use of its functions, just as you did in the VB 6 example.
Add event handlers for each of the four buttons. The event handlers will delegate their work to the ActiveX control you wrote in VB 6 and imported into .NET.
Example 23-3 shows the source code for the event handlers.
using System; using System.Windows.Forms; namespace InteropTest { public partial class Form1 : Form { public Form1( ) { InitializeComponent( ); } private void btnAdd_Click(object sender, EventArgs e) { double left = double.Parse(textBox1.Text); double right = double.Parse(textBox2.Text); label1.Text = axCalculator1.Add(ref left, ref right).ToString( ); } private void btnSubtract_Click(object sender, EventArgs e) { double left = double.Parse(textBox1.Text); double right = double.Parse(textBox2.Text); label1.Text = axCalculator1.Subtract(ref left, ref right).ToString( ); } private void btnMultiply_Click(object sender, EventArgs e) { double left = double.Parse(textBox1.Text); double right = double.Parse(textBox2.Text); label1.Text = axCalculator1.Multiply(ref left, ref right).ToString( ); } private void btnDivide_Click(object sender, EventArgs e) { double left = double.Parse(textBox1.Text); double right = double.Parse(textBox2.Text); label1.Text = axCalculator1.Divide(ref left, ref right).ToString( ); } } }
Each implementing method obtains the values in the text fields, converts them to double using the static method double.Parse( )
, and passes those values to the calculator’s methods. The results are cast back to a string and inserted in the label, as shown in Figure 23-10.
It is possible to invoke unmanaged code from within C#. Typically, you would do this if you needed to accomplish something you couldn’t accomplish through the FCL. With the 2.0 version of .NET, the use of P/Invoke will become relatively rare.
The .NET platform invoke facility (P/Invoke) was originally intended only to provide access to the Windows API, but you can use it to call functions in any DLL.
To see how this works, let’s revisit Example 22-3 from Chapter 22. You will recall that you used the FileInfo
class to rename files by invoking the MoveTo( )
method:
file.MoveTo(fullName + ".bak");
You can accomplish the same thing by using the Windows kernel32.dll and invoking the MoveFile
method.[21] To do so, you need to declare the method as a static extern
and use the DllImport
attribute:
[DllImport("kernel32.dll", EntryPoint="MoveFile", ExactSpelling=false, CharSet=CharSet.Unicode, SetLastError=true)] static extern bool MoveFile( string sourceFile, string destinationFile);
The DllImport
attribute class is used to indicate that an unmanaged method will be invoked through P/Invoke. The parameters are as follows:
The name of the DLL you are invoking.
EntryPoint
Indicates the name of the DLL entry point (the method) to call.
ExactSpelling
Allows the CLR to match methods with slightly different names based on the CLR’s knowledge of naming conventions.
CharSet
Indicates how the string arguments to the method should be marshaled.
SetLastError
Setting this to true
allows you to call Marshal.GetLastWin32 Error
, and check whether an error occurred when invoking this method.
The rest of the code is virtually unchanged, except for the invocation of the MoveFile( )
method itself. Notice that MoveFile( )
is declared to be a static method of the class, so use static method semantics:
Tester.MoveFile(file.FullName,file.FullName + ".bak");
Pass in the original filename and the new name, and the file is moved, just as it was when calling file.MoveTo( )
. In this example, there is no advantage—and actually considerable disadvantage—to using P/Invoke. You have left managed code, and the result is that you’ve abandoned type safety and your code will no longer run in “partial-trusted” scenarios. Example 23-4 shows the complete source code for using P/Invoke to move the files.
using System; using System.IO; using System.Runtime.InteropServices; namespace UsingPInvoke { class Tester { // declare the WinAPI method you wish to P/Invoke [DllImport("kernel32.dll", EntryPoint = "MoveFile", ExactSpelling = false, CharSet = CharSet.Unicode, SetLastError = true)] static extern bool MoveFile( string sourceFile, string destinationFile); public static void Main( ) { // make an instance and run it Tester t = new Tester( ); string theDirectory = @"c: estmedia"; DirectoryInfo dir = new DirectoryInfo(theDirectory); t.ExploreDirectory(dir); } // Set it running with a directory name private void ExploreDirectory(DirectoryInfo dir) { // make a new subdirectory string newDirectory = "newTest"; DirectoryInfo newSubDir = dir.CreateSubdirectory(newDirectory); // get all the files in the directory and // copy them to the new directory FileInfo[] filesInDir = dir.GetFiles( ); foreach (FileInfo file in filesInDir) { string fullName = newSubDir.FullName + "\" + file.Name; file.CopyTo(fullName); Console.WriteLine("{0} copied to newTest", file.FullName); } // get a collection of the files copied in filesInDir = newSubDir.GetFiles( ); // delete some and rename others int counter = 0; foreach (FileInfo file in filesInDir) { string fullName = file.FullName; if (counter++ % 2 == 0) { // P/Invoke the Win API Tester.MoveFile(fullName, fullName + ".bak"); Console.WriteLine("{0} renamed to {1}", fullName, file.FullName); } else { file.Delete( ); Console.WriteLine("{0} deleted.", fullName); } } // delete the subdirectory newSubDir.Delete(true); } } } Output: c: estmediachimes.wav copied to newTest c: estmediachord.wav copied to newTest c: estmediadesktop.ini copied to newTest c: estmediading.wav copied to newTest c: estmediadts.wav copied to newTest c: estmediaflourish.mid copied to newTest c: estmediair_begin.wav copied to newTest c: estmediair_end.wav copied to newTest c: estmediair_inter.wav copied to newTest c: estmedia otify.wav copied to newTest c: estmediaonestop.mid copied to newTest c: estmedia ecycle.wav copied to newTest c: estmedia ingout.wav copied to newTest c: estmediaSpeech Disambiguation.wav copied to newTest c: estmediaSpeech Misrecognition.wav copied to newTest c: estmedia ewTestchimes.wav renamed to c: estmedia ewTestchimes.wav c: estmedia ewTestchord.wav deleted. c: estmedia ewTestdesktop.ini renamed to c: estmedia ewTestdesktop.ini c: estmedia ewTestding.wav deleted. c: estmedia ewTestdts.wav renamed to c: estmedia ewTestdts.wav c: estmedia ewTestflourish.mid deleted. c: estmedia ewTestir_begin.wav renamed to c: estmedia ewTestir_begin.wav c: estmedia ewTestir_end.wav deleted. c: estmedia ewTestir_inter.wav renamed to c: estmedia ewTestir_inter.wav c: estmedia ewTest otify.wav deleted. c: estmedia ewTestonestop.mid renamed to c: estmedia ewTestonestop.mid c: estmedia ewTest ecycle.wav deleted. c: estmedia ewTest ingout.wav renamed to c: estmedia ewTest ingout.wav c: estmedia ewTestSpeech Disambiguation.wav deleted.
Until now, you’ve seen no code using C-/C++-style pointers. Only here, in the final paragraphs of the final pages of the book, does this topic arise, even though pointers are central to the C family of languages. In C#, pointers are relegated to unusual and advanced programming; typically, they are used only with P/Invoke.
C# supports the usual C pointer operators, listed in Table 23-1.
Operator | Meaning |
& | The address-of operator returns a pointer to the address of a value |
* | The dereference operator returns the value at the address of a pointer |
-> | The member access operator is used to access the members of a type |
The use of pointers is almost never required, and is nearly always discouraged. When you do use pointers, you must mark your code with the C# unsafe
modifier. The code is marked unsafe because you can manipulate memory locations directly with pointers. This is a feat that is otherwise impossible within a C# program. In unsafe code, you can directly access memory, perform conversions between pointers and integral types, take the address of variables, and so forth. In exchange, you give up garbage collection and protection against uninitialized variables, dangling pointers, and accessing memory beyond the bounds of an array. In essence, unsafe code creates an island of C++ code within your otherwise safe C# application, and your code will not work in partial-trust scenarios.
As an example of when this might be useful, read a file to the console by invoking two Win32 API calls: CreateFile
and ReadFile
. ReadFile
takes, as its second parameter, a pointer to a buffer. The declaration of the two imported methods is straightforward:
[DllImport("kernel32", SetLastError=true)] static extern unsafe int CreateFile( string filename, uint desiredAccess, uint shareMode, uint attributes, uint creationDisposition, uint flagsAndAttributes, uint templateFile); [DllImport("kernel32", SetLastError=true)] static extern unsafe bool ReadFile( int hFile, void* lpBuffer, int nBytesToRead, int* nBytesRead, int overlapped);
You will create a new class, APIFileReader
, whose constructor will invoke the CreateFile( )
method. The constructor takes a filename as a parameter, and passes that filename to the CreateFile( )
method:
public APIFileReader(stringfilename
)
{
fileHandle = CreateFile(
filename, // filename
GenericRead, // desiredAccess
UseDefault, // shareMode
UseDefault, // attributes
OpenExisting, // creationDisposition
UseDefault, // flagsAndAttributes
UseDefault); // templateFile
}
The APIFileReader
class implements only one other method, Read( )
, which invokes ReadFile( )
. It passes in the file handle created in the class constructor, along with a pointer into a buffer, a count of bytes to retrieve, and a reference to a variable that will hold the number of bytes read. It is the pointer to the buffer that is of interest to us here. To invoke this API call, you must use a pointer.
Because you will access it with a pointer, the buffer needs to be pinned
in memory; the .NET Framework can’t be allowed to move the buffer during garbage collection. To accomplish this, use the C# fixed
keyword. fixed
allows you to get a pointer to the memory used by the buffer, and to mark that instance so that the garbage collector won’t move it.
The block of statements following the fixed
keyword creates a scope, within which the memory will be pinned. At the end of the fixed
block, the instance will be unmarked so that it can be moved. This is known as declarative pinning:
public unsafe int Read(byte[] buffer, int index, int count) { int bytesRead = 0; fixed (byte* bytePointer = buffer) { ReadFile( fileHandle, bytePointer + index, count, &bytesRead, 0); } return bytesRead; }
Notice that the method must be marked with the unsafe
keyword. This creates an unsafe context and allows you to create pointers. To compile this you must use the /unsafe
compiler option. The easiest way to do so is to open the project properties, click the Build tab, and check the “Allow unsafe code” checkbox, as shown in Figure 23-11.
The test program instantiates the APIFileReader
and an ASCIIEncoding
object. It passes the filename (8Swnn10.txt) to the constructor of the APIFileReader
and then creates a loop to repeatedly fill its buffer by calling the Read( )
method, which invokes the ReadFile
API call. An array of bytes is returned, which is converted to a string using the ASCIIEncoding
object’s GetString( )
method. That string is passed to the Console.Write( )
method, to be displayed on the console. Example 23-5 shows the complete source.
The text that it will read is a short excerpt of Swann’s Way (by Marcel Proust), currently in the public domain and downloaded as text from Project Gutenberg (http://www.gutenberg.org/wiki/Main_Page).
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace UsingPointers
{
class APIFileReader
{
const uint GenericRead = 0x80000000;
const uint OpenExisting = 3;
const uint UseDefault = 0;
int fileHandle;
[DllImport("kernel32", SetLastError = true)]
static extern unsafe int CreateFile(
string filename,
uint desiredAccess,
uint shareMode,
uint attributes,
uint creationDisposition,
uint flagsAndAttributes,
uint templateFile);
[DllImport("kernel32", SetLastError = true)]
static extern unsafe bool ReadFile(
int hFile,
void* lpBuffer,
int nBytesToRead,
int* nBytesRead,
int overlapped);
// constructor opens an existing file
// and sets the file handle member
public APIFileReader(string filename)
{
fileHandle = CreateFile(
filename, // filename
GenericRead, // desiredAccess
UseDefault, // shareMode
UseDefault, // attributes
OpenExisting, // creationDisposition
UseDefault, // flagsAndAttributes
UseDefault); // templateFile
}
public unsafe int Read(byte[] buffer, int index, int count)
{
int bytesRead = 0;
fixed (byte* bytePointer = buffer)
{
ReadFile(
fileHandle, // hfile
bytePointer + index, // lpBuffer
count, // nBytesToRead
&bytesRead, // nBytesRead
0); // overlapped
}
return bytesRead;
}
}
class Test
{
public static void Main( )
{
// create an instance of the APIFileReader,
// pass in the name of an existing file
APIFileReader fileReader =new APIFileReader("8Swnn10.txt");
// create a buffer and an ASCII coder
const int BuffSize = 128;
byte[] buffer = new byte[BuffSize];
ASCIIEncoding asciiEncoder = new ASCIIEncoding( );
// read the file into the buffer and display to console
while (fileReader.Read(buffer, 0, BuffSize) != 0)
{
Console.Write("{0}", asciiEncoder.GetString(buffer));
}
}
}
}
The key section of code where you create a pointer to the buffer and fix that buffer in memory using the fixed
keyword is shown in bold. You need to use a pointer here because the API call demands it.
Output:
Altogether, my aunt used to treat him with scant ceremony. Since she was of the opinion that he ought to feel flattered by our invitations, she thought it only right and proper that he should never come to see us in summer without a basket of peaches or raspberries from his garden, and that from each of his visits to Italy he should bring back some photographs of old masters for me. It seemed quite natural, therefore, to send to him whenever we wanted a recipe for some special sauce or for a pineapple salad for one of our big dinner-parties, to which he himself would not be invited, not seeming of sufficient importance to be served up to new friends who might be in our house for the first time. If the conversation turned upon the Princes of the House of France, "Gentlemen, you and I will never know, will we, and don't want to, do we?" my great-aunt would say tartly to Swann, who had, perhaps, a letter from Twickenham in his pocket; she would make him play accompaniments and turn over music on evenings when my grandmother's sister sang; manipulating this creature, so rare and refined at other times and in other places, with the rough simplicity of a child who will play with some curio from the cabinet no more carefully than if it were a penny toy. Certainly the Swann who was a familiar figure in all the clubs of those days differed hugely from, the Swann created in my great-aunt's mind when, of an evening, in our little garden at Combray, after the two shy peals had sounded from the gate, she would vitalise, by injecting into it everything she had ever heard about the Swann family, the vague and unrecognisable shape which began to appear, with my grandmother in its wake, against a background of shadows, and could at last be identified by the sound of its voice. But then, even in the most insignificant details of our daily life, none of us can be said to constitute a material whole, which is identical for everyone, and need only be turned up like a page in an account-book or the record of a will; our social personality is created by the thoughts of other people. Even the simple act which we describe as "seeing some one we know" is, to some extent, an intellectual process. We pack the physical outline of the creature we see with all the ideas we have already formed about him, and in the complete picture of him which we compose in our minds those ideas have certainly the principal place. In the end they come to fill out so completely the curve of his cheeks, to follow so exactly the line of his nose, they blend so harmoniously in the sound of his voice that these seem to be no more than a transparent envelope, so that each time we see the face or hear the voice it is our own ideas of him which we recognise and to which we listen. And so, no doubt, from the Swann they had built up for their own purposes my family had left out, in their ignorance, a whole crowd of the details of his daily life in the world of fashion, details by means of which other people, when they met him, saw all the Graces enthroned in his face and stopping at the line of his arched nose as at a natural frontier; but they contrived also to put into a face from which its distinction had been evicted, a face vacant and roomy as an untenanted house, to plant in the depths of its unvalued eyes a lingering sense, uncertain but not unpleasing, half-memory and half-oblivion, of idle hours spent together after our weekly dinners, round the card-table or in the garden, during our companionable country life. Our friend's bodily frame had been so well lined with this sense, and with various earlier memories of his family, that their own special Swann had become to my people a complete and living creature; so that even now I have the feeling of leaving some one I know for another quite different person when, going back in memory, I pass from the Swann whom I knew later and more intimately to this early Swann—this early Swann in whom I can distinguish the charming mistakes of my childhood, and who, incidentally, is less like his successor than he is like the other people I knew at that time, as though one's life were a series of galleries in which all the portraits of any one period had a marked family likeness, the same (so to speak) tonality—this early Swann abounding in leisure, fragrant with the scent of the great chestnut-tree, of baskets of raspberries and of a sprig of tarragon. And yet one day, when my grandmother had gone to ask some favour of a lady whom she had known at the Sacré Coeur (and with whom, because of our caste theory, she had not cared to keep up any degree of intimacy in spite of several common interests), the Marquise de Villeparisis, of the famous house of Bouillon, this lady had said to her: "I think you know M. Swann very well; he is a great friend of my nephews, the des Laumes."
3.138.67.203