Figure 1. Image of the HelloCli
sample program.
Introduction
Welcome to Step 7 of our DCOM tutorial. This is the last step!
If you want to follow along with this tutorial and add code and use the Visual C++ Wizards as we go along, that's great. In fact, I very very highly recommend that, because otherwise this tutorial is a big waste of electronic ink (?). However, I follow along exactly with the tutorial myself, as I write it, and develop the code and use the Visual C++ wizards just as I say you should. The screenshots, in fact, are from my development of the files for each step! To download this already-developed code to compare with your own, simply click the 'Download the Step n Files - n KB" links at the top of each step. There's also an archive of the files for all the steps at the Questions and Answers page for this tutorial. I still recommend that you follow along with us as we go; this way, you can learn while you code. If you ever have problems along the way with this tutorial, feel free to:
Remember, our steps in developing the software in this tutorial are as follows:
- Step 1: Create the server,
HelloServ
, using the ATL COM AppWizard.
- Step 2: Modify the starter files provided by AppWizard.
- Step 3: Use the New ATL Object Wizard to add a simple COM object, the
HelloWorld
object, to the server.
- Step 4: Modify the
IHelloWorld
interface to include a SayHello()
method.
- Step 5: Add an event method,
OnSayHello()
, to the connection point source interface, DHelloWorldEvents
.
- Step 6: Build the server, and install it on the server computer.
- Step 7: Create a MFC client,
HelloCli
, which calls the server and handles the connection point event sink.
We're currently on Step 7 of this tutorial (the last!), where we put together a little MFC program, called HelloCli
, to test our server. Let's plunge in:
Step 7: Create a MFC HelloCli
Client to Test the Server
As you can see from the screenshot above, I built a dialog-based application using MFC AppWizard (EXE). I added a status window to report status, so I can see just where errors occurred, and I also successfully handled Connection Points. To add text to the status window, which is just an Edit control with the read-only style set, I added a CString
member variable, m_strStatus
to the dialog class with ClassWizard, and then anytime I needed to add a message line to the edit control, it was made easy with this code:
m_strStatus += "This is a status line for the edit control\r\n";
UpdateData(FALSE);
Listing 1. Adding a status line to the edit control.
We add on text to the contents of m_strStatus
with the +=
operator of CString
, and then we call UpdateData(FALSE)
to move the contents of the member variable from the variable to the edit control.
To start my sample project, I brought up the New dialog box, clicked 'MFC AppWizard (EXE)' in the list, and then typed 'HelloCli
' for the name of my project. After completing AppWizard, I opened the STDAFX.H
file and added the line shown in bold in Listing 2:
#if !defined(AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_)
#define AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif
#define VC_EXTRALEAN
#define _WIN32_WINNT 0x0400
#include < afxwin.h > // MFC core and standard components
#include < afxext.h > // MFC extensions
#include < afxdisp.h > // MFC Automation classes
#include < afxdtctl.h > // MFC support for Internet Explorer 4 Common Controls
#ifndef _AFX_NO_AFXCMN_SUPPORT
#include < afxcmn.h > // MFC support for Windows Common Controls
#endif
#endif
Listing 2. Adding the
#define _WIN32_WINNT
line to
STDAFX.H
so that DCOM works.
The next thing to do is to add something to let the client know about the interfaces and everything else that the server supports. There are two ways we can do this:
- Using
#import
to bring in the type library of the server. This file, HelloServ.tlb
, is produced by MIDL
when it compiles the HelloServ.idl
file.
- Using
#include "HelloServ.h"
to just include the C++ and C declarations of interfaces. This is nice, but then you have to also define, in your code, all the GUIDs that the server responds to. These are CLSID_HelloWorld
, IID_IHelloWorld
, DIID_DHelloWorldEvents
, and LIBID_HELLOSERVLib
. If you use #import
, this is done for you.
I like the idea of using #import
, because of not only what was explained above, but also because you get to use smart pointers with #import
, too. Be careful, though; we have a custom (that is, IUnknown
-derived) interface that our server uses. We can use #import
just fine in this example since the IHelloWorld::SayHello()
method takes no parameters. If the IHelloWorld::SayHello()
method took parameters, and they weren't of OLE-Automation-compatible types, then we would have to skip using #import
, because it will only recognize those types. However, if you mark your custom interface with the [oleautomation]
attribute and use OLE Automation-compatible types in your methods, this will work.
With custom interfaces, it's generally a better idea to use the second method above. However, like I said earlier, we'll go ahead and use #import
this time because our method doesn't take any parameters. So this means that we need to copy the HelloServ.tlb
file to our HelloCli
project folder from the HelloServ
project folder, and then add an #import
line somewhere. How about in good ol' STDAFX.H
again? We'll also add #include
lines for atlbase.h
and afxctl.h
, since these files give us support for things we'll use later on. Doing all this in STDAFX.H
will help us when we build our program to keep the build time down, too:
#if !defined(AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_)
#define AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif
#define VC_EXTRALEAN
#define _WIN32_WINNT 0x0400
#include < afxwin.h > // MFC core and standard components
#include < afxext.h > // MFC extensions
#include < afxdisp.h > // MFC Automation classes
#include < afxdtctl.h > // MFC support for Internet Explorer 4 Common
#ifndef _AFX_NO_AFXCMN_SUPPORT
#include < afxcmn.h > // MFC support for Windows Common Controls
#endif
#include < atlbase.h > // Support for CComPtr< >
#include < afxctl.h > // MFC support for Connection Points
#import "HelloServ.tlb" no_namespace named_guids raw_interfaces_only
#endif
Listing 3. Adding other needed code to
STDAFX.H
.
Bring in the Connection Point
NOTE: This only works if the event source interface is a dispinterface
, like our DHelloWorldEvents
interface.
The next thing to do is to use ClassWizard to give us a class which will implement our connection point for us (!). Yes, we've finally arrived!! To do this, open up ClassWizard, click the Message Maps tab, click Add Class, and then click New, as shown below in Figure 2:
Figure 2. Adding a new class with ClassWizard.
The next thing to do is to specify the new class we want to add to our project. Since this class is (kind of) implementing an interface, that is, the DHelloWorldEvents
dispinterface, we'll call this class the CHelloWorldEvents
class. Next, we specify that we want to derive this class from the MFC CCmdTarget
class, which helps us with all the COM implementation. People have often said that "MFC doesn't really have any COM support besides that needed for OLE and UI stuff. And then, it only does dispinterfaces." None of the preceeding sentence is entirely correct. MFC is great at helping us out with UI stuff, but I have seen example code (that works) where MFC is used to implement any interface you like, even in COM servers with non-dispinterface and non-IDispatch
interfaces! The CCmdTarget
class is the key.
Anyway, enough of my blathering. The last thing to do before we can click OK in the New Class dialog box is to click the Automation option button. This turns on the support in CCmdTarget
that we need to use; don't worry, choosing this won't even add so much as an .ODL file to your project, and you needn't have checked 'Automation' in AppWizard to use this. When everything in the New Class dialog box is as it should be, it should look like Figure 3, below:
Figure 3. Specifying the settings for our new CHelloWorldEvents
class in ClassWizard.
ClassWizard will add the CHelloWorldEvents
class to your project, but it will whine because you didn't specify Automation support in AppWizard. Since you didn't, your project doesn't have a HelloCli.odl
file. Too bad for ClassWizard; it shows you the protest message below, but you can click OK and ignore it:
Figure 4. ClassWizard should just grow up, and quit its whining; but, oh well... Ignore this warning and click OK.
Make Changes to CHelloWorldEvents
ClassWizard, helpful as it is, did make one booboo that we'll want to erase. Open the HelloWorldEvents.cpp
file and remove the line shown below in bold:
BEGIN_MESSAGE_MAP(CHelloWorldEvents, CCmdTarget)
END_MESSAGE_MAP()
BEGIN_DISPATCH_MAP(CHelloWorldEvents, CCmdTarget)
END_DISPATCH_MAP()
static const IID IID_IHelloWorldEvents =
{ 0xb0652fb5, 0x6e0f, 0x11d4, { 0xa3, 0x5b, 0x0, 0x10, 0x4b, 0x73, 0x24, 0x42 } };
Listing 4. Delete the lines of code that are shown in
bold.
Next, find the code shown in bold in Listing 5, below. We're going to replace it with the DIID (DispInterfaceID) of the DHelloWorldEvents
interface:
BEGIN_INTERFACE_MAP(CHelloWorldEvents, CCmdTarget)
INTERFACE_PART(CHelloWorldEvents, IID_IHelloWorldEvents, Dispatch)
END_INTERFACE_MAP()
Listing 5. The code to look for, shown in
bold.
Replace IID_IHelloWorldEvents
with DIID_DHelloWorldEvents
, as shown in Listing 6:
BEGIN_INTERFACE_MAP(CHelloWorldEvents, CCmdTarget)
INTERFACE_PART(CHelloWorldEvents, DIID_DHelloWorldEvents, Dispatch)
END_INTERFACE_MAP()
Listing 6. Putting
DIID_DHelloWorldEvents
in place of the Class-Wizard-added
IID_IHelloWorldEvents
identifier.
Now, we're going to use ClassWizard to add the handler function which gets called when the server fires the OnSayHello
event. Bring up ClassWizard, and select the Automation tab. Make sure that CHelloWorldEvents
is selected in the Class Name box, as shown in Figure 5 below:
Figure 5. Selecting the CHelloWorldEvents
class on the Automation tab of ClassWizard.
Click Add Method. The Add Method dialog box appears, as shown in Figure 6 below. Here we're going to specify the "external name" for our event handler method, as well as other information. The "external name" should ALWAYS match the name of the event method that we used when we added it to the server! The Internal Name box should hold the name of the member function that will get called when the event comes in; this can be whatever you please. We're going to use what ClassWizard suggests; a name that matches the OnSayHello
"external name." ALWAYS specify void
for the return type of an event handler, because the server always uses HRESULT
as its return types. For clients, anytime we're handling connection point events, the return type should always be void
. Next, specify a parameter, LPCTSTR lpszHost
as the event handler's single parameter. You notice that BSTR
isn't in the list of event handler types (alright!). This is because you use LPCTSTR
instead; ClassWizard makes sure that MFC will convert between BSTR
and LPCTSTR
for you (!).
Figure 6. Setting up a handler for the OnSayHello
event.
Click OK. ClassWizard adds code to CHelloWorldEvents
to make the magic happen (with CCmdTarget's
help), and then shows a new entry in its External Names listbox to show that the event handler has been added:
Figure 7. ClassWizard showing the addition of our new event handler.
Save your changes! Make sure and click OK in ClassWizard, otherwise it will roll-back all of its changes that it made. If you click Cancel, you'll have to add the event handler again. Just a word of warning. Now it's time to implement our event handler. We'll grab the name of the server computer from lpszHost
, and then we'll show the user a message box saying that the server said Hello. We might also want to add text to the Status window of our dialog saying that the event handler function got called. Here's how I did that:
#include "HelloCliDlg.h"
void CHelloWorldEvents::OnSayHello(LPCTSTR lpszHost)
{
CHelloCliDlg* pDlg = (CHelloCliDlg*)AfxGetMainWnd();
if (pDlg != NULL)
{
pDlg->m_strStatus
+= "The OnSayHello() connection point method has been called\r\n";
pDlg->UpdateData(FALSE);
}
CString strMessage = "Hello, world, from ";
strMessage += lpszHost;
AfxMessageBox(strMessage, MB_ICONINFORMATION);
}
Listing 7. Implementing the
CHelloWorldEvents::OnSayHello()
event handler function.
I also had to add a friend
statement to the declaration of CHelloWorldEvents
, because CWnd::UpdateData()
is a protected
function:
class CHelloWorldEvents : public CCmdTarget
{
friend class CHelloCliDlg;
...
};
The next thing to do is to add some data members to the CHelloCliDlg
class in order to hold the pointers and objects that we'll be using in working with the server. There are quite a few of them, and you'll have to make sure to add the line
#include "HelloWorldEvents.h"
to the top of the
HelloCliDlg.h
file:
protected:
HICON m_hIcon;
DWORD m_dwCookie; BOOL m_bSinkAdvised;
IHelloWorldPtr* m_pHelloWorld; IUnknown* m_pHelloWorldEventsUnk;
CHelloWorldEvents m_events;
Listing 8. Data members we need to add to the
CHelloCliDlg
class.
Next, we need to add code to the dialog's constructor:
CHelloCliDlg::CHelloCliDlg(CWnd* pParent )
: CDialog(CHelloCliDlg::IDD, pParent)
{
...
m_dwCookie = 0;
m_pHelloWorldEventsUnk = m_events.GetIDispatch(FALSE);
m_bSinkAdvised = FALSE;
}
Listing 9. Code to add to the
CHelloCliDlg::CHelloCliDlg()
constructor function.
To advise the server, I added a AdviseEventSink()
, protected
member function to CHelloCliDlg
using ClassView. This function is implemented the basically the same for anytime you want to advise a server about your MFC connection point:
BOOL CHelloCliDlg::AdviseEventSink()
{
if (m_bSinkAdvised)
return TRUE;
IUnknown* pUnk = NULL;
CComPtr< IUnknown > spUnk = (*m_pHelloWorld);
pUnk = spUnk.p;
BOOL bResult = AfxConnectionAdvise(pUnk, DIID_DHelloWorldEvents,
m_pHelloWorldEventsUnk, TRUE, &m_dwCookie);
return bResult;
}
Listing 10. Implementation of advising the server. This demonstrates how to call
AfxConnectionAdvise()
. You must have properly registered the server like we did in Step 6, or else this won't work.
When we're ready to go back to being aloof to the server and its events that it fires, we can call AfxConnectionUnadvise()
:
BOOL CHelloCliDlg::UnadviseEventSink()
{
if (!m_bSinkAdvised)
return TRUE;
IUnknown* pUnk = NULL;
CComPtr< IUnknown > spUnk = (*m_pHelloWorld);
pUnk = spUnk.p;
if (spUnk.p)
{
return AfxConnectionUnadvise(pUnk, DIID_DHelloWorldEvents,
m_pHelloWorldEventsUnk, TRUE, m_dwCookie);
}
return FALSE;
}
Listing 11. Unadvising the event source and sink with
AfxConnectionUnadvise()
.
To actually make the method call, you can see how I implemented all of this and where my AdviseEventSink()
and UnadviseEventSink()
play in in the sample program. Remember, though, to add this code to OnInitDialog()
:
BOOL CHelloCliDlg::OnInitDialog()
{
CDialog::OnInitDialog();
...
CoInitialize(NULL);
CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_NONE,
RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL);
return TRUE;
}
Listing 12. Adding intialization code to
OnInitDialog()
.
The OnStartServer()
function handles a button the user clicks when they want to start the server (how circular). As you can see, I had a server computer on the network named \\Viz-06
which I connected to with DCOM:
void CHelloCliDlg::OnStartServer()
{
COSERVERINFO serverInfo;
ZeroMemory(&serverInfo, sizeof(COSERVERINFO));
COAUTHINFO athn;
ZeroMemory(&athn, sizeof(COAUTHINFO));
athn.dwAuthnLevel = RPC_C_AUTHN_LEVEL_NONE;
athn.dwAuthnSvc = RPC_C_AUTHN_WINNT;
athn.dwAuthzSvc = RPC_C_AUTHZ_NONE;
athn.dwCapabilities = EOAC_NONE;
athn.dwImpersonationLevel = RPC_C_IMP_LEVEL_IMPERSONATE;
athn.pAuthIdentityData = NULL;
athn.pwszServerPrincName = NULL;
serverInfo.pwszName = L"\\\\Viz-06";
serverInfo.pAuthInfo = &athn;
serverInfo.dwReserved1 = 0;
serverInfo.dwReserved2 = 0;
MULTI_QI qi = {&IID_IHelloWorld, NULL, S_OK};
...
try
{
m_pHelloWorld = new IHelloWorldPtr;
}
catch(...)
{
AfxMessageBox(AFX_IDP_FAILED_MEMORY_ALLOC, MB_ICONSTOP);
...
return;
}
HRESULT hResult = CoCreateInstanceEx(CLSID_HelloWorld, NULL,
CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER, &serverInfo, 1, &qi);
if (FAILED(hResult))
{
...
return;
}
m_pHelloWorld->Attach((IHelloWorld*)qi.pItf);
...
return;
}
Listing 13. How to get an interface pointer to the
IHelloWorld
interface on the remote server.
Calling the method is a simple matter of executing this statement:
HRESULT hResult = (*m_pHelloWorld)->SayHello();
Listing 14. Calling the
IHelloWorld::SayHello()
method.
To release the server when we're done with it, simply delete
the m_pHelloWorld
pointer:
delete m_pHelloWorld;
m_pHelloWorld = NULL;
Listing 15.
The End... My Friend...
There! Now we have a living, breathing, DCOM client/server software system. It doesn't do much, but it can do a lot... Anyway, I hope this tutorial has been enlightening, and DCOM demystified. I always encourage you to e-mail me just whenever, and ask me questions. No question is a stupid question, and I will be happy to help you. Click Back below if you want to go back to Step 6, or click Questions and Answers to see if someone else asked a question you need answered.
Until next time... it's been fun.
History
- 22 Dec 2003 - updated download
<< Back
Questions and Answers