Chapter 23. Programming .NET and COM

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.

Importing ActiveX Controls

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.

Creating an ActiveX Control

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.

Creating a VB ActiveX control
Figure 23-1. Creating a VB ActiveX control

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.

Example 23-1. Implementing the CalcControl ActiveX control
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.

Adding the CalcControl to the VB 6 toolbox
Figure 23-2. Adding the CalcControl to the VB 6 toolbox

This action puts a new control on the toolbox, as shown circled in Figure 23-3.

Locating CalcControl in the VB 6 toolbox
Figure 23-3. Locating CalcControl in the VB 6 toolbox

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.

Building the TestForm user interface
Figure 23-4. Building the TestForm user interface

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.

Example 23-2. Using the CalcControl ActiveX control in a VB program (TestForm)
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

Importing a Control in .NET

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.

Building a Windows Form to test the CalcControl ActiveX control
Figure 23-5. Building a Windows Form to test the CalcControl ActiveX control

Importing a control

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.

Adding the CalcControl to the toolbox
Figure 23-6. Adding the CalcControl to the toolbox

Manually importing the control

Alternatively, you can open a command box and import the control manually using the Aximp.exe utility, as shown in Figure 23-7.

Running Aximp
Figure 23-7. Running Aximp

Aximp.exe takes one argument, the ActiveX control you want to import (CalcControl.ocx). It produces three files:

AxCalcControl.dll

A .NET Windows control

CalcControl.dll

A proxy .NET class library

AxCalcControl.pdb

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.

Browsing for the imported control
Figure 23-8. Browsing for the imported control

Adding the control to the form

Once imported, the control appears on the toolbox menu, as shown in Figure 23-9.

New control in the toolbox
Figure 23-9. New control in the toolbox

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.

Example 23-3. Implementing 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.

Testing the Interop control
Figure 23-10. Testing the Interop control

P/Invoke

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:

DLL name

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.

Example 23-4. Using P/Invoke to call a Win32 API method
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.

Pointers

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.

Table 23-1. C# pointer operators

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.

Checking “Allow unsafe code”
Figure 23-11. Checking “Allow unsafe code”

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.

Tip

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

Example 23-5. Using pointers in a C# program
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."


[21] * In fact, this is what Fileinfo.Move( ) is doing itself.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.67.203