Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

DCOM D-Mystified: A DCOM Tutorial, Step 7

0.00/5 (No votes)
21 Dec 2003 6  
At last! We finish our tutorial by writing a client with MFC, AppWizard, and ClassWizard, like back in the good ol' days (sighhh...)

Image of the HelloCli sample program
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 // _MSC_VER > 1000

#define VC_EXTRALEAN        // Exclude rarely-used stuff from Windows headers

#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 // _AFX_NO_AFXCMN_SUPPORT

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before
//  the previous line.

#endif // !defined(AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_)
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 // _MSC_VER > 1000

#define VC_EXTRALEAN        // Exclude rarely-used stuff from Windows headers

#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 // _AFX_NO_AFXCMN_SUPPORT

#include < atlbase.h >    // Support for CComPtr< >
#include < afxctl.h >     // MFC support for Connection Points

#import "HelloServ.tlb" no_namespace named_guids raw_interfaces_only

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before
// the previous line.

#endif // !defined(AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_)
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:

Adding a new class with ClassWizard.
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:

Specifying the settings for our new CHelloWorldEvents class in ClassWizard.
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:

ClassWizard should just grow up, and quit its whining, but oh well...
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)
    //{{AFX_MSG_MAP(CHelloWorldEvents)
        // NOTE - the ClassWizard will add and remove mapping macros here.
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

BEGIN_DISPATCH_MAP(CHelloWorldEvents, CCmdTarget)
    //{{AFX_DISPATCH_MAP(CHelloWorldEvents)
        // NOTE - the ClassWizard will add and remove mapping macros here.
    //}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()

// Note: we add support for IID_IHelloWorldEvents to support typesafe binding
//  from VBA.  This IID must match the GUID that is attached to the 
//  dispinterface in the .ODL file.

// {B0652FB5-6E0F-11D4-A35B-00104B732442}
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:

Selecting the CHelloWorldEvents class on the Automation tab.
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 (!).

Setting up a handler for the OnSayHello event.
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:

ClassWizard showing the addition of our new event handler.
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);
    }

    // Show a message box saying 'Hello, world, from host ' + lpszHost:
    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:
// Implementation
protected:
    HICON        m_hIcon;

    DWORD        m_dwCookie;    // Cookie to keep track of connection point
    BOOL         m_bSinkAdvised;    // Were we able to advise the server?

    IHelloWorldPtr* m_pHelloWorld;  // Pointer to the IHelloWorld interface pointer
    IUnknown*       m_pHelloWorldEventsUnk; // Pointer to the IUnknown of the
                                            // event "sink"

    CHelloWorldEvents    m_events;   // Our event-handler object
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 /*=NULL*/)
    : CDialog(CHelloCliDlg::IDD, pParent)
{
    ...

    m_dwCookie = 0;
    m_pHelloWorldEventsUnk = m_events.GetIDispatch(FALSE);   
                             // So we don't have to call Release()
    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;

    // Advise the connection point
    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;

    // Get the IHelloWorld IUnknown pointer using a smart pointer.
    // The smart pointer calls QueryInterface() for us.
    IUnknown* pUnk = NULL;    
    
    CComPtr< IUnknown > spUnk = (*m_pHelloWorld);
    pUnk = spUnk.p;

    if (spUnk.p)
    {
        // Unadvise the connection with the event source
        return AfxConnectionUnadvise(pUnk, DIID_DHelloWorldEvents,
            m_pHelloWorldEventsUnk, TRUE, m_dwCookie);
    }

    // If we made it here, QueryInterface() didn't work and we can't 
    // unadvise the server
    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));

    // Set up the NULL security information
    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);

    // Now we have a live pointer to the IHelloWorld interface
    // on the remote host
   
    ...

    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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here