2.3 C3PO Sample: the Customer Tracking Application

2.3.1 Creating the Customer-Tracking C3PO with Delphi

Start Delphi and create a new project. Rename the project to CusTrack. Rename unit1.pas to exesrv, change the form name to CTS, and set the form caption to Customer Tracking.

C3POServer

Add a new OLE object called C3POServer. This creates a new unit; rename it to servobj.pas. In addition, an object class named C3POServer will be created.

The C3POServer object is the first object that needs to be created for the C3PO. Change C3POServer so that it subclasses from TC3POServer instead of TAutoObject. Then add functions, procedures, and properties as shown in the following:

C3POServer = class(TC3POServer) 
private 
{ Private declarations } 
function GetCmdFact: Variant; 
function GetDescription : string; 
automated 
{ Automated declarations } 
property CommandFactory : Variant read GetCmdFact; 
property Description: string read GetDescription; 
function CanShutdown: TOleBool; 
procedure Init(Manager: variant); 
end;

The C3POServer object is required and all properties must be set up. TC3POServer is defined in C3POin.pas, and is a complete C3POServer object with all C3PO objects and routines necessary for these objects. The C3POServer that you defined subclasses TC3POServer, and overrides only the routines that you need to use. TC3POServer will handle the others. This keeps your servobj.pas simple and easy to read.

Add C3POin.pas and ObjApiin.pas from the SDK to your project. These files are similar to C header files. They contain information you need for using C3PO software and the Object API. In servobj.pas, add C3POin, OLE2, and Windows to the uses directive as shown in the following:

uses 
OLEAuto, OLE2, Windows, C3POin;

Add routines for the functions and procedures defined in the C3POServer object. This is shown in servobj.pas. Notice that C3POServer.Init( ) saves "manager" into a global variable called g_C3POManager, which is used later to get the ClientState.

C3POServer.GetCmdFact returns g_CommandFactory.OLEObject, which means you need to define a CommandFactory object. This is done in the sample under the vars directive. Make g_CommandFactory a global variable of type CommandFactory. Then, under initialization, give g_CommandFactory to CommandFactory.Create. This creates a CommandFactory object. Finally, under finalization, release g_CommandFactory to free up memory.

CommandFactory

A CommandFactory class must be defined. To do this, create a new unit called C3PO.pas, then add C3PO to the uses directive in servobj.pas. Next, create a CommandFactory class in C3PO.pas as shown in the following:

CommandFactory = 
class(TCommandFactory) 
private 
ContextMenuID: integer; 
public 
public 
Constructor Create;     // Used to create CommandFactory Object 
automated 
function CustomizeMenu( Context: string; 
GWMenu: variant): TOleBool; 
function Init(lcid : longint): longint; 
end;

You are subclassing from TCommandFactory defined in C3POin.pas. You are going to change only the menus at this time, so all you need to define are the CustomizeMenu and Init functions.

Customize Menus

Create the CommandFactory.Init( ) function as in the sample. Return eGW_CMDINIT_MENUS to say that you are customizing the menus.

Create the CommandFactory.CustomizeMenu( ) function. This is the routine that will change the GroupWise client menus. See the sample.

Each menu item must have a GWCommand object. To do this, you need to define a Command class as in the following example:

Command = class(TGWCommand) 
private 
{ Private declarations } 
LongPrmt : string; 
ToolTp : string; 
function GetLongPrompt : string; 
function GetToolTip : string; 
public 
m_nCmd : longint;     // Command ID information 
Constructor Create(nCmd: longint);     // Used to create Command Object 
automated 
property LongPrompt: string read 
GetLongPrompt; 
property ToolTip: string read 
GetToolTip; 
procedure Execute; 
function Validate: longint; 
end;

Create the functions and procedures as in the sample. As a command object for each menu item is created, a unique ID is stored in m_nCmd. This ID is used to determine which menu item has been chosen by the user when Execute is called.

Compile and register CusTrack.

Customize ContextMenus

CusTrack also customizes the context menu by overriding CommandFactory.CustomizeContextMenu( ) in CommandFactory. To do this, add the routine shown in the following example. Change CommandFactory.Init( ) to look like this example:

result := eGW_CMDINIT_MENUS or     // modify menus 
eGW_CMDINIT_CONTEXT_MENUS;         // modify context menus

Customize Toolbars

CusTrack customizes the toolbar by overriding CommandFactory.CustomizeToolBar in CommandFactory. To do this, add the routine as shown in the following example. Change CommandFactory.Init to look like this example:

result := eGW_CMDINIT_MENUS or     // modify menus 
eGW_CMDINIT_CONTEXT_MENUS or       // modify context menus 
eGW_CMDINIT_TOOLBARS;              // modify toolbars

Predefined Commands

The CusTrack C3PO defines three new objects:

  • CTS_COMPANY_OBJ = ’GW.MESSAGE.MAIL.NGWCOMPANY’;
  • CTS_CONTACT_OBJ = ’GW.MESSAGE.MAIL.NGWCONTACT’;
  • CTS_ACTION_OBJ = ’GW.MESSAGE.MAIL.NGWACTION’;

These objects subclass from GW.MESSAGE.MAIL.

CusTrack supports the Open predefined command for these three objects. To do this, you need to set up the registry. You also need to override CommandFactory.WantCommand and CommandFactory.BuildCommand as shown in C3PO.pas. WantCommand checks context and the PersistentID to see if the predefined command that is being set up will be supported. A check is made to see if it is the Open command, and then the context is checked to make sure it is one of the new objects. TRUE is then returned to tell the C3POManager that we will handle the open of these new objects. In BuildCommand a check is again made for the PersistentID and Context for the proper objects. A GWCommand is then built to handle Open.

IconFactory

To show your own icons for your custom objects, you need to support IconFactory. Override the IconFactory property in C3POServer as shown in the following example:

C3POServer = class(TC3POServer) 
private 
{ Private declarations } 
function GetCmdFact: Variant; 
function GetDescription : string; 
function GetIconFactory : variant; 
automated 
{ Automated declarations } 
property CommandFactory : Variant read GetCmdFact; 
property Description: string read 
GetDescription; 
property IconFactory: variant read GetIconFactory; 
function CanShutdown: TOleBool; procedure Init(Manager: variant); 
end;

Under the vars directive in servobj.pas, add the following:

g_IconFactory : IconFactory;     // Create global IconFactory object

Under the initialization directive in servobj.pas, add the following:

g_IconFactory := IconFactory.Create;

Under the finalization directive in servobj.pas, add the following:

g_IconFactory.Release;

In C3PO.pas, define IconFactory as shown below.

IconFactory = class(TIconFactory) 
private 
{ Private declarations } 
public 
automated 
procedure GetIcons( 
ObjClass: string; 
var pIconFile: string; 
var plUnOpenIcon: longint; 
var plOpenIcon:longint); 
end;

Create the GetIcons procedure as shown in the sample.

EventMonitor

CusTrack uses two events, OnReady and OnShutdown. To do this, override the EventMonitor property in C3POServer as shown in the following example:

C3POServer = class(TC3POServer) 
private 
{ Private declarations } 
function GetCmdFact: Variant; function GetDescription : string; 
function GetEventMonitor : variant; 
function GetIconFactory : variant; 
automated 
{ Automated declarations } 
property CommandFactory : Variant read GetCmdFact; 
property Description: string read 
GetDescription; 
property EventMonitor: variant read GetEventMonitor; 
property IconFactory: variant read GetIconFactory; 
function CanShutdown: TOleBool; 
procedure Init(Manager: variant); 
end;

Under the vars directive in servobj.pas, add the following:

g_EventMonitor :EventMonitor;     // Create global EventMonitor object

Under the initialization directive in servobj.pas, add the following:

g_EventMonitor :=EventMonitor.Create;

Under the finalization directive in servobj.pas, add the following:

g_EventMonitor.Release;

In C3PO.pas, define EventMonitor as shown in the following:

EventMonitor =  class(TEventMonitor) 
private 
public 
automated 
procedure Notify(Context: string; evt: variant); 
end;

Create the Notify procedure as shown in the sample.

2.3.2 Creating the Customer-Tracking C3PO with C++

The Customer tracking sample application was created as a in-process server (DLL) using Microsoft Visual C++ 4.0. A C3PO in C/C++ must use a COM interface. Begin by creating a DLL project called C3PO.

Add a new file to the project called C3PO.cpp. In C3PO.cpp add new routines DllGetClassObject, DllCanUnloadNow, and BuildIUnkDispatch as shown in the C3PO.cpp sample. Next, use GUIDGEN.EXE to define a new GUID for the COM server. Put the definition in C3PO.h. For example:

DEFINE_GUID(CLSID_SAMPLEC3PO,  
            0xd49, 0ce00, 0x8bb, 0x11cf, 0xbb, 0xf3,  
            0x0, 0x20, 0xaf, 0xe0, 0x28, 0x9c);

The next step is to build a Class Factory. In C3PO.h define MyFactory as shown in the following:

class MyFactory : 
public IClassFactory 
{ 
public: 
/* IUnknown methods */ 
STDMETHOD(QueryInterface)( 
THIS_ REFIID riid, 
LPVOID FAR*ppvObj); 
STDMETHOD_(ULONG, AddRef)(THIS); 
STDMETHOD_(ULONG, Release)(THIS); 
STDMETHODIMP CreateInstance(IUnknown *, REFIID, void**); 
STDMETHODIMP LockServer(BOOL); 
MyFactory( ); 
~MyFactory( ); 
private: 
ULONG m_cRef;

Next, you need to build routines for the methods defined in MyFactory as shown in C3PO.cpp. In MyFactory::CreateInstance, build a C3POServer called CC3PO.

STDMETHODIMP MyFactory::CreateInstance( 
IUnknown *pUnkOuter,  
REFIID riid, 
void** ppv) 
{ 
if(NULL != pUnkOuter) 
return CLASS_E_NOAGGREGATION; 
CC3PO *pIC3PO = new CC3PO; 
if(pIC3PO == NULL) 
return E_OUTOFMEMORY; 
pIC3PO->Create( ); 
HRESULT hr = pIC3PO->QueryInterface(riid, ppv); 
if(FAILED(hr)) 
delete pIC3PO; 
else 
g_cObj++; 
return hr; 
}

C3POServer

You now need to define a C3POServer object. Every C3PO must support this interface. It is used to initialize the C3PO. Add a new Class called CC3PO in C3PO.h. Subclass it from IC3POServer. This is the object that was created in MyFactory::CreateInstance.

class CC3PO : public 
IC3POServer 
{ 
public:     // IUnknown methods 
STDMETHOD(QueryInterface)( 
     THIS_ REFIID riid, 
     LPVOID FAR* ppvObj); 
STDMETHOD_(ULONG, AddRef)(THIS); 
STDMETHOD_(ULONG, Release)(THIS); 
/* IC3POServer methods */ 
STDMETHOD(get_CommandFactory)( 
     THIS_ IDispatch * FAR*ppIDispCommandFactory); 
STDMETHOD(get_Description)( 
     THIS_ BSTR FAR* pbstrDescription); 
STDMETHOD(get_EventMonitor)( 
     THIS_ IDispatch * FAR*ppIDispEventMonitor); 
STDMETHOD(get_IconFactory)( 
     THIS_ IDispatch * FAR*ppIDispIconFactory); 
STDMETHOD(CanShutdown)( 
     THIS_ VARIANT_BOOL FAR* pbCanShutdown); 
STDMETHOD(DeInit)(THIS); 
STDMETHOD(Init)( 
     THIS_ IDispatch * pIDispManager);     // Constructor 
CC3PO( );     // Destructor 
virtual ~CC3PO( );     // RefCount required method 
BOOL Create( ); 
private: 
ULONG m_cRef; 
CCommandFactory  *m_pICmdFact; 
EventMonitor  *m_pIEventMonitor; 
IconFactory  *m_pIIconFactory; 
IUnknown *m_pIUnkDispServ; 
};

All IC3POServer methods must be declared. If a method is not needed, it simply does a return. The Init( ) method is the first one called and passes in the C3POManager. The Manager object is saved to get the Client State and is valid until a future DeInit( ) call. If the server fails the call, the C3POServer is unloaded.

The Customer Tracking C3PO uses the functionality from the CommandFactory, EventMonitor and IconFactory methods. In Create( ) they are created and in QueryInterface( ) a check of the riid is made for these interfaces and the appropriate object is returned. In each get the interface for the object is returned. See C3PO.cpp.

CommandFactory

This interface is used to manage commands in GroupWise. Commands are located in menus and toolbars. Since menu, context menu, and the toolbar are to be modified in Customer Tracking , this interface must be supported. Begin by defining a new class in Setupcmd.h called CCommandFactory.

class CCommandFactory : 
public ICommandFactory 
{ 
public: 
/* IUnknown methods */ 
STDMETHOD(QueryInterface)( 
     THIS_ REFIID riid, LPVOID FAR*ppvObj); 
STDMETHOD_(ULONG, AddRef)(THIS) 
{ return m_pUnkOuter->AddRef( ); } 
STDMETHOD_(ULONG, Release)(THIS) 
{ return m_pUnkOuter->Release( ); } 
/* ICommandFactory methods */ 
STDMETHOD(BuildCommand)( 
     THIS_ BSTR bstrContext, 
     BSTR bstrPersistentID, 
     IDispatch *pIDispBaseCommand, 
     IDispatch * pIDispParameters, 
     IDispatch *FAR* ppIDispGWCommand); 
STDMETHOD(CustomizeContextMenu)( 
     THIS_ BSTR bstrContext, 
     IDispatch * pIDispGWMenu); 
STDMETHOD(CustomizeMenu)( 
     THIS_ BSTR bstrContext, 
     IDispatch *pIDispGWMenu, 
     VARIANT_BOOL FAR* pbChanged); 
STDMETHOD(CustomizeToolbar)( 
     THIS_ BSTR bstrContext, 
     IDispatch *pIDispGWToolbar, 
     VARIANT_BOOL FAR* pbChanged 
STDMETHOD(Init 
     THIS_ long lcid, 
     long FAR* plCmdFlags 
STDMETHOD(WantCommand 
     THIS_ BSTR bstrContext, 
     BSTR bstrPersistentID, 
VARIANT_BOOL FAR*pbChanged 
CCommandFactory(IUnknown *pUnk 
CCommandFactory 
private 
IUnknown   *m_pUnkOuter; 
IUnknown *m_pIUnkDispFact; 
};

All methods in CommandFactory must be supported. After QueryInterface( ), the first method to be called is the Init( ) routine.

STDMETHODIMP CCommandFactory::Init(long lcid, 
                                   long FAR* plCmdFlags) 
{ 
*plCmdFlags = eGW_CMDINIT_MENUS     // modify menus 
eGW_CMDINIT_CONTEXT_MENUS |         // modify context menus 
eGW_CMDINIT_TOOLBARS;               // modify toolbars 
return NOERROR; 
}

A flag is returned in *plCmdFlags indicating if modify menus, context menus and/or toolbars are to be modified. In this case, all of them will be modified.

CustomizeMenus

The CustomizeMenu method must be supported. If you do not want to customize a menu, simply return NOERROR. To support the method, you must first define a new class named CGWCommand in Setupcmd.h.

class CGWCommand : 
public IGWCommand 
{ 
public: 
/* IUnknown methods */ 
 STDMETHOD(QueryInterface)( 
THIS_ REFIID riid, LPVOID FAR* ppvObj); 
STDMETHOD_(ULONG,AddRef) ( ); 
STDMETHOD_(ULONG,Release) ( ); 
/* IGWCommand methods */ 
STDMETHOD(get_BaseCmd)( 
     THIS_ IDispatch * FAR* ppIDispBaseCmd); 
STDMETHOD(get_LongPrompt)( 
     THIS_ BSTR FAR* pbstrLongPrompt); 
STDMETHOD(get_Parameters)( 
     THIS_ IDispatch * FAR* ppIDispBaseCmd); 
STDMETHOD(get_PersistentID)( 
     THIS_ BSTR FAR* pbstrPersistentID); 
STDMETHOD(get_ToolTip)( 
     THIS_ BSTR FAR* pbstrToolTip); 
STDMETHOD(Execute)(THIS); 
STDMETHOD(Help)(THIS); 
STDMETHOD(Undo)(THIS); 
STDMETHOD(Validate)( 
     THIS_ long FAR* plValidate); 
CGWCommand(int nID); 
~CGWCommand( ); 
BSTR bstrLongPrompt; 
BSTR bstrToolTip; 
private: 
ULONG  m_cRef; 
int   m_nID; 
IUnknown *m_pIUnkDisp; 
};

Once again, all methods must be supported. LongPrompt is only for menu items and Tooltip is only for toolbar items. One command is distinguished from another with the m_nID. As each command is created it is given a unique ID, so that you can tell each command apart. Validate returns whether the command is enabled, disabled, or checked for menu items and toolbar items. Execute is called when the item has been selected by the user. See Setupcmd.cpp.

With GWCommand set up, you can now build or modify a menu item. CCommandFactory::CustomizeMenu( ) adds another menu to the New menu of the File menu. In the new menu, called Customer Tracking, three new menu items are included. See CCommandFactory::CustomizeMenu( ) in Setupcmd.cpp.

In CGWCommand::Validate( ), a check is made to see if any customer tracking message has been selected by the user. If no customer tracking message has been selected, the contact and action menu items are disabled and company menu item is enabled. If the user has selected a company message, then the company and contact menu items are enabled. If the user has selected a contact message, then the company and action menu items are enabled.

CustomizeContextMenu

For customer tracking, you want to add a new item to the context menu only when the user has selected a customer tracking message item, and then right-clicks to bring up the context menu. To do this the value of bstrContext is checked to see if it is a CTS_COMPANY_OBJ, CTS_CONTACT_OBJ or CTS_ACTION_OBJ message object. If the user has selected a company object that then a new menu item is set for creating a contact message. If the user has selected a contact message, then a new menu item is set for creating an action message. If the user has selected an action message, a new menu item is set for creating another action message. This is done in CCommandFactory::CustomizeContextMenu( ) in Setupcmd.cpp.

CustomizeToolBars

For customer tracking, three new buttons are set up on the toolbar, one each to create a new company, contact, and action message. CCommandFactory::CustomizeToolbar( ) makes the modifications to the toolbar. Since the same command IDs in CustomizeToolbar( ) are used as in CustomizeMenu( ), the buttons on the toolbar will act the same as the items on the custom menu that was created.

Predefined Commands

The CusTrack C3PO defines three new objects:

  • CTS_COMPANY_OBJ = ’GW.MESSAGE.MAIL.NGWCOMPANY’;
  • CTS_CONTACT_OBJ = ’GW.MESSAGE.MAIL.NGWCONTACT’;
  • CTS_ACTION_OBJ = ’GW.MESSAGE.MAIL.NGWACTION’;

These objects subclass from GW.MESSAGE.MAIL.

CusTrack supports the Open predefined command for these three objects. To do this, enter the registry entries shown in Cts.reg. After the C3PO is registered, CCommandFactory::WantCommand( ) will be called when the user attempts to open one of the custom objects. In WantCommand, check bstrPersistentID see if the Open command is being called. If so then *pbChanged = TRUE is returned. If not, FALSE is returned. If TRUE is returned, then CCommandFactory::BuildCommand( ) is called. Here you once again check for the correct persistent ID, and if it is correct build a GWCOMMAND for this Open. See Setupcmd.cpp.

IconFactory

This interface is used to retrieve icons that represent the state of C3PO records. Use IconFactory to change the icons for custom objects.

class IconFactory : public 
IIconFactory 
{ 
public: 
/* IUnknown methods */ 
STDMETHOD(QueryInterface)( 
     THIS_ REFIID riid, LPVOID FAR*ppvObj); 
STDMETHOD_(ULONG, AddRef)(THIS) 
{ return m_pUnkOuter->AddRef( ); } 
STDMETHOD_(ULONG, Release)(THIS) 
{ return m_pUnkOuter->Release( ); } 
/* IconFactory methods */ 
STDMETHOD(GetIcons)( 
     THIS_ BSTR bstrObjClass, 
     BSTR FAR*pbstrIconFile, 
     long FAR* plUnOpenIcon, 
     long FAR*plOpenIcon); 
IconFactory(IUnknown *pUnk); 
~IconFactory( ); 
private: 
IUnknown *m_pUnkOuter; 
IUnknown *m_pIUnkDispFact; 
};

In GetIcons( ), bstrObjClass is checked to see which custom object is being called for, then the icon file and the icon index for both an opened and unopened state of the object are returned. See Setupcmd.cpp. In addition, IconFactory needs to be registered; an example is found in Cts.reg.

EventMonitor

Customer Tracking uses two events, OnReady and OnShutdown.

class EventMonitor : 
public IEventMonitor 
{ 
public: 
/* IUnknown methods */ 
STDMETHOD(QueryInterface)( 
     THIS_ REFIID riid, LPVOID FAR* ppvObj); 
STDMETHOD_(ULONG, AddRef)(THIS) 
{ return m_pUnkOuter->AddRef(  ); } 
STDMETHOD_(ULONG, Release)(THIS) 
{ return m_pUnkOuter->Release( ); } 
/* IEventMonitor methods */ 
STDMETHOD(Notify)( 
     THIS_ BSTR bstrContext, 
     IDispatch *pIDispGWEvent); 
EventMonitor(IUnknown *pUnk); 
~EventMonitor( ); 
private: 
IUnknown *m_pUnkOuter;\ 
IUnknown *m_pIUnkDispFact; 
};

In the EventMonitor::Notify( ), bstrPersistentID is checked to see which event is calling and then it performs the desired function. See Setupcmd.cpp.