The Distributed Component Object Model (DCOM) allows you to share objects easily over a network. In this chapter, you will see how to use BCB to implement DCOM. In particular, you will see how to create two applications that can control one another across a network and how to create distributed database applications. In the process of describing this technology, I will also show you how to create local OLE Automation objects.
BCB and Windows NT 4.0 provide full support for DCOM. You can also easily add DCOM client support to Windows 95. If you want, you can also add DCOM server support to Windows 95, though this option has some drawbacks and limitations.
The following subjects are covered in this chapter:
At the time of this writing, DCOM is not built into Windows 95, although it is built into Windows NT 4.0. To add DCOM support to Windows 95, you can download Windows 95 DCOM from the Microsoft Web server. You should start looking for it in the OleDev section: www.microsoft.com/oledev. Alternatively, you can purchase a product called OLE Enterprise (OLEnterprise) from Borland.
Before I begin describing the fairly simple technical steps involved in implementing DCOM, perhaps I should talk about this technology from a high level so that all the key ideas will be clear to everyone. If you already understand DCOM and just want to see how to implement it in BCB, you can skip this section.
Distributed COM is important because it allows applications to talk to one another across a network. In particular, it allows you to share objects that reside on two separate machines. You therefore can create an object in one application or DLL and then call the methods of that object from an application or DLL that resides on a different computer.
DCOM is built on top of COM, which is the technology that underlies both OLE and ActiveX. The relationship between COM and OLE is a bit confusing, and the boundaries separating the two technologies seem to shift at times, depending on the vagaries of the Microsoft marketing machine. In general, I can safely say that OLE is a subset of COM. That is, everything that is part of OLE is also part of COM, but not everything that is part of COM is a part of OLE. Many people, however, use the words "COM" and "OLE" virtually interchangeably, and indeed, the two technologies are very closely bound together.
COM is simply a specification for defining an object hierarchy. In particular, it lays out a set of rules for defining objects that can be used across applications and languages. DCOM extends this specification to allow objects on separate machines to talk to one another.
One of the most important aspects of this technology is that it allows you to distribute the load of a task across several machines. For example, if you have a complex database query to run, you can use DCOM to ask an object on a separate machine to run it. That way, your current processor will not have to expend any clock cycles on the task, nor will any large database- related tools be loaded into memory on your own machine. You are therefore free to continue playing DOOM or Quake while your server loses clock cycles and precious RAM to your background task.
You need to understand that the COM specification, as the name itself implies, is really only a set of rules for defining an object hierarchy. These rules include defining the names and methods of many of the key objects in the hierarchy, as well as the specific techniques for structuring the objects themselves. (When you think of the definition this way, you might find some value in regarding OLE as an implementation of certain parts of the COM specification.)
The COM specification can be compared to the specification for other object hierarchies such as VCL, OWL, or MFC. For example, all COM objects descend from a base class called IUnknown, just as all VCL objects descend from a base class called TObject. COM supports polymorphism and encapsulation, and it uses a series of unique interfaces to achieve the same ends as traditional inheritance in standard object-oriented languages. Unlike OWL or VCL, however, COM is not tied to any particular language, nor is it bound by application boundaries.
In short, COM is an alternative to the VCL, OWL, or MFC that attempts to go these object hierarchies one better by allowing you to use COM objects across language, application, and now even machine boundaries. As a result, you can write a COM object in Object Pascal, extend it in BCB, and then use it in a third language such as Visual Basic. You can call methods of the object from inside a single application, from one application that is calling into a DLL, or from one application that is calling an object in a separate application. What DCOM brings to the picture is the capability to call objects that reside in applications located on separate machines. It allows you to "distribute" the objects across the network. In particular, this capability allows you to divide up the load of running a major task across several machines.
If you already understand COM, then you are ready to use DCOM without any further work. DCOM works exactly the same way that COM works. You can, in fact, at least theoretically convert existing COM objects into DCOM objects with no change to your code. This system works great between two Windows NT machines, but Windows 95 requires that you switch from Share Level to User Level access, which might crimp your style in some cases. In particular, User Level sharing requires that an NT machine or some other source of user access lists be available on your network.
NOTE: You can switch from Share Level access to User Level access via the Network applet found in the Control Panel. To then help configure your server, you can use the DComCfg.exe application freely available from Microsoft's Web server.
If you don't want to switch to User Level access, you can choose an alternative that will allow you to run DCOM as a client service on Windows 95 machines. In that case, you need to add only a single new parameter to your calls into an OLE function named CoGetClassObject. I'll explain more in the technical section of this chapter.
NOTE: Consider these three points: n In most cases, User Level access controls require that you have an NT machine in your network. n NT machines don't require any special configuration to be able to act as a DCOM server. n You cannot remotely launch an OLE server that resides on a Windows 95 machine. The process must be in memory before you can call it. This is not true of NT machines, which can automatically launch a server if it is not already in memory. The upshot of this point is that you can't set up a DCOM server unless you have an NT machine on your network, and even under the best circumstances, a Windows 95 box is crippled as a server. Given these facts, I prefer to have the NT machine act as my DCOM server and to let the Windows 95 machines act only as clients. When connecting the two via DCOM for the first time, sign on to both machines with the same name and password. The simplest way to do this is to create a new user on the server with the same name and password you use on your Win95 box. Then sign on to the NT server with this name and password and sign on to your Windows 95 machine with the same name and password. Once you get things working this way, then you can try connecting with more stringent security.
The key point to grasp here is that DCOM is really nothing more than a new capability added to the already-existing COM technology. If you have working COM objects, upgrading them to work with DCOM is easy.
At the time of this writing (February '97), DCOM has been working on Windows NT 4.0 for over six months, but Microsoft has only just released DCOM for Windows 95. Microsoft has stated that, in the future, COM and DCOM will be ported to other platforms such as UNIX and the Mac.
Having, in a sense, made the case for COM and DCOM in the preceding section, I should perhaps step back for a moment and describe some competing systems. In this technical chapter, I'm not interested in advocating any particular system. However, describing the current state of this technology is probably worthwhile so that you can put this chapter in perspective.
COM is a Microsoft technology that is competing with similar technologies such as CORBA and DSOM, which are created by other corporations or groups of corporations. Adherents of these alternative technologies can rally numerous arguments regarding who implemented what first and who has designed the most sophisticated technology. Furthermore, many people have invested themselves heavily in technologies such as OWL or MFC that can be seen as "competing," in some sense, with COM. OWL, MFC, and the VCL don't have the same capabilities as COM, DSOM, OPENDOC, or CORBA, but you still get a feeling that the technologies are to some degree competing for the mind share of contemporary programmers. There is no specific reason that you can't use COM and VCL in the same program, and indeed that is the approach I take in this chapter.
My point here is not to advocate any particular solution, but only to make it clear that this controversial topic tends to excite strong opinions. If you're considering using COM in your projects, you might also want to look at CORBA and SOM. Conversely, if you hear criticisms of COM from other members working in the industry, you might check to see whether they are so heavily invested in some alternative technology that they are perhaps somewhat unfairly predisposed to be critical of COM and DCOM.
BCB programmers can use the IDispatch COM interface to gain easy access to the capabilities of DCOM. This interface is encapsulated inside the BCB TAutoObject class. If you understand TAutoObject and the theory behind IDispatch and IMarshal, you can probably skip this section and move on to the next.
OLE Automation is a technique that allows you to control one application from inside a second application. In particular, it allows you to control an object placed inside one application from the code of a second application.
The key to OLE Automation is a COM object called IDispatch. All OLE technologies are based on COM, and in this particular case the functionality behind OLE Automation is implemented by IDispatch. In short, OLE Automation is really just a marketing term for publicizing the technology found in IDispatch. Or, more charitably, OLE Automation is an implementation of the IDispatch specification.
IDispatch is not difficult to understand, but it can be a bit awkward at times to implement. To help simplify the use of IDispatch, the VCL has a class called TAutoObject that encapsulates all the functionality of IDispatch inside an easy-to-use and highly leveraged technology.
In this chapter, I focus much of the technical content on an analysis of TAutoObject. However, you can automate any COM object, not just IDispatch. I have chosen to concentrate on this one technology at the exclusion of others because it provides a simple workaround to the difficult problem of trying to marshal code and data back and forth between two applications.
Marshaling is a COM-specific term for the technique used to transfer data or function calls back and forth between two applications that reside in separate processes. For example, if you have to pass a parameter to a function between two applications, you have to be sure that it is treated properly by both applications. For example, if you declare the parameter as an Integer in Pascal, that means you're passing a four-byte ordinal value. How do you express that same concept in C? How do you do it Visual Basic? The answers to these questions are expressed in COM by a complex interface called IMarshal that is beyond the scope of this chapter. Indeed, IMarshal is notorious for being difficult to implement.
Here is how the Microsoft documentation defines IMarshal: "`Marshaling' is the process of packaging data into packets for transmission to a different process or machine. `Unmarshaling' is the process of recovering that data at the receiving end. In any given call, method arguments are marshaled and unmarshaled in one direction, while return values are marshaled and unmarshaled in the other." This is all good and well. Unfortunately, as I stated earlier, the IMarshal interface is very hard to implement.
If you're using a standard COM object, you don't have to implement IMarshal because these interfaces will be marshaled for you automatically by the system. In other words, if you're implementing an instance of IDispatch, IUnknown, IClassFactory, IOleContainer, or any other predefined COM class, you don't have to worry about marshaling. Microsoft will take care of this job for you. However, if you're creating a custom object of your own, you need to implement IMarshal or come up with some alternative scheme.
Because of the complexity of IMarshal, C programmers also generally choose not to attempt an implementation of IMarshal. Instead, they rely on an intermediate language called Interface Definition Language (IDL) that can be compiled into source code by a Microsoft-created program called MIDL.EXE. The IDL is a special language meant to allow people to define interfaces in a neutral language that can be compiled into source that can be used by multiple languages such as Pascal, C, and Visual Basic.
In other words, you can theoretically write your COM object in C or Pascal, use IDL to define its interface, and then use MIDL to turn that interface into a set of files that can be used by any language. In other words, MIDL automatically takes care of the IMarshal business for you as long as you first describe your interface in IDL.
This approach is quite reasonable, but I will not treat it in this current book, in part because BCB does not ship with MIDL. It is, however, available from the Microsoft SDKs and may be freely available via their Web site. You should also note that Delphi 3.0 includes tools that automate this process without your having to learn IDL or work with MIDL.
For now, however, I will back away from both IMarshal (because it is so complex) and MIDL (because it doesn't ship with BCB). This situation would appear to leave no good way to handle DCOM, were it not for the power of IDispatch and TAutoObject. IDispatch is a COM interface designed to make controlling one application from inside a second application easy. In implementing this code, Microsoft provided an alternative means for solving the whole problem of marshaling data between applications. In TAutoObject, the VCL provides a very simple means of using IDispatch.
Here is how Microsoft defines IDispatch: "IDispatch is a COM interface that is designed in such a way that it can call virtually any other COM interface." In other words, if you put a COM object in an application, you can call its methods from a second application by using IDispatch. This way, OLE Automation allows you to control one application from inside a second application. (In particular, IDispatch was created to help make COM programming easier from inside the limited confines of a Visual Basic application.)
To understand why IDispatch works, you need to remember that marshaling is taken care of for you automatically as long as you're using an existing COM interface. In other words, you don't have to implement marshaling for IDispatch because it is a standard COM object, not a custom object designed by yourself or someone on your team. IDispatch exists to allow you to call the methods of any legal COM object. In other words, it is designed to solve the whole problem of marshaling data. As such, it is the perfect solution for BCB programmers who want to use DCOM without engaging in too much manual labor.
Before closing this section, I should emphasize that you don't have to use IDispatch. If you prefer, you can use other predefined COM objects, or you can implement IMarshal, or you can attempt to use MIDL. BCB has none of the limitations found in languages like Visual Basic, so you don't need to be confined to using IDispatch unless you find its relative simplicity appealing.
Now you're ready to move away from theoretical issues and to concentrate instead on technical matters. Ironically, the theory behind this technology is much harder to understand than the technology itself. In short, in this section and the next, I outline a simple technique for using DCOM that can be used by any intermediate-level BCB programmer.
You learned in the preceding sections that TAutoObject is BCB's wrapper around IDispatch. IDispatch is the COM object that makes OLE Automation possible. In this section, I show you how to implement OLE Automation that works not only between two applications, but also between two applications that reside on separate machines.
If you go to the File menu in BCB and choose New, you can pop up the Object
Repository.
On the first page of the Object Repository is an icon you can select if you want
to create an Automation object. After selecting the Automation Object icon, you are
presented with a dialog, as shown in Figure 27.1.
FIGURE 27.1.
Selecting the Automation object from the Object Repository.
You can fill in the fields of this dialog as you like, or you can put in the following default values:
Class Name: TMyDCOM OLE Class Name: MyProj.MyDCom Description: My DCOM Object Instancing: Multiple Instance
When you're done, BCB spits out two pages of code as shown in
Listing 27.1 and
Listing 27.2. As you can see, I have, for the sake of fidelity to the compiler's
output and contrary to habit, left in the mystifying series of dashes inserted by
the compiler.
Listing 27.1. The header file produced
by the BCB OLE Automation Wizard.
//-------------------------------------------------------------------------- #ifndef Unit1H #define Unit1H //-------------------------------------------------------------------------- #include <vcl\OleAuto.hpp> #include <vcl\Classes.hpp> //-------------------------------------------------------------------------- class TMyDCom : public TAutoObject { private: public: __fastcall TMyDCom(); __automated: }; //-------------------------------------------------------------------------- #endif
Listing 27.2. The main source file produced by the OLE Automation Wizard.
//-------------------------------------------------------------------------- #include <vcl\vcl.h> #pragma hdrstop #include "Unit1.h" //-------------------------------------------------------------------------- __fastcall TMyDCom::TMyDCom() : TAutoObject() { } //-------------------------------------------------------------------------- void __fastcall RegisterTMyDCom() { TAutoClassInfo AutoClassInfo; AutoClassInfo.AutoClass = __classid(TMyDCom); AutoClassInfo.ProgID = "MyProj.MyDCom"; AutoClassInfo.ClassID = "{FCB9F540-87FF-11D0-BCD7-0080C80CF1D2}"; AutoClassInfo.Description = "My DCOM Object"; AutoClassInfo.Instancing = acMultiInstance; Automation->RegisterClass(AutoClassInfo); } //-------------------------------------------------------------------------- #pragma startup RegisterTMyDCom //---------------------------------------------------------------------------
The RegisterTMyDCOM procedure is used to register your object with the system--that is, to list it in the Registry. The details of this process are described in the section called "Registration Issues." For now, you need only take note of the ClassID assigned to your object because you will need this ID when you try to call the object from another machine, as described in the next section.
The act of registering the object is not something you necessarily have to understand because it will occur automatically whenever you run the client application of which TMyDCOM is a part.
NOTE: The object will be registered repeatedly, whenever you run the program, which ensures that you will find it easy to register the object, while simultaneously requiring very little overhead in terms of system resources. If you move the application to a new location, you can register this change with the system by running it once. This capability guarantees that the old items associated with your CLSID will be erased, and new items will be filled in their place. Registering a class ID multiple times does not mean that you will end up with multiple items in the Registry because each registration of a CLSID will overwrite the previous registration. All OLE servers worth their name provide this service. For example, Word and Excel update the Registry each time they are run.
Besides the registration procedure, the other key part of the code generated by the Automation expert is the class definition found at the top of the header:
class TMyDCom : public TAutoObject { private: public: __fastcall TMyDCom(); __automated: };
This code has two sections, one called private and the other called automated. In the __automated section, you can declare methods or properties that you want to call across program or machine boundaries. In other words, any methods or properties that you declare in this space will automatically be marshaled for you by the underlying IDispatch object encapsulated by TAutoObject.
Consider the following code fragments:
class TSimpleDCOM : public TAutoObject { private: public: virtual __fastcall TSimpleDCOM(); __automated: AnsiString __fastcall GetName(); int __fastcall Square(int A); }; AnsiString __fastcall TSimpleDCOM::GetName() { return "SimpleDCOM"; } int __fastcall TSimpleDCOM::Square(int A) { return A * A; }
This object has two methods: one that states the name of the object and one that can square an integer. These two methods are declared in the automated section of the object, so they can be accessed from another program via another program.
The TSimpleDCOM object exports two methods that IDispatch will automatically marshal for you across application or machine boundaries. You can go on adding methods to this object as you like. Any data that you want to add to the object should go in the private section, and any methods or properties that you don't want to export should also go in the private section. All methods that you want to call from inside another application should go in the automated section. You should declare these exported methods as __fastcall.
Some limits to the marshaling will be done for you by IDispatch. In particular, the following types are legal to use in the declarations for the methods or properties in the automated section:
int, float, double, Currency, TDateTime, AnsiString, WordBool Short String unsigned short Variant
The following types are illegal to use in the declarations for the methods or properties in the automated section:
arrays char * void * structs
For additional information, see the "Automating properties and methods" section in the online help for the VCL.
The apparent limitations created by the lack of support from IDispatch for custom types can be considerably mitigated by an intelligent use of variant arrays. These structures can be so helpful that I have added a section later in this chapter called "Using Variant Arrays to Pass Data" to describe their use.
The complete source for a simple DCOM server is
shown in Listing 27.3 through
Listing 27.6. Notice that OleAuto is included in this project. This unit
is essential to OLE Automation programming with the VCL.
Listing 27.3. The
heading for the
SimpleObject file from the EasyDCOM project.
/////////////////////////////////////// // SimpleObject.h // EasyDCOM // Copyright (c) 1997 by Charlie Calvert // #ifndef SimpleObjectH #define SimpleObjectH #include <vcl\oleauto.hpp> #include <vcl\Classes.hpp> class TSimpleDCOM : public TAutoObject { private: public: virtual __fastcall TSimpleDCOM(); __automated: AnsiString __fastcall GetName(); int __fastcall Square(int A); }; #endif
Listing 27.4. The main source file of an OLE Automation object.
/////////////////////////////////////// // SimpleObject.cpp // EasyDCOM // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #undef RegisterClass #include "SimpleObject.h" int Initialization(); static int Initializer = Initialization(); __fastcall TSimpleDCOM::TSimpleDCOM() : TAutoObject() { } AnsiString __fastcall TSimpleDCOM::GetName() { return "SimpleDCOM"; } int __fastcall TSimpleDCOM::Square(int A) { return A * A; } void __fastcall RegisterTSimpleDCOM() { TAutoClassInfo AutoClassInfo; AutoClassInfo.AutoClass = __classid(TSimpleDCOM); AutoClassInfo.ProgID = "EasyDCOM.SimpleDCOM"; AutoClassInfo.ClassID = "{E2674A60-2DF2-11D0-92C5-000000000000}"; AutoClassInfo.Description = "Easiest possible DCOM program"; AutoClassInfo.Instancing = acMultiInstance; Automation->RegisterClass(AutoClassInfo); } int Initialization() { RegisterTSimpleDCOM(); return 0; }
Listing 27.5. The header for the main source file for the EasyDCOM OLE server.
#ifndef MainH #define MainH #include <vcl\Classes.hpp> #include <vcl\Controls.hpp> #include <vcl\StdCtrls.hpp> #include <vcl\Forms.hpp> class TForm1 : public TForm { __published: TLabel *Label1; private: public: virtual __fastcall TForm1(TComponent* Owner); }; extern TForm1 *Form1; #endif
Listing 27.6. The main source file for the EasyDCOM OLE server.
#include <vcl\vcl.h> #pragma hdrstop #include "Main.h" #pragma resource "*.dfm" TForm1 *Form1; __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { }
This
program is meant to be run from a client. As such, it has no controls on
it and no public interface other than the OLE object itself. I do, however, give
the main form a distinctive look, as you can see in Figure 27.2.
FIGURE 27.2.
The main form for the EasyDCOM program.
Of course, there is no reason that a single program could not simultaneously have an OLE server interface and a set of standard controls. For example, Word and Excel are both OLE servers, and standard applications run through a set of menus and other controls. In fact, the same application can work as a server, a standard application, and as a client.
Note that, by using two different approaches, you can ensure that the application is registered each time it is run. One technique involves including an initialization procedure:
int Initialization() { RegisterTSimpleDCOM(); return 0; }
The second technique, shown earlier in the chapter, involves a pragma:
#pragma startup RegisterTSimpleDCom
Both technologies achieve the same effect. As a rule, you don't have to think about this part of the process because the code will be inserted automatically by the Automation Wizard. Needless to say, nothing is magic about the Automation expert, and you can simply create the code yourself by typing it in. In that case, you are free to use either technique, though the pragma is probably easier to write.
That's all I'm going to say for now about creating the server side of a BCB DCOM project. Remember that this code will not work unless you first register the TSimpleDCOM object with the system by running the server once. After you run the server the first time, you never have to run it again, as it will be called automatically by the client program described in the next section. Let me repeat that the whole point of this exercise is that the client program can be located on a separate machine.
The GetDCOM program found on the CD that accompanies this book will call the functions in the server program described in the preceding section. In particular, GetDCOM can automatically launch the server program and then call its GetName and Square functions.
NOTE: When I say that GetDCOM can automatically launch the server, I'm assuming that the server is either on the current system (in which case, it is launched via COM) or on an NT machine (in which case, it is launched via DCOM). DCOM cannot launch an application residing on a remote Windows 95 box.
You can run this application in two different modes.
You can run it as a client
to a local Automation server or as a client to a remote Automation server. If you
look at the main form for the program, shown in Figure 27.3, you can see that it
has three buttons: one for launching the server remotely, one
for launching it locally,
and a third that will be used to call a simple function on the server.
FIGURE 27.3.
The main form for the GetDCOM
application.
The source for the GetDCOM program is shown in Listing 27.7 and Listing 27.8. This program uses a routine called CreateRemoteObject that is declared in the CodeBox unit found in the Utils subdirectory on the CD that accompanies this book. You need to add the CodeBox unit to your project; otherwise, it will not compile. I do not include the entire CodeBox unit in this chapter, but it is available on the CD, and I do include the CreateRemoteObject function in its entirety later in this chapter. Notice also that this project includes the OleAuto unit to call CreateOleObject to retrieve a local instance of IDispatch.
When using this
program, please note that I have hard coded the IP address of
my server into the source. You will need to change this so that it works with your
server. When making the connection between a Windows 95 and Windows NT machine, you
should start by
calling from the Windows 95 machine to the Windows NT machine; that
is, put the client on the Windows 95 machine. You should also start by signing on
to both machines with the same name and password. That way you don't have to worry
about security
issues on the server while you are first getting the technology up
and running. Also, give yourself all possible rights on the server. Make yourself
an administrator.
Listing 27.7. The header
for the
GetDCOM OLE client application.
/////////////////////////////////////// // Main.h // Project: GetDCOM // Copyright (c) 1997 by Charlie Calvert // #ifndef MainH #define MainH #include <vcl\Classes.hpp> #include <vcl\Controls.hpp> #include <vcl\StdCtrls.hpp> #include <vcl\Forms.hpp> #include <vcl\Buttons.hpp> class TForm1 : public TForm { __published: TBitBtn *GetLocalObjectBtn; TBitBtn *GetRemoteObjectBtn; TEdit *Edit1; TBitBtn *SquareBtn; void __fastcall GetLocalObjectBtnClick(TObject *Sender); void __fastcall GetRemoteObjectBtnClick(TObject *Sender); void __fastcall SquareBtnClick(TObject *Sender); void __fastcall FormDestroy(TObject *Sender); private: Variant V; public: virtual __fastcall TForm1(TComponent* Owner); }; extern TForm1 *Form1; #endif
Listing 27.8. The main source file for the GetDCOM application.
/////////////////////////////////////// // Main.cpp // Project: GetDCOM // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #include <vcl\OleAuto.hpp> #include <vcl\ole2.hpp> #include <initguid.h> #pragma hdrstop #include "Main.h" #include "codebox.h" #pragma resource "*.dfm" TForm1 *Form1; __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { CoInitialize(NULL); } void __fastcall TForm1::GetLocalObjectBtnClick(TObject *Sender) { V = CreateOleObject("EasyDCOM.SimpleDCOM"); ShowMessage(V.OleFunction("GetName")); } DEFINE_GUID(ClassID, 0xE2674A60, 0x2DF2, 0x11D0, 0x92,0xC5, 0x00,0x00,0x00,0x00,0x00,0x00); void __fastcall TForm1::GetRemoteObjectBtnClick(TObject *Sender) { Screen->Cursor = crHourGlass; if (CreateRemoteObject(ClassID, "143.186.149.228", V)) { ShowMessage(V.OleFunction("GetName")); } else { ShowMessage("Failed"); } Screen->Cursor = crDefault; } void __fastcall TForm1::SquareBtnClick(TObject *Sender) { try { ShowMessage(V.OleFunction("Square", Edit1->Text)); } catch(Exception &E) { ShowMessage(E.Message); } } void __fastcall TForm1::FormDestroy(TObject *Sender) { CoUninitialize(); }
The code declares the CLSID created by the BCB Automation expert in the preceding section of this chapter:
DEFINE_GUID(ClassID, 0xE2674A60, 0x2DF2, 0x11D0, 0x92,0xC5, 0x00,0x00,0x00,0x00,0x00,0x00);
This is the CLSID associated with the server half of this DCOM project, and you need to include it here in the client program. Additional information about CLSIDs and the registration process will be presented later in this chapter.
NOTE: You need to include the standard Windows API initguids.h file in projects that use GUIDs.
The actual call to automate the object is nearly identical to the call you would make if you wanted to automate an object on your local machine. The only difference is that you call CreateRemoteOleObject rather than CreateOleObject. Here is the way to call the object locally:
void __fastcall TForm1::GetLocalObjectBtnClick(TObject *Sender) { V = CreateOleObject("EasyDCOM.SimpleDCOM"); ShowMessage(V.OleFunction("GetName")); }
This code assumes that the variable V, of type Variant is a field of TForm1. The code calls a built-in function of the VCL called CreateOleObject. This function takes the ProgID of an object, looks up its CLSID in the Registry, finds the place on the hard drive where the program that owns the object is located, launches the program, and retrieves the object in a Variant.
The code then uses the OleFunction method of the Variant object to call one of the methods of the OLE server. OleFunction takes one or more parameters, specifying the name of the function you want to call and any parameters you want to pass to it. BCB does not have support for named parameters.
Later in the program, you can call the Square method of the Automation server:
void __fastcall TForm1::SquareBtnClick(TObject *Sender) { try { ShowMessage(V.OleFunction("Square", Edit1->Text)); } catch(Exception &E) { ShowMessage(E.Message); } }
Again, I use the OleFunction method to make the call to the Square method via OleAutomation. As you can see, I wrap the function call in a try..except block because chances are good that the user might click the Square button before initializing the object with a call to CreateOleObject or CreateRemoteOleObject. Notice that this time OleFunction takes two parameters, one stating the name of the OLE server method to be called and the second specifying a parameter to be passed to that method.
Here is the method of GetDCOM that I use to summon the remote server:
void __fastcall TForm1::GetRemoteObjectBtnClick(TObject *Sender) { Screen->Cursor = crHourGlass; if (CreateRemoteObject(ClassID, "143.186.149.228", V)) { ShowMessage(V.OleFunction("GetName")); } else { ShowMessage("Failed"); } Screen->Cursor = crDefault; }
This function is no different in substance from the GetDCOM routine that retrieves the local object. The only difference is that I call CreateRemoteObject rather than CreateOleObject. Remember that you can call CreateOleObject to retrieve remote objects if you are on an NT machine or if you have set the Windows 95 server machine into User Access mode via the Network applet in the Control Panel.
NOTE: Let me just reiterate that you need to pass in the IP address, or server name, of the machine on which your server is located. Here I type in the IP address of my NT server: 143.186.149.228. You replace this number with the name or number of your server. If you're confused by the topic of IP addresses, you might be able to glean some information from the discussion of TCP/IP in Chapter 8, "Database Basics and Database Tools."
CreateRemoteObject is a custom function I have written; it looks like this:
BOOL CreateRemoteObject(GUID ClassID, char *Server, Variant &V) { Ole2::IClassFactory *ClassFactory; Ole2::IUnknown *Unknown1; COSERVERINFO Info; OLECHAR Dest[MAX_PATH]; int i = MultiByteToWideChar(CP_ACP, 0, Server, -1, Dest, MAX_PATH); if (i <= 0) return FALSE; ClassFactory = NULL; Info.dwReserved1 = 0; Info.pAuthInfo = NULL; Info.dwReserved2 = 0; Info.pwszName = Dest; HRESULT hr = CoGetClassObject(ClassID, CLSCTX_REMOTE_SERVER, &Info, Ole2::IID_IClassFactory, (void **)&ClassFactory); OleCheck(hr); if (ClassFactory != NULL) { hr = ClassFactory->CreateInstance(NULL, Ole2::IID_IUnknown, (void **)&Unknown1); OleCheck(hr); V = VarFromInterface(Unknown1); ClassFactory->Release(); if (VarType(V) != varNull) return True; else return False; } return FALSE; }
This routine is declared in the CodeBox unit found in the Utils subdirectory on the CD that accompanies this book. As I stated earlier, you need to add the CodeBox unit to your project; otherwise, it will not compile. Alternatively, you can simply copy this routine into your project. However, keeping it in a separate unit makes sense because you might want to call it from multiple applications.
Whether you understand this routine is not really important. You can just plug it into your applications the same way you do CreateOleObject. However, I will talk about it briefly for those who are interested.
The CreateRemoteObject routine takes three parameters. The first contains the ID of the object you want to obtain, and the second contains the name of the server where the object resides. The last parameter contains a variant that will hold the instance of IDispatch retrieved from the system. (Sometimes you might have to use the IP address itself rather than the name of the server.) CreateRemoteObject returns a variant that "contains" a copy of the object that you want to call. You can use this variant to call all the methods in the automated section of your object.
Variants are special BCB types that can contain a wide variety of data types, including OLE objects. I discussed variants at some length in Chapter 3, "C++Builder and the VCL."
When you call the methods of an OLE object off a variant, no runtime checking for the calls occurs. BCB just assumes you know what you're doing, and if the call fails, you won't know until runtime. This problem is addressed in Delphi 3, and so I assume it will be addressed in future releases of C++Builder.
The key call in CreateRemoteOleObject is to CoGetClassObject:
HRESULT hr = CoGetClassObject(ClassID, CLSCTX_REMOTE_SERVER, &Info, Ole2::IID_IClassFactory, (void **)&ClassFactory); OleCheck(hr);
This routine has long been a part of COM, but it has been altered slightly to support DCOM. Here is how the routine is currently declared in ObjBase.h:
WINOLEAPI CoGetClassObject( REFCLSID rclsid, // The ID of the object you want DWORD dwClsContext, // In process, local or remote server? LPVOID pvReserved, // Previously reserved, now used for CoServerInfo REFIID riid, // Usually IID_IClassFactory LPVOID FAR* ppv); // Where the class factory is returned
The function returns an HRESULT variable containing information on the outcome of the call. If HRESULT is set to zero, then the call succeeded. Most other values represent an error in the form of a number. You can retrieve a human-readable string by passing that number to a VCL function called OleCheck.
The third parameter to CoGetClassObject, previously reserved, is now the place where you pass in the name of the server you want to access. The server is usually designated with either a string or a literal IP address, such as 143.186.149.111. You would pass in the IP address in the form of a string. That is, don't try to pass a number; just put the IP address in quotation marks and pass it in as a string. Here is the new declaration for CoGetClassObject, as found in the MSDN:
STDAPI CoGetClassObject( REFCLSID rclsid, //CLSID associated with the class object DWORD dwClsContext, //Context for running executable code COSERVERINFO * pServerInfo, // Machine on which object is to be instantiated REFIID riid, //Reference to the identifier of the interface LPVOID * ppv //Indirect pointer to the interface );
In particular, here is the record you pass in for the third parameter:
typedef struct _COSERVERINFO { DWORD dwSize; // must be set to sizeof(COSERVERINFO) OLECHAR* pszName; // machine name } COSERVERINFO;
The first field of this record is just a version check field that should contain the size of the TCoServerInfo record. The second parameter contains a Unicode string that has the name of the server or its IP address embedded in it. Use the MultiByteToWideChar Windows API function to convert a standard BCB string into a Unicode string:
OLECHAR Dest[MAX_PATH]; int i = MultiByteToWideChar(CP_ACP, 0, Server, -1, Dest, MAX_PATH); if (i <= 0) return FALSE;
The call to CoGetClassObject retrieves a ClassFactory. After you have the ClassFactory back from the server, you can use it to retrieve an instance of the object you want to call. What you retrieve back, of course, is an instance of IDispatch. You can convert this instance into a variant by calling the BCB routine VarFromInterface, which is found in the OleAuto unit that ships with BCB.
If you want, you can simplify this call by using CoCreateInstanceEx. CoCreateInstanceEx is superior to CoGetClassObject because it retrieves the object you want with only one call instead of having to first get the ClassFactory and then call CreateInstance on the ClassFactory. In short, CoCreateInstanceEx executes faster than CoGetClassObject. (Remember, all calls between objects on separate machines are going to have a considerable overhead associated with them!) Another advantage of CoCreateInstanceEx is that it takes a MultiQI structure that can contain a list of multiple objects to retrieve. That way, you can retrieve multiple objects through a single call. Again, using this method will save considerable time.
Before I close this section, let me review the key points covered so far:
Before going further, I want to mention a few issues about CLSIDs and the Registry. If you already understand the Registry, you can skip this section. I covered some aspects of the Registry in Chapter 13, "Flat-File, Real-World Databases." However, I will go over this material again here from the perspective of an OLE application.
The Registry is a place where information can be stored. It's a database.
CLSIDs are statistically unique numbers that can be used by the operating system to reference an OLE object. CLSIDs are stored in the Registry.
In this case, visiting the actual perpetrator in its native habitat is probably best. In the example explained here, I'm assuming that you have a copy of Word loaded on your system.
To get started, use the Run menu on the Windows taskbar to launch the RegEdit program that ships with Windows NT. Just type RegEdit and click OK. Search through the HKEY_CLASSES_ROOT for the Word.Basic entry, as shown in Figure 27.4. When you find it, you can see that it's associated with the following CLSID:
{000209FE-0000-0000-C000-000000000046}
This unique class ID is inserted into the Registry of all machines that contain a valid, and properly installed, copy of Word for Windows. The only application that uses this ID is Word for Windows. It belongs uniquely to that application.
Now go further up HKEY_CLASSES_ROOT and look for the CLSID branch. Open it and search for the CLSID shown above. When you find it, you can see two entries associated with it: one is called LocalServer, or LocalServer32, and the other is called ProgID. The ProgID is set to word.basic. The LocalServer entry looks something like this:
C:\WINWORD\WINWORD.EXE /Automation
FIGURE 27.4. If you run the Windows program Regedit.exe, then
you can see the registration database entry for Word.Basic under
HKEY_CLASSES_ROOT.
If you look at this command, you can begin to grasp how Windows can translate the CLSID passed to CoGetClassObject into the name of an executable. In particular, Windows looks up the CLSID in the Registry and then uses the LocalServer32 entry to find the directory and name of the executable or DLL you want to launch.
Having these kinds of entries in the registration database does not mean that the applications in question are necessarily Automation servers. For example, many applications with LocalServer and ProgID entries are not Automation servers. However, all Automation servers do have these two entries. Note, further, that this is a reference to the Automation server in Word, not a reference to Word as a generic application. It references an Automation object inside Word, not Word itself. (The Automation object is an instance of IDispatch. It was not created with TAutoObject, but it has all the same attributes.)
The same basic scenario outlined here takes place when you call CoGetClassObject and specify the CLSID of an object on another machine. In particular, Windows contacts the specified machine, asks it to look up the CLSID in the Registry, and then marshals information back and forth between the two machines.
CLSIDs are said to be statistically unique. You can create a new CLSID by calling CoCreateGuid. The following code shows one way to make this call:
CoInitialize(NULL); CoCreateGuid(GUID); // eventually you should call CoUninitialize;
The code shown here begins by calling CoInitialize, which is usually unnecessary in BCB because the OLE2 unit will call this function automatically when your program is launched; that is, it will do so if you include OLE2 in the uses clause of one of your units.
CoCreateGuid is the call that retrieves the new CLSID from the system. This ID is guaranteed to be unique as long as you have a network card on your system. Each network card has a unique number on it, and this card number is combined with the date and time and other random bits of information to create a unique number that could only be generated on a machine with your network card at a particular date and time. Rumors that the phase of the moon and current age of Bill Gates's children are also factored in are probably not true. At any rate, the result is a number that is guaranteed to be statistically unique, within the tolerance levels for your definition of that word given your faith in mathematicians in general and Microsoft-based mathematicians in particular.
The StringFromCLSID routine converts a CLSID into a string. The ParseGuid routine is a custom function I wrote to convert a string of type
{FC41CC90-C01D-11CF-8CCD-0080C80CF1D2}
into a record of type GUID that can be used in a BCB application as defined in Wtypes.h:
typedef struct _GUID { DWORD Data1; WORD Data2; WORD Data3; BYTE Data4[ 8 ]; } GUID;
That's all I want to say about the Registry for now. This subject can appear a bit tricky at first, but ultimately it is not complicated.
BCB enables you to create variant arrays, which are the VCL version of the safe arrays used in OLE Automation. You can use variant arrays to pass large chunks of data back and forth between COM objects. For example, you can pass a bitmap, AVI file, or text file between two applications using variant arrays. In short, this type can help you avoid the shortcomings created by the limited types supported by IDispatch.
Variant arrays (and safe arrays) are costly in terms of memory and CPU cycles, so you normally would not use them except in automation or DCOM code, or in special cases in which they provide obvious benefits over standard arrays. For example, the database code makes some use of variant arrays.
The Variant class type, found in SysDefs.h and covered in Chapter 3 has constructors for creating variant arrays:
// constructor for array of variants of type varType __fastcall Variant(const int* bounds, const int boundsSize, Word varType); // constructor for one-dimensional array of type Variant __fastcall Variant(const Variant* values, const int valuesSize);
If you know the type of the elements to be used in an array, you can set the VarType parameter to that type. For example, if you know you're going to be working with integers, you can write the following:
Variant MyVariant(OPENARRAY(int, (0, 5)), varInteger);
You cannot use varString in the last parameter; instead, use varOleStr. Remember that an array of Variant takes up 16 bytes for each member of the array, and other types might take up less space.
Arrays of Variant can be resized with the VarArrayRedim function:
extern void __fastcall VarArrayRedim(Variant &A, int HighBound);
The variable to be resized is passed in the first parameter, and the number of elements to be contained in the resized array is held in the second parameter.
You declare a two-dimensional array like this:
Variant MyVariant(OPENARRAY(int, (0, 5, 0, 5)), varInteger);
This array has two dimensions, each with six elements. To access a member of this array, you write code that looks like the following:
for (i = 0; i < 6; i++) for (j = 0; j < 6; j++) MyVariant.PutElement(i * j, i, j); for (i = 0; i < 6; i++) { for (j = 0; j < 6; j++) { S = S + " " + MyVariant.GetElement(i, j); } S = S + `\r'; }
The following code fragment shows how to use a one-dimensional array and how to query an array to find out about its composition:
AnsiString TForm1::GetInfo(Variant &V) { int Count, HighBound, LowBound, i; AnsiString S; Count = VarArrayDimCount(V); S = AnsiString("\nDimension Count: ") + IntToStr(Count) + `\n'; for (i = 1; i <= Count; i++) { HighBound = VarArrayHighBound(V, i); LowBound = VarArrayLowBound(V, i); S = S + "LowBound: " + IntToStr(LowBound) + `\n'; S = S + "HighBound: " + IntToStr(HighBound) + `\n'; } return S + `\n';
}
void __fastcall TForm1::bOneDimClick(TObject *Sender) { AnsiString S; int i; S = ""; Variant MyVariant(OPENARRAY(int, (0, 5)), varInteger); for (i = 0; i <= 5; i++) MyVariant.PutElement(i * 2, i); for (i = 0; i <= 5; i++) S = S + " " + MyVariant.GetElement(i); S = GetInfo(MyVariant) + S; ShowMessage(S); }
The GetInfo method demonstrates how to work with a variant array passed as a parameter. Notice that you don't have to do anything special to access a variant as an array. The type travels with the variable.
If you try to pass a variant with a VType of varInteger to this function, BCB raises an exception when you try to treat the variant as an array. In short, the variant must have a VType of VarArray; otherwise, the call to GetInfo will fail. You can use the VarType function to check the current setting for the VType of a variant, or you can call VarIsArray, which returns a Boolean value.
You can use the VarArrayHighBound, VarArrayLowBound, and VarArrayDimCount functions to find out about the number of dimensions in your array and about the bounds of each dimension. The following GetInfo function creates a string showing the number of dimensions in a variant array, as well as the high and low values for each dimension:
AnsiString TForm1::GetInfo(Variant &V) { int Count, HighBound, LowBound, i; AnsiString S; Count = VarArrayDimCount(V); S = AnsiString("\nDimension Count: ") + IntToStr(Count) + `\n'; for (i = 1; i <= Count; i++) { HighBound = VarArrayHighBound(V, i); LowBound = VarArrayLowBound(V, i); S = S + "LowBound: " + IntToStr(LowBound) + `\n'; S = S + "HighBound: " + IntToStr(HighBound) + `\n'; } return S + `\n'; }
This routine starts by getting the number of dimensions in the array. It then iterates through each dimension, retrieving its high and low values. If you create an array with the call
Variant MyVariant(OPENARRAY(int, (0, 5, 1, 3)), varInteger);
the GetInfo function produces the following output if passed MyVariant:
Dimension Count: 2 HighBound: 5 LowBound: 0 HighBound: 3 LowBound: 1
GetInfo raises an exception if you pass in a variant that causes VarIsArray to return False.
A certain amount of overhead is involved in working with variant arrays. If you want to process the arrays quickly, you can use two functions called VarArrayLock and VarArrayUnlock. The first of these routines returns a pointer to the data stored in an array. In particular, VarArrayLock takes a variant array and returns a standard Pascal array. For it to work, the array must be explicitly declared with one of the standard types listed earlier in the chapter. The type used in the variant array and the type used in the Pascal array must be identical.
Here is an example of using VarArrayLock and VarArrayUnlock:
Variant GetArrayData() { int i, j; Variant V(OPENARRAY(int, (1, Max, 1, Max)), varInteger); for (i = 1; i < Max; i++) for (j = 1; j < Max; j++) V.PutElement(i * j, j, i); return V; } void __fastcall TForm1::LockedArray1Click(TObject *Sender) { int Data[Max][Max]; int i, j; Variant V; V = GetArrayData(); void *P = VarArrayLock(V); memcpy(Data, P, sizeof(Data)); for (i = VarArrayLowBound(V, 1); i < VarArrayHighBound(V, 1); i++) for (j = VarArrayLowBound(V, 2); j < VarArrayHighBound(V, 2); j++) Grid->Cells[i-1][j-1] = Data[i-1][j-1]; VarArrayUnlock(V); }
Notice that this code first locks down the array and then accesses it as a pointer to a standard array. Finally, it releases the array when the operation is finished. You must remember to call VarArrayUnlock when you're finished working with the data from the array:
for (i = VarArrayLowBound(V, 1); i < VarArrayHighBound(V, 1); i++) for (j = VarArrayLowBound(V, 2); j < VarArrayHighBound(V, 2); j++) Grid->Cells[i-1][j-1] = Data[i-1][j-1]; VarArrayUnlock(V);
Remember that the point of using VarArrayLock and VarArrayUnlock is that they speed access to the array. The actual code you write is more complex and verbose, but the performance is faster.
If you don't want to lock down an array, you can still access the data. You have to do so by brute-force means, however, and can't use vast pointer-manipulation routines such as memcpy. The following NormalArray1Click method shows how to proceed if you don't lock down the data:
void __fastcall TForm1::NormalArray1Click(TObject *Sender) { int i, j; Variant V = GetArrayData(); for (i = 1; i < VarArrayHighBound(V, 1); i++) for (j = 1; j < VarArrayHighBound(V, 2); j++) Grid->Cells[i-1][j-1] = V.GetElement(i, j); }
One of the most useful reasons for using a variant array is to transfer binary data to and from a server. If you have a binary file, say a WAV file or an AVI file, you can pass it back and forth between your program and an OLE server using variant arrays. Such a situation would present an ideal time for using VarArrayLock and VarArrayUnlock. You would, of course, use VarByte as the second parameter to VarArrayCreate when you're creating the array. That is, you would be working with an array of Byte and accessing it directly by locking down the array before moving data into and out of the structure. Such arrays are not subject to translation while being marshaled across boundaries.
The next program in this chapter shows how to pass data back and forth between
programs using
this technique. Listing 27.9 and Listing 27.10 contain a single sample
program that encapsulates most of the ideas that you have seen in this section on
variant arrays. The program from which this code is excerpted is called VarArray,
and you can find
it in the Chap27 directory on the disk. Some screen shots
from the program are shown in Figure 27.5 and Fig- ure 27.6.
FIGURE 27.5.
Using the
VarArray program to view information about a two-dimensional
array.
FIGURE 27.6. Viewing a two-dimensional array that is locked down to get fast access to its data.
Listing 27.9. The header for the VarArray program. VarArray is designed to show how to use variant arrays.
/////////////////////////////////////// // Main.cpp // Project: VarArray // Copyright (c) 1997 by Charlie Calvert // #ifndef MainH #define MainH #include <vcl\Classes.hpp> #include <vcl\Controls.hpp> #include <vcl\StdCtrls.hpp> #include <vcl\Forms.hpp> #include "Grids.hpp" #include <vcl\Menus.hpp> class TForm1 : public TForm { __published: TStringGrid *Grid; TMainMenu *MainMenu1; TMenuItem *Options1; TMenuItem *CreateOneDimensionalArray1; TMenuItem *CreateTwoDimensionalArray1; TMenuItem *NormalArray1; TMenuItem *LockedArray1; void __fastcall bOneDimClick(TObject *Sender); void __fastcall bTwoDimClick(TObject *Sender); void __fastcall NormalArray1Click(TObject *Sender); void __fastcall LockedArray1Click(TObject *Sender); private: AnsiString GetInfo(Variant &V); public: __fastcall TForm1(TComponent* Owner); }; extern TForm1 *Form1; #endif
Listing 27.10. The main source file for the VarArray program.
/////////////////////////////////////// // Main.cpp // Project: VarArray // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "Main.h" #pragma link "Grids" #pragma resource "*.dfm" #define Max 13 TForm1 *Form1; __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } AnsiString TForm1::GetInfo(Variant &V) { int Count, HighBound, LowBound, i; AnsiString S; Count = VarArrayDimCount(V); S = AnsiString("\nDimension Count: ") + IntToStr(Count) + `\n'; for (i = 1; i <= Count; i++) { HighBound = VarArrayHighBound(V, i); LowBound = VarArrayLowBound(V, i); S = S + "LowBound: " + IntToStr(LowBound) + `\n'; S = S + "HighBound: " + IntToStr(HighBound) + `\n'; } return S + `\n'; } void __fastcall TForm1::bOneDimClick(TObject *Sender) { AnsiString S; int i; S = ""; Variant MyVariant(OPENARRAY(int, (0, 5)), varInteger); for (i = 0; i <= 5; i++) MyVariant.PutElement(i * 2, i); for (i = 0; i <= 5; i++) S = S + " " + MyVariant.GetElement(i); S = GetInfo(MyVariant) + S; ShowMessage(S); } void __fastcall TForm1::bTwoDimClick(TObject *Sender) { int i, j; AnsiString S; Variant MyVariant(OPENARRAY(int, (0, 5, 0, 5)), varInteger); for (i = 0; i < 6; i++) for (j = 0; j < 6; j++) MyVariant.PutElement(i * j, i, j); for (i = 0; i < 6; i++) { for (j = 0; j < 6; j++) { S = S + " " + MyVariant.GetElement(i, j); } S = S + `\r'; } S = GetInfo(MyVariant) + S; ShowMessage(S); } Variant GetArrayData() { int i, j; Variant V(OPENARRAY(int, (1, Max, 1, Max)), varInteger); for (i = 1; i < Max; i++) for (j = 1; j < Max; j++) V.PutElement(i * j, j, i); return V; } void __fastcall TForm1::NormalArray1Click(TObject *Sender) { int i, j; Variant V = GetArrayData(); for (i = 1; i < VarArrayHighBound(V, 1); i++) for (j = 1; j < VarArrayHighBound(V, 2); j++) Grid->Cells[i-1][j-1] = V.GetElement(i, j); } void __fastcall TForm1::LockedArray1Click(TObject *Sender) { int Data[Max][Max]; int i, j; Variant V; V = GetArrayData(); void *P = VarArrayLock(V); memcpy(Data, P, sizeof(Data)); for (i = VarArrayLowBound(V, 1); i < VarArrayHighBound(V, 1); i++) for (j = VarArrayLowBound(V, 2); j < VarArrayHighBound(V, 2); j++) Grid->Cells[i-1][j-1] = Data[i-1][j-1]; VarArrayUnlock(V); }
This program has two menu items:
Remember that variant arrays are of use only in special circumstances. They are powerful tools, especially when you're making calls to OLE automation objects. However, they are slower and bulkier than standard BCB arrays and should be used only when necessary.
The
DataCom directory on the CD that accompanies this book contains two
programs. One is an OLE Automation server, and the other is an OLE Automation client.
I will talk about the server first. The code for the server is shown in Listing 27.11
through Listing 27.17. The interface for the server isn't very important from the
perspective of this book, but you can see it in Figure 27.7. Note that the Globals.h
and Globals.cpp files used by both the client and server
applications are
stored in the client application's directory.
FIGURE 27.7.
The DataServer OLE Automation server allows you to view the data and test
the
routines that will be exported to other applications.
Listing 27.11. The header for the main module in the DataServer OLE Automation program.
/////////////////////////////////////// // Main.h // Project: DataServer // Copyright (c) 1997 by Charlie Calvert // #ifndef MainH #define MainH #include <vcl\Classes.hpp> #include <vcl\Controls.hpp> #include <vcl\StdCtrls.hpp> #include <vcl\Forms.hpp> #include <vcl\DBGrids.hpp> #include "Grids.hpp" #include <vcl\DBCtrls.hpp> #include <vcl\ExtCtrls.hpp> class TForm1 : public TForm { __published: TDBGrid *DBGrid1; TButton *bFillStrGrid; TStringGrid *Grid; TButton *bUpdate; TDBNavigator *DBNavigator1; void __fastcall bFillStrGridClick(TObject *Sender); void __fastcall bUpdateClick(TObject *Sender); private: public: __fastcall TForm1(TComponent* Owner); Variant __fastcall GetData(); WordBool __fastcall DoUpdate(Variant V); void __fastcall UpdateParams(AnsiString CustNo, AnsiString Company, AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip); }; extern TForm1 *Form1; #endif #endif
Listing 27.12. The main source file for the DataServer application.
/////////////////////////////////////// // Main.cpp // Project: DataServer // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "Main.h" #include "DMod1.h" #pragma link "Grids" #pragma resource "*.dfm" TForm1 *Form1; __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } Variant __fastcall TForm1::GetData() { TCustomerRecord *Customer = new TCustomerRecord(); void *P; DMod->GetCustAry(*Customer); Variant V(OPENARRAY(int, (0, sizeof(TCustomerRecord))), varByte); P = VarArrayLock(V); memcpy(P, Customer, sizeof(TCustomerRecord)); VarArrayUnlock(V); return V; } // This function merely tests GetData to make sure it is working void __fastcall TForm1::bFillStrGridClick(TObject *Sender) { TCustomerRecord *Customer = new TCustomerRecord(); void *P; int i; Variant V = GetData(); P = VarArrayLock(V); memcpy(Customer, P, sizeof(TCustomerRecord)); VarArrayUnlock(V); Grid->RowCount = Customer->Count; for (i = 0; i < Customer->Count; i++) { Grid->Cells[0][i] = Customer->CustAry[i].CustNo; Grid->Cells[1][i] = Customer->CustAry[i].Company; Grid->Cells[2][i] = Customer->CustAry[i].Address; Grid->Cells[3][i] = Customer->CustAry[i].City; Grid->Cells[4][i] = Customer->CustAry[i].State; Grid->Cells[5][i] = Customer->CustAry[i].Zip; } } WordBool __fastcall TForm1::DoUpdate(Variant V) { void *P; TCustomer C; try { P = VarArrayLock(V); memcpy(&C, P, sizeof(TCustomer)); VarArrayUnlock(V); ShowMessage("Ok"); DMod->Update(C); } catch(...) { return False; } return True; } void __fastcall TForm1::UpdateParams(AnsiString CustNo, AnsiString Company, AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip) { TCustomer Customer; strcpy(Customer.CustNo, CustNo.c_str()); strcpy(Customer.Company, Company.c_str()); strcpy(Customer.Address, Address.c_str()); strcpy(Customer.City, City.c_str()); strcpy(Customer.State, State.c_str()); strcpy(Customer.Zip, Zip.c_str()); DMod->Update(Customer); } void __fastcall TForm1::bUpdateClick(TObject *Sender) { TCustomer Customer; AnsiString CustNo; CustNo = ""; InputQuery("CustNo of Record to Edit", "Enter CustNo: ", CustNo); strcpy(Customer.Company, "Company"); strcpy(Customer.Address, "Address"); strcpy(Customer.City, "City"); strcpy(Customer.State, "State"); strcpy(Customer.Zip, "Zip"); strcpy(Customer.CustNo, CustNo.c_str()); void *P; Variant V(OPENARRAY(int, (0, sizeof(TCustomer))), varByte); P = VarArrayLock(V); memcpy(P, &Customer, sizeof(TCustomer)); VarArrayUnlock(V); DoUpdate(V); }
Listing 27.13. The header for the data module for the DataServer application.
/////////////////////////////////////// // DMod1.h // Project: DataServer // Copyright (c) 1997 by Charlie Calvert // #ifndef DMod1H #define DMod1H #include <vcl\Classes.hpp> #include <vcl\Controls.hpp> #include <vcl\StdCtrls.hpp> #include <vcl\Forms.hpp> #include <vcl\DBTables.hpp> #include <vcl\DB.hpp> #include "Globals.h" class TDMod : public TDataModule { __published: TTable *CustomerTable; TFloatField *CustomerTableCustNo; TStringField *CustomerTableCompany; TStringField *CustomerTableAddr1; TStringField *CustomerTableAddr2; TStringField *CustomerTableCity; TStringField *CustomerTableState; TStringField *CustomerTableZip; TStringField *CustomerTableCountry; TStringField *CustomerTablePhone; TStringField *CustomerTableFAX; TFloatField *CustomerTableTaxRate; TStringField *CustomerTableContact; TDateTimeField *CustomerTableLastInvoiceDate; TDataSource *CustomerSource; TQuery *UpdateQuery; private: public: __fastcall TDMod(TComponent* Owner); void GetCustAry(TCustomerRecord &Customer); void Update(TCustomer Customer); }; extern TDMod *DMod; #endif
Listing 27.14. The data module for the DataServer application.
/////////////////////////////////////// // DMod1.cpp // Project: DataServer // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #include <vcl\bde.hpp> #pragma hdrstop #include "DMod1.h" #pragma resource "*.dfm" TDMod *DMod; __fastcall TDMod::TDMod(TComponent* Owner) : TDataModule(Owner) { CustomerTable->Open(); } void TDMod::GetCustAry(TCustomerRecord &Customer) { int i = 0; Variant V; Double Num; CustomerTable->First(); CustomerSource->Enabled = False; while (!CustomerTable->Eof) { Num = CustomerTable->FieldByName("CustNo")->AsFloat; sprintf(Customer.CustAry[i].CustNo, "%f", Num); strcpy(Customer.CustAry[i].Company, CustomerTableCompany->AsString.c_str()); strcpy(Customer.CustAry[i].Address, CustomerTableAddr1->AsString.c_str()); strcpy(Customer.CustAry[i].City, CustomerTableCity->AsString.c_str()); strcpy(Customer.CustAry[i].State, CustomerTableState->AsString.c_str()); strcpy(Customer.CustAry[i].Zip, CustomerTableZip->AsString.c_str()); i++; CustomerTable->Next(); } Customer.Count = i - 1; CustomerSource->Enabled = True; } void TDMod::Update(TCustomer Customer) { float Value; UpdateQuery->Close(); UpdateQuery->Params->Items[0]->AsString = Customer.Company; UpdateQuery->Params->Items[1]->AsString = Customer.Address; UpdateQuery->Params->Items[2]->AsString = Customer.City; UpdateQuery->Params->Items[3]->AsString = Customer.State; UpdateQuery->Params->Items[4]->AsString = Customer.Zip; Value = StrToFloat(Customer.CustNo); UpdateQuery->Params->Items[5]->AsFloat = Value; UpdateQuery->ExecSQL(); }
Listing 27.15. The header for the OLE Automation object in the DataServer application.
/////////////////////////////////////// // DataObject.h // Project: DataServer // Copyright (c) 1997 by Charlie Calvert // #ifndef DataObjectH #define DataObjectH #include <vcl\OleAuto.hpp> #include <vcl\Classes.hpp> class TDataServer : public TAutoObject { private: public: __fastcall TDataServer(); __automated: AnsiString __fastcall GetName(); Variant __fastcall TDataServer::GetData(); WordBool __fastcall UpdateRecord(Variant V); void __fastcall UpdateParams(AnsiString CustNo, AnsiString Company, AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip); }; #endif
Listing 27.16. The main source file for the OLE Automation object in the DataServer application.
/////////////////////////////////////// // DataObject.cpp // Project: DataServer // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "DataObject.h" #include "DMod1.h" #include "Main.h" __fastcall TDataServer::TDataServer() : TAutoObject() { } AnsiString __fastcall TDataServer::GetName() { return AnsiString("TDataServer: ") + Now(); } Variant __fastcall TDataServer::GetData() { return Form1->GetData(); } WordBool __fastcall TDataServer::UpdateRecord(Variant V) { return Form1->DoUpdate(V); } void __fastcall TDataServer::UpdateParams(AnsiString CustNo, AnsiString Company, AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip) { Form1->UpdateParams(CustNo, Company, Address, City, State, Zip); } void __fastcall RegisterTDataServer() { TAutoClassInfo AutoClassInfo; AutoClassInfo.AutoClass = __classid(TDataServer); AutoClassInfo.ProgID = "DataServer.DataServer"; AutoClassInfo.ClassID = "{34BADDC0-884F-11D0-BCD7-0080C80CF1D2}"; AutoClassInfo.Description = "DCOM DataServer "; AutoClassInfo.Instancing = acMultiInstance; Automation->RegisterClass(AutoClassInfo); } #pragma startup RegisterTDataServer
Listing 27.17. The Globals unit contains some declarations used by both the DataServer and the GetData client applications. It is stored in the GetData directory.
/////////////////////////////////////// // Globals.h // Project: GetData // Copyright (c) 1997 by Charlie Calvert // #ifndef GlobalsH #define GlobalsH struct TCustomer { char CustNo[256]; char Company[256]; char Address[256]; char City[256]; char State[256]; char Zip[256]; }; typedef TCustomer TCustAry[100]; struct TCustomerRecord { int Count; TCustAry CustAry; }; #endif
This program exports the entire Customer table to remote clients. It also allows the clients to edit a particular row of data. This way, you can both view and edit the data from a database without ever loading any database tools on your machine.
NOTE: Understanding that remote datasets give you many of the advantages of the Web without the slow performance of the Internet and the limited interface capabilities of HTML is very important. For example, you can access remote datasets via DCOM without having to load any database tools. All that you need on your system is the subset of OLE DLLs that concern COM. All these DLLs, and more, are loaded whenever you launch the Internet Explorer. Both Web browsers (via ISAPI and CGI) and DCOM give you access to remote datasets. DCOM, however, is a more efficient, albeit platform-specific, solution.
DCOM is also limited in terms of its range because it works best on an intranet but might not currently be a viable solution if you're trying to access data halfway around the world. Once again, the key is to know the available technologies and to use the one that makes sense in a particular context.
ServerData is fairly long, but the important sections of code are really fairly brief, and not particularly difficult to understand. I include the whole program so you can follow the logic of the entire application at your leisure, but I will focus mostly on a few key elements.
Here is the declaration for the Automation class:
class TDataServer : public TAutoObject { private: public: __fastcall TDataServer(); __automated: AnsiString __fastcall GetName(); Variant __fastcall GetData(); WordBool __fastcall UpdateRecord(Variant V); void __fastcall UpdateParams(AnsiString CustNo, AnsiString Company, AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip); }; #endif
The GetName function is provided primarily so that you can test your connection to the server. If you can call GetName, then you know that you have access to the server.
The GetData function retrieves a variant array that contains an entire dataset. At any rate, I grab the key fields from a dataset and iterate through all the records of the dataset to get the information I need.
The UpdateRecord and UpdateParams functions are used by the client when it wants to update data on the server. For example, the user might edit one particular record and then send the edits back to the server via these functions.
NOTE: At the time of this writing, the first versions of BCB out of the dock apparently will not handle UpdateRecord properly, although they will handle UpdateParams. The problem with the UpdateRecord function has to do with what appears to be a bug in how BCB handles variants that are passed as parameters. In short, you simply cannot pass a variant to a procedure or function by value; you must pass it by reference. OLE Automation cannot handle parameters that are passed by reference; you must pass them by value. As a result, you cannot pass variants as parameters between BCB TAutoObject-based automation clients and servers. You can, however, return a variant from any BCB method, including BCB Automation methods.
If you contemplate this bug for a second, you can see that it has absolutely nothing to do with OLE Automation. That part of the equation is handled fine by BCB and the VCL. The problem shown here is completely a BCB bug involving its implementation of variants, and it has nothing to do with the VCL, and nothing to do with OLE Automation.
I'm sure this bug will be fixed very quickly, and you should check to see whether your version of BCB handles it correctly, or if you can download patches to fix the problem. I, of course, have not been able to properly test UpdateRecord because of this bug, but I believe that it will work when the BCB problem is cleaned up.
The GetData method looks like this:
Variant __fastcall TDataServer::GetData() { return Form1->GetData();
}
As you can see, I delegate the actual implementation of GetData to the main form. This practice is common in OLE Automation because the Automation object is supposed to be a wrapper around the built-in functionality of your server. For example, ServerData provides access to the Customer table. The goal of the Automation server is simply to export that functionality to other programs. As a result, the fact that the Automation object would simply wrap methods already existing in the program makes sense.
The TForm1 implementation of GetData looks like this:
Variant __fastcall TForm1::GetData() { TCustomerRecord *Customer = new TCustomerRecord(); void *P; DMod->GetCustAry(*Customer); Variant V(OPENARRAY(int, (0, sizeof(TCustomerRecord))), varByte); P = VarArrayLock(V); memcpy(P, Customer, sizeof(TCustomerRecord)); VarArrayUnlock(V); return V; }
This method asks the data module to retrieve a custom structure that contains the data from the Customer table. I will explain how that process works in one moment. For now, just concentrate on the fact that the GetData method converts the custom structure into a variant array by using VarArrayLock and VarArrayUnlock. This process was described earlier in the chapter, in the section on the VarArray program.
The custom data structure used by this program consists of an array of TCustomer structures:
struct TCustomer { char CustNo[256]; char Company[256]; char Address[256]; char City[256]; char State[256]; char Zip[256]; }; typedef TCustomer TCustAry[100];
The program takes this array and hides inside a custom structure that defines the number of records in the array:
struct TCustomerRecord { int Count; TCustAry CustAry; };
Clearly, I could find more memory-efficient ways to store this data, but I wanted to keep this part of the program simple so that you would be able to follow the logic of the program without getting bogged down by a mass of irrelevant pointer manipulation. The important point of this program is how it handles OLE Automation; finding the best way to store data in memory is really another subject altogether.
After you declare the data structures, you simply need to fill them out in the data module for the application:
void TDMod::GetCustAry(TCustomerRecord &Customer) { int i = 0; Variant V; Double Num; CustomerTable->First(); CustomerSource->Enabled = False; while (!CustomerTable->Eof) { Num = CustomerTable->FieldByName("CustNo")->AsFloat; sprintf(Customer.CustAry[i].CustNo, "%f", Num); strcpy(Customer.CustAry[i].Company, CustomerTableCompany->AsString.c_str()); strcpy(Customer.CustAry[i].Address, CustomerTableAddr1->AsString.c_str()); strcpy(Customer.CustAry[i].City, CustomerTableCity->AsString.c_str()); strcpy(Customer.CustAry[i].State, CustomerTableState->AsString.c_str()); strcpy(Customer.CustAry[i].Zip, CustomerTableZip->AsString.c_str()); i++; CustomerTable->Next(); } Customer.Count = i - 1; CustomerSource->Enabled = True; }
This code simply iterates through the entire dataset, using brute-force methods to copy the data into the array. Notice that I disable the DataSource for the module so that the program does not waste time updating the visual display for a program that is, after all, running on a remote server.
The data module also provides a method for updating the dataset when the user sends back a record with new data:
void TDMod::Update(TCustomer Customer) { float Value; UpdateQuery->Close(); UpdateQuery->Params->Items[0]->AsString = Customer.Company; UpdateQuery->Params->Items[1]->AsString = Customer.Address; UpdateQuery->Params->Items[2]->AsString = Customer.City; UpdateQuery->Params->Items[3]->AsString = Customer.State; UpdateQuery->Params->Items[4]->AsString = Customer.Zip; Value = StrToFloat(Customer.CustNo); UpdateQuery->Params->Items[5]->AsFloat = Value; UpdateQuery->ExecSQL(); }
The preceding is just standard TQuery code, of the type that was explained in depth in Chapter 10, "SQL and the TQuery Object."
The SQL for the UpdateQuery looks like this:
update Customer set Company = :Company, Addr1 = :Address, City = :City, State = :State, Zip = :Zip where CustNo = :CustNo
As you can see, the code will update an existing record given its CustNo. However, this program makes no provisions for inserting new data. Obviously, adding that functionality to the program would not be hard, but I have not done so in this example. In particular, all you would have to do is insert the new record rather than just update it. You would, however, have to provide a technique for providing a valid CustNo.
That's all I'm going to say about the ServerData program. You will probably want to study a few other parts of the program on your own, but overall this program is not a complex piece of work. One of the great advantages of DCOM and OLE Automation is that both technologies are easy to use.
The GetData application, found on the CD that accompanies this book, shows how to access a remote dataset from a client application. A simple menu allows you to retrieve a dataset from either a local OLE Automation server or from a remote DCOM Automation server. In both cases, the server is the ServerData application explained in the preceding section of this chapter.
After the user connects to the data, it is
displayed in the main form of the program,
as shown in Figure 27.8. The user can then edit the data in a custom form, as shown
in Figure 27.9.
FIGURE 27.8.
Viewing the data retrieved over the network from a remote server.
FIGURE 27.9. Editing a row of data before sending it back to the server.
The code for the GetData program is shown in Listing 27.18 through Listing 27.21.
I do not show the Globals unit here because it was included in the listings
for the ServerData program. I also omit the CodeBox
unit, which is found
in the Utils directory on the CD that accom- panies this book. I bring in
the CodeBox unit because I need to call CreateRemoteObject. I supplied
the full source for CreateRemoteObject previously
in the section "Creating
the DCOM Client."
Listing 27.18. The header file for
the main module of the GetData application. GetData is an OLE client that retrieves
a database table from
a server via OLE Automation.
/////////////////////////////////////// // Main.h // Project: GetData // Copyright (c) 1997 by Charlie Calvert // #ifndef MainH #define MainH #include <vcl\Classes.hpp> #include <vcl\Controls.hpp> #include <vcl\StdCtrls.hpp> #include <vcl\Forms.hpp> #include "Grids.hpp" #include <vcl\Buttons.hpp> #include <vcl\Menus.hpp> #include "globals.h" class TForm1 : public TForm { __published: TStringGrid *Grid; TMainMenu *MainMenu1; TMenuItem *File1; TMenuItem *MakeConnection1; TMenuItem *MakeConnectionLocal1; TMenuItem *N1; TMenuItem *Exit1; TMenuItem *Options1; TMenuItem *Edit1; TMenuItem *UpdateData1; void __fastcall MakeConnectionBtnClick(TObject *Sender); void __fastcall ShowDataClick(TObject *Sender); void __fastcall Edit1Click(TObject *Sender); void __fastcall UpdateData1Click(TObject *Sender); void __fastcall MakeConnection1Click(TObject *Sender); private: Variant V; TCustomerRecord FCustomerRecord; void FillGrid(); public: __fastcall TForm1(TComponent* Owner); }; extern TForm1 *Form1; #endif
Listing 27.19. The main module for the GetData program.
/////////////////////////////////////// // Main.cpp // Project: GetData // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "Main.h" #include "OleAuto.hpp" #include "Globals.h" #include "codebox.h" #include "EditData1.h" #include "initguid.h" #pragma link "Grids" #pragma resource "*.dfm" TForm1 *Form1; __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } void __fastcall TForm1::MakeConnectionBtnClick(TObject *Sender) { V = CreateOleObject("DataServer.DataServer"); ShowDataClick(NULL); } DEFINE_GUID(CLSID_IDATASERVER, 0x34BADDC0, 0x884F, 0x11D0, 0xBC,0xD7,0x00,0x80,0xC8,0x0C,0xF1,0xD2); void __fastcall TForm1::MakeConnection1Click(TObject *Sender) { CreateRemoteObject(CLSID_IDATASERVER, "143.186.149.228", V); ShowDataClick(NULL); } void TForm1::FillGrid() { int i; Grid->RowCount = FCustomerRecord.Count; for (i = 0; i < FCustomerRecord.Count; i++) { Grid->Cells[0][i] = FCustomerRecord.CustAry[i].CustNo; Grid->Cells[1][i] = FCustomerRecord.CustAry[i].Company; Grid->Cells[2][i] = FCustomerRecord.CustAry[i].Address; Grid->Cells[3][i] = FCustomerRecord.CustAry[i].City; Grid->Cells[4][i] = FCustomerRecord.CustAry[i].State; Grid->Cells[5][i] = FCustomerRecord.CustAry[i].Zip; } } void __fastcall TForm1::ShowDataClick(TObject *Sender) { Variant Data; void *P; Data = V.OleFunction("GetData"); P = VarArrayLock(Data); memcpy(&FCustomerRecord, P, sizeof(TCustomerRecord)); VarArrayUnlock(Data); FillGrid(); } void __fastcall TForm1::Edit1Click(TObject *Sender) { /* if (EditData->EditCustomer(FCustomerRecord.CustAry[Grid->Selection.Top]) == mrOk) { FillGrid(); Variant Temp = EditData->GetCustomerAsVariant(); V.OleFunction("UpDateRecord", Temp); } */ if (EditData->EditCustomer(FCustomerRecord.CustAry[Grid->Selection.Top]) == mrOk) { FillGrid(); EditData->SendCustomerAsStrings(V); } } void __fastcall TForm1::UpdateData1Click(TObject *Sender) { ShowDataClick(NULL); }
Listing 27.20. The EditData module provides a form for editing an individual record. This is the header file for the unit.
///////////////////////////////////////// EditData.h // Project: GetData // Copyright (c) 1997 by Charlie Calvert // #ifndef EditData1H #define EditData1H #include <vcl\Classes.hpp> #include <vcl\Controls.hpp> #include <vcl\StdCtrls.hpp> #include <vcl\Forms.hpp> #include <vcl\ExtCtrls.hpp> #include <vcl\Buttons.hpp> #include "globals.h" class TEditData : public TForm { __published: TLabel *Label1; TLabel *Label2; TLabel *Label3; TLabel *Label4; TLabel *Label5; TLabel *Label6; TBevel *Bevel1; TEdit *ECompany; TEdit *EAddress; TEdit *ECity; TEdit *EState; TEdit *EZip; TEdit *ECustNo; TBitBtn *BitBtn1; TBitBtn *BitBtn2; void __fastcall BitBtn1Click(TObject *Sender); private: TCustomer FCustomer; public: __fastcall TEditData(TComponent* Owner); Variant GetCustomerAsVariant(); void GetCustomer(); void FillCustomer(); int EditCustomer(TCustomer &ACustomer); void SendCustomerAsStrings(Variant &V); }; extern TEditData *EditData; #endif
Listing 27.21. The EditData module provides a form for editing an individual record. You can then send the updated data back to the server via OLE Automation.
/////////////////////////////////////// // EditData.cpp // Project: GetData // Copyright (c) 1997 by Charlie Calvert // #include <vcl\vcl.h> #pragma hdrstop #include "EditData1.h" #pragma resource "*.dfm" TEditData *EditData; __fastcall TEditData::TEditData(TComponent* Owner) : TForm(Owner) { } Variant TEditData::GetCustomerAsVariant() { void *P; GetCustomer(); Variant V(OPENARRAY(int, (0, sizeof(TCustomer))), varByte); P = VarArrayLock(V); memcpy(P, &FCustomer, sizeof(TCustomer)); VarArrayUnlock(V); return V; } void TEditData::SendCustomerAsStrings(Variant &V) { GetCustomer(); V.OleProcedure("UpdateParams", FCustomer.CustNo, FCustomer.Company,FCustomer.Address, FCustomer.City, FCustomer.State, FCustomer.Zip); } void TEditData::GetCustomer() { strcpy(FCustomer.CustNo, ECustNo->Text.c_str()); strcpy(FCustomer.Company, ECompany->Text.c_str()); strcpy(FCustomer.Address, EAddress->Text.c_str()); strcpy(FCustomer.City, ECity->Text.c_str()); strcpy(FCustomer.State, EState->Text.c_str()); strcpy(FCustomer.Zip, EZip->Text.c_str()); } void TEditData::FillCustomer() { ECustNo->Text = FCustomer.CustNo; ECompany->Text = FCustomer.Company; EAddress->Text = FCustomer.Address; ECity->Text = FCustomer.City; EState->Text = FCustomer.State; EZip->Text = FCustomer.Zip; } int TEditData::EditCustomer(TCustomer &ACustomer) { FCustomer = ACustomer; FillCustomer(); int Result = ShowModal(); if (Result == mrOk) ACustomer = FCustomer; return Result; } void __fastcall TEditData::BitBtn1Click(TObject *Sender) { GetCustomer(); }
This program starts out by retrieving the server either locally or remotely:
void __fastcall TForm1::MakeConnectionBtnClick(TObject *Sender) { V = CreateOleObject("DataServer.DataServer"); ShowDataClick(NULL); } DEFINE_GUID(CLSID_IDATASERVER, 0x34BADDC0, 0x884F, 0x11D0, 0xBC,0xD7,0x00,0x80,0xC8,0x0C,0xF1,0xD2); void __fastcall TForm1::MakeConnection1Click(TObject *Sender) { CreateRemoteObject(CLSID_IDATASERVER, "143.186.149.228", V); ShowDataClick(NULL); }
I
described all the code shown here in some depth earlier in the chapter. Notice
that I use the GUID from the OLE server to retrieve the program from a remote location.
As I mentioned earlier, you can use the DComCfg.exe application to access
remote servers using the same techniques used with local servers. However, you'll
experience some drawbacks using this system when Windows 95 is involved in the equation.
Figure 27.10 shows DComCfg.exe running on an NT server.
FIGURE 27.10.
You can use the DComCfg.exe utility to make remote servers appear
as local servers so that you can call them with
CreateOleObject.
After you connect to the server, you can ask it for a copy of the dataset from the Customer table and then display the data to the user:
void __fastcall TForm1::ShowDataClick(TObject *Sender) { Variant Data; void *P; Data = V.OleFunction("GetData"); P = VarArrayLock(Data); memcpy(&FCustomerRecord, P, sizeof(TCustomerRecord)); VarArrayUnlock(Data); FillGrid(); }
This code calls the GetData function of the ServerData program. It then locks down the variant array returned by the server and extracts the custom record from it. This operation is the reverse of the operation performed in the ServerData, where you say how to pack the custom data into a variant array.
The FillGrid method simply displays the data in a string grid:
void TForm1::FillGrid() { int i; Grid->RowCount = FCustomerRecord.Count; for (i = 0; i < FCustomerRecord.Count; i++) { Grid->Cells[0][i] = FCustomerRecord.CustAry[i].CustNo; Grid->Cells[1][i] = FCustomerRecord.CustAry[i].Company; Grid->Cells[2][i] = FCustomerRecord.CustAry[i].Address; Grid->Cells[3][i] = FCustomerRecord.CustAry[i].City; Grid->Cells[4][i] = FCustomerRecord.CustAry[i].State; Grid->Cells[5][i] = FCustomerRecord.CustAry[i].Zip; } }
The Cells property of the TStringGrid object allows you to access the array of data underlying the grid.
Now that the user can see the remote dataset, the only thing left to do is give him or her a chance to edit it. The following line of code retrieves the currently selected row from the string grid:
if (EditData->EditCustomer(FCustomerRecord.CustAry[Grid->Selection.Top]) == mrOk)
The key point to grasp here is that Grid->Selection.Top designates the currently selected row in the grid.
Inside the TEditData form, only one routine is of any real interest. This routine is called GetCustomerAsVariant:
Variant TEditData::GetCustomerAsVariant() { void *P; GetCustomer(); Variant V(OPENARRAY(int, (0, sizeof(TCustomer))), varByte); P = VarArrayLock(V); memcpy(P, &FCustomer, sizeof(TCustomer)); VarArrayUnlock(V); return V; }
This code uses the GetCustomer function, which follows, to retrieve the newly edited data from the TEditData form. It then moves the data into a variant array by first locking down the array and then moving some bytes around via a call to memcpy. Here is the simple GetCustomer method used to retrieve the data from the visual controls in the TEditForm:
void TEditData::GetCustomer() { strcpy(FCustomer.CustNo, ECustNo->Text.c_str()); strcpy(FCustomer.Company, ECompany->Text.c_str()); strcpy(FCustomer.Address, EAddress->Text.c_str()); strcpy(FCustomer.City, ECity->Text.c_str()); strcpy(FCustomer.State, EState->Text.c_str()); strcpy(FCustomer.Zip, EZip->Text.c_str()); }
If you don't want to pass the data back to the server using a variant array, you can just pass the strings of the record back directly:
void TEditData::SendCustomerAsStrings(Variant &V) { GetCustomer(); V.OleProcedure("UpdateParams", FCustomer.CustNo, FCustomer.Company, FCustomer.Address, FCustomer.City, FCustomer.State, FCustomer.Zip); }
This code retrieves the text that the user has edited and then calls the UpdateParams procedure of the OLE server. UpdataParams will execute a SQL update statement to insert the new data into the Customer table.
That's all I'm going to say about remote datasets. They're one of the more powerful aspects of DCOM, and I'm sure you can imagine many other ways to use this technology. If you want to extend this technology with a set of robust tools, you should look into Entera, a remote client/server technology provided by Borland.
In this chapter, you learned how to use BCB to build applications that take advantage of the Distributed Component Object Model, or DCOM. You have seen that combining BCB, DCOM, and OLE Automation provides a simple method for allowing one application to control or use another application that resides on a second machine.
People who are interested in this field should look at Borland's Entera and OLEnterprise products, as well as the very powerful OLE-based tools found in Delphi 3.0. The plan at the time of this writing is for all the tools in Delphi 3.0 to appear in future versions of BCB.
Windows programmers have seen so many extraordinary technical developments in the last few years that it's difficult to single out any one technology and say that it is significantly more important than the rest. Nevertheless, DCOM appears to be a viable solution to one of the major problems faced by contemporary programmers. In short, we can now easily distribute the workload of a particular product across multiple machines. Just how much impact this technology will have on the industry is hard to say at this early stage, but DCOM (and related technologies such as CORBA) certainly have the potential to change the way we build applications.
©Copyright, Macmillan Computer Publishing. All rights reserved.