Overview
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:
- An overview of DCOM technology
- Some caveats and general comments about DCOM, including a brief overview
of competing technologies such as DSOM and CORBA
- Using TAutoObject to implement a DCOM server
- Creating a DCOM client
- Reviewing some key points about the Registry
- Working with remote datasets
- Using DCOM to access a database server on a different system without
loading any database tools on your current system
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.
What Is DCOM?
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.
Why Is COM Controversial?
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.
DCOM, IDispatch, Marshaling, and OLE Automation
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.
Thinking About 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.
Using TAutoObject to Implement a DCOM Server
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.
Creating the DCOM Client
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:
- DCOM allows you to call objects located in one application or DLL on one
machine from inside a separate application on a separate machine.
- You can use TAutoObject to create the server side of your
application.
- You can use the custom CreateRemoteObject to call the server from
a client program located on a second server.
Registration Issues
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.
Using Variant Arrays to Pass Data
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:
- One enables you to look at the dimensions and bounds of two different
variant arrays. The first array has one dimension, and the second has two.
- The second pop-up menu enables you to display an array in a string grid
using two different methods. The first method accesses the array through
standard techniques, and the second lets you lock down the data before
accessing it.
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.
Using Remote Datasets with DCOM
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.
Understanding the ServerData Program
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.
GetData: The Client Program for Remote Datasets
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.
Summary
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.
|