Programming 版 (精华区)
发信人: lofe ()感激生活(), 信区: Programming
标 题: COM Techniques by Panther Software(4)
发信站: 哈工大紫丁香 (Sun Sep 3 08:09:38 2000), 转信
COM Techniques by Panther Software
----------------------------------------------------------------------------
----
Steve Robinson
Panther Software
Part 3: The Client Application
Now that we have completed the DCOM server and have created three dynamically
interchangeable plug-ins, it is time to tie them all together in a client ap
plication. As noted in the beginning of Part 2, the client application
will provide a list of currencies and request dollar equivalents from the
DCOM server. When the DCOM server processes the request, it returns the
dollar conversion value to the client through the client's ICallBackObj
interface.
To accomplish this, the client application is the responsible for creating
the ICallBackObj interface and handing it to the DCOM server. In Part 1, we
included CallBackObj.idl in the DCOM server's IDL file, so that the DCOM
server would have intimate knowledge of the interface and so that the
ICallBackObj interface could be passed as a signature type to the DCOM
server. We could have generated the ICallBackObj methods with the MIDL
compiler separately from the DCOM server application and passed an IUnknown*
to the DCOM server. The DCOM server would then have to QueryInterface for
IID_ICallBackObj. This methodology would work effectively, and is very
similar to using Connection Points. However, passing an IUnknown* interface
and querying for IID_ICallBackObj results in a run-time error if the passed
interface does not support ICallBackObj. The methodology used in our sample
results in a compile-time error if any interface other than an ICallBackObj
is passed in IDCOMServerObj::RegisterCallBack. A general rule of thumb is to
use Connection Points in situations where the client may be implemented in
script; use a proprietary interface when you desire a proprietary call back.
Before we proceed and implement the ICallBackObj in the client and process
the results returned from the DCOM server, let's review a bit more of the
required functionality in the client. Once the client receives the dollar
conversion value from the server, we also want to show the originally
selected currency type converted into a third currency (currency traders
call this cross trading). Accordingly, we need to be able to select the
source currency as well as the target currency and load the plug-in data
converter for the target currency. When we are done, our client application
should look like the application below:
Figure 12. The currency conversion application
We need to generate a dialog-based application using the MFC Application
Wizard. In the samples that come with the article, we created a dialog-based
application and assigned "ClientApp" as the application name. The other
wizard defaults are fine.
Once the application is generated, we need to obtain a connection to the
DCOM Server. Setting up a client application for connecting to an ATL-based
COM server requires the following steps:
Make available the CLSID of the COM server and the interface IDs that we
desire to utilize
Provide a way for the client application to find the method declarations of
the interface methods
Include the ATL related files and initialize ATL related variables
Steps 1 and 2 involve including a series of files in the projects and making
sure the project can locate the files that are included. This is accomplished
by setting "Additional include directories" under menu item Project Settings
| C++ | Category Preprocessor, as shown in the screen shot below. In addition
to including the relative path to the DCOM server, we also include the
relative path to the BaseClassDataProcessor folder, since we will eventually
need the method declarations for IBaseClassDataProcessorObj.
Figure 13. Adding the relative path
Making the CLSID and IIDs available is now pretty easy. At the top of Client
AppDlg.cpp, we simply add the lines:
//midl generated code with interface prototypes
#include <DCOMServer.h>
//CLSID and IID
#include <DCOMServer_i.c>
That provides us with the interface method prototypes and the CLSIDs and
IIDs of the DCOM server.
In order to add the ATL-related declarations and variables in stdafx.h, we
add the following lines that declare all our ATL-related variables and
function declarations.
//Add the includes for ATL and atlimpl.cpp since there is
//there is no ATL lib file.
#include <atlbase.h>
extern CComModule _Module;
#include <atlcom.h>
#include <atlimpl.cpp>
The last step is to initialize the CComModule variable that is declared in
stdafx.h above. Inside the application module, CClientApp.cpp, we create
stdafx.h's externally declared _Module variable and initialize it. Also,
even though the wizard-provided defaults enabled the application as an Activ
eX Control Container, they did not provide us with the one line required
to initialize COM. This very important line is AfxOleInit().
//----------------------------------------------------------------
CComModule _Module;
/////////////////////////////////////////////////////////////////
// CClientAppApp initialization
BOOL CClientAppApp::InitInstance()
{
AfxOleInit(); //initialize OLE
AfxEnableControlContainer();
//ATL loading
_Module.Init(NULL, NULL);
…
We are now actually ready to implement the ICallBackObj in the client. Our
C++ class representation of ICallBackObj will be CCallBackObj. Because
ICallBackObj derives from IDispatch, the CCallBackObj class needs to inherit
not only from CComObjectRoot (which handles reference counting) but also
from IdispatchImpl, which provides a default implementation for the IDispatc
h
portion of the interface on this COM object.
In addition to providing the implementation of ICallBackObj::ReceiveDataFrom
Server, we need to provide one more method,
SetOwnerWindow. When we receive data from the DCOM Server, the data is being
sent on a thread that belongs to the DCOM server. Since we are using MFC,
data needs to be processed on the main thread that hosts the MFC GUI objects.
In order to move data from the DCOM server's worker thread back to our
application's main thread, we have the ICallBackObj::ReceiveDataFromServer
method post a message to the application's main thread with the data as the
message's LPARAM. The complete source for CallBackObj.h is listed below; pay
special attention to the ReceiveDataFromServer method:
#ifndef __CALLBACKOBJ_H_
#define __CALLBACKOBJ_H_
#include "resource.h" // main symbols
#include <DCOMServer.h> //midl generated code
class CCallBackObj :
public CComObjectRoot,
public IDispatchImpl<ICallBackObj, &IID_ICallBackObj,
&LIBID_DCOMSERVERLib>
{
public:
CCallBackObj()
{
m_pOwnerWnd = NULL;
m_lLastDataReceivedFromServer = -1;
}
void SetOwnerWindow(CWnd* pOwnerWnd)
{
m_pOwnerWnd = pOwnerWnd;
}
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CCallBackObj)
COM_INTERFACE_ENTRY(ICallBackObj)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
protected:
CWnd* m_pOwnerWnd;
long m_lLastDataReceivedFromServer;
public:
STDMETHOD(ReceiveDataFromServer)(/*[in]*/ long lData)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState())
if(m_pOwnerWnd != NULL &&
m_pOwnerWnd->GetSafeHwnd())
{
//post a message since we are on a different thread
//which will put us back on the main thread
m_lLastDataReceivedFromServer = lData;
m_pOwnerWnd->PostMessage( WM_RECEIVE_DATA_FROM_PLUG_IN,
0, m_lLastDataReceivedFromServer);
}
return S_OK;
}
};
#endif //__CALLBACKOBJ_H_
Our next step is to connect to the server, create the ICallBackObj interface
,
set the owner window, and pass the ICallBackObj to the server. The correct
place to do this is in OnInitDialog, since we are using class member
variables to hold the COM objects. The source code from OnInitDialog that
does this is shown below:
IUnknown* pUnk = NULL;
HRESULT hRes = ::CoCreateInstance(CLSID_DCOMServerObj, NULL, CLSCTX_ALL,
IID_IUnknown, (void**)&pUnk);
if(SUCCEEDED(hRes))
//get our IDCOMServerObj into our class member
hRes = pUnk->QueryInterface(IID_IDCOMServerObj, (void**)&m_p
IDCOMServerObj);
pUnk->Release();
pUnk = NULL;
ASSERT(SUCCEEDED(hRes));
ASSERT(m_pIDCOMServerObj != NULL);
if(SUCCEEDED(hRes))
{
//create the CCallBackObj and set the owner window
hRes =
CComObject<CCallBackObj>::CreateInstance(
&m_pIServerCallBack);
ASSERT(SUCCEEDED(hRes));
if(SUCCEEDED(hRes))
{
m_pIServerCallBack->SetOwnerWindow(this);
//registher the interface with the DCOM Server
hRes = m_pIDCOMServerObj-RegisterCallBack(m_pIServerCallBack);
ASSERT(SUCCEEDED(hRes));
}
}
}
The next to-do item is to obtain the CLSIDs of available plug-ins that
support IBaseClassDataProcessorObj for currency conversions. This is also
done in OnInitDialog, but rather than instantiating the COM objects at this
time, we will place the currency names in a list box for display and their
CLSIDs as data items in the list box entries. Here is the code that does
this:
/*
Fill the list box with available plug-ins.
Each plug-in has an easy to use name and a CLSID as string. All plug-ins
support
the same interface.
The easy to use name goes in the list box to display the CLSID as
string
goes in the list box's item data.
*/
BOOL bCLSID = FALSE;
CString sKey =
_T("Software\\PantherCurrencyComponents\\AvailablePlugInDataProcessors
\\");
//first open the registry key
HKEY hKey = NULL;
LONG lRes = ::RegOpenKeyEx(HKEY_LOCAL_MACHINE, sKey, 0,
KEY_READ, &hKey);
if (lRes == ERROR_SUCCESS && hKey != NULL)
{
DWORD dwIndex = 0; DWORD size;
TCHAR buff[128]; LONG lRes;
DWORD type; byte buff2[128];
DWORD size2 = 128;
//Read registry entries until the end of key is reached
while(TRUE)
{
buff[0] = '\0';
size = 128;
lRes = ::RegEnumValue(hKey, dwIndex++, buff, &size,
NULL, &type, buff2, &size2);
if(lRes == ERROR_SUCCESS)
{
CString sKeyName = buff;
int iIndex = m_ListBoxWithCLSIDs.AddString(sKeyName);
CString* psClsid = new CString(buff2);
m_ListBoxWithCLSIDs.SetItemData(iIndex, (DWORD)psClsid);
CString sTemp = *psClsid;
sTemp += _T("\n");
TRACE(sTemp);
}
else
{
break;
}
}
// now close the registry key
::RegCloseKey(hKey);
hKey = NULL;
}
if(m_ListBoxWithCLSIDs.GetCount())
m_ListBoxWithCLSIDs.SetCurSel(0);
Now, let's take a look at what occurs when data is received from the DCOM
Server. As we saw earlier, ICallBackObj::ReceiveDataFromServer posts a
WM_RECEIVE_DATA_FROM_PLUG_IN message back to this dialog. The dialog picks
up the message vis-?vis the message-mapped function OnMessageFromPlugIn.
The LPARAM of the message is the actual dollar conversion rate (long value)
received from the DCOM server, so it is displayed as is in a static control.
But we do want to "cross trade" the original currency into another currency
that may not be Dollars. Accordingly, we need to load the plug-in converter
that performs the currency conversion. Since the CLSIDs of the all the
conversion plug-ins are already loaded in a list box, and they all support
the same interface, all we need to do is determine which list box entry is
highlighted, obtain its data item, covert it to a CLSID, cocreate the COM
object, and make a method call. The code that does this is shown below:
/*
1) Get index from list box.
2) Get string from item data.
3) Convert to CLSID
4) CoCreateInstance
5) Call interface method.
*/
iIndex = m_ListBoxWithCLSIDs.GetCurSel();
if(iIndex < 0)
{
::MessageBox(NULL, _T("Please highlight a plug-in"), _T("Message"), MB
_OK);
return -1;
}
CString sPlugInCurrency;
m_ListBoxWithCLSIDs.GetText(iIndex, sPlugInCurrency);
if(sPlugInCurrency.Find(_T("Dollar")) != -1)
sPlugInCurrency = _T("Dollar");
else if(sPlugInCurrency.Find(_T("Mark")) != -1)
sPlugInCurrency = _T("Mark");
else if(sPlugInCurrency.Find(_T("Yen")) != -1)
sPlugInCurrency = _T("Yen");
else
{
ASSERT(false);
}
CString* pstrCLSID = (CString*)m_ListBoxWithCLSIDs.GetItemData(iIndex);
CComBSTR bstr(*pstrCLSID);
CLSID clsid;
HRESULT hRes = ::CLSIDFromString(bstr, &clsid);
if(!SUCCEEDED(hRes))
{
::MessageBox(NULL, _T("Higlighted item does not have a valid CLSID"),
_T("Message"), MB_OK);
return -1;
}
IUnknown* pUnk = NULL;
hRes = CoCreateInstance(clsid, NULL, CLSCTX_ALL,
IID_IUnknown, (void**)&pUnk);
if(SUCCEEDED(hRes))
{
IBaseClassDataProcessorObj* pIDataProcessor = NULL;
hRes = pUnk->QueryInterface(IID_IBaseClassDataProcessorObj, (void**)
&pIDataProcessor);
pUnk->Release();
if(SUCCEEDED(hRes))
{
float fltValueFromDataProcessor;
hRes = pIDataProcessor->ProcessData(1, &fltValueFromDataProcessor);
ASSERT(SUCCEEDED(hRes));
pIDataProcessor->Release();
//sTargetCurrency contains target currency
CString sMessage = _T("1 ");
sMessage += sTargetCurrency;
sMessage += _T(" gets you ");
sValue.Format(_T("%f "), ( ((float)1)/ fltValueFromDataProcessor /
((float)lValueFromServer)));
sMessage += sValue;
sMessage += sPlugInCurrency;
sMessage += _T("(s)");
pWnd = GetDlgItem(IDC_STATIC_RESULT2);
pWnd->SetWindowText(sMessage);
}
else
{
::MessageBox(NULL, _T("Selected component does not support
IID_IBaseClassDataProcessorObj interface
"),
_T("Message"), MB_OK);
}
}
Other than submitting requests to the server (which is done in
CClientAppDlg::OnSubmit()), that is pretty much everything the client does.
One of the benefits of interface immutability is that it allows for
extensibility through plug-ins that can be created independently of the
applications that use them. Your next step is to port these COM objects to
Windows CE, move to Chicago, and get a seat of the Chicago Board of Trade.
Panther Software is a software design and consulting company that provides
advanced technology solutions to leading software companies and IT divisions
of Fortune 1000 companies. Panther can be reached on the Internet at
http://www.panthersoft.com/.
--
路漫漫兮,其修远。
吾将上下而求索。
※ 修改:.haojs 于 Sep 3 08:07:14 修改本文.[FROM: bbs.hit.edu.cn]
--
※ 转寄:.武汉白云黄鹤站 bbs.whnet.edu.cn.[FROM: bbs.hit.edu.cn]
--
☆ 来源:.哈工大紫丁香 bbs.hit.edu.cn.[FROM: haojs.bbs@bbs.whnet.]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:205.469毫秒