Implementing DDE Using C++ Classes

Nigel Thompson
Microsoft Developer Network Technology Group

March 6, 1995

Click to open or copy the files in the DDECPP set of samples for this technical article.

Abstract

This article describes a set of C++ classes that can be used in implementing DDE servers and clients. The server implementation provides full support for the System topic. These classes were developed using Visual C++™ version 2.0 under Microsoft® Windows NT™ version 3.5.

Introduction

Many moons ago, my good friend and alter ego Herman Rodent wrote a series on dynamic data exchange (DDE) culminating in the article "Quick and Easy DDE Server," in which he described how to implement a complete DDE server using only one application programming interface (API) call. Of course, this API call required you to cut and paste some 3,000 lines of code into your application first.

Well, there I was at home this Christmas suffering slightly from present-opening overload with my children and wondering if a bit of computer tinkering might be in order. I had recently purchased an X10 home controller interface and was in the middle of writing some control software for it and had decided that a Net DDE interface would be a good add-on feature. If the controller sat on our server at home (yes, my house is networked), my wife could run a simple Visual Basic® application to turn on the printer upstairs in my workroom, play with the lighting and so on. Sooo. . . since the new application was a 32-bit C++ beast, and I didn't want to do any more work than necessary to implement my DDE server, I decided to try to port Herman's old 16-bit DDE server code into my new Visual C++™ application. Well, I did that and it worked, but I wasn't satisfied with the results, so when I got back to work after the vacation, I decided to implement the same level of DDE support in some custom-built classes. This technical article describes those classes and the test applications that use them. This work actually goes a little beyond support for a server—it also includes a conversation object, which makes using DDE in a client application much simpler, too.

DDE Object Classes

Figure 1 shows the set of classes used to support DDE. All objects are derived from the Microsoft® Foundation Class Library version 3.0.

Figure 1. C++ Classes used to support DDE

OK, I forgive you if you think I've gone a bit overboard here. That's pretty much what I thought when I created the diagram. In fact, there isn't as much here as there might seem. Let's go through the classes and see what they do.

CHSZ

The CHSZ class provides a convenient way to create the HSZ string handles that the DDEML library uses. These objects are constructed using a pointer to a string and contain the HSZ handle value. An HSZ casting operator enables the object to be used anywhere an HSZ handle would be required in a DDEML library function call. We'll see these in use in the code a little later.

CDDECountedObject

When I implemented the CDDEConv class, I found that the class needed to be reference-counted, so I created a simple base class to encapsulate the reference-counting functions, AddRef and Release. Those of you familiar with the Component Object Model on which OLE is based will recognize these functions. This is a trivial class but simplifies the classes derived from it slightly. Again, we'll see it in use later when we look at managing conversations.

CDDEConv and CDDEConvList

CDDEConv encapsulates a DDE conversation. Because conversations can be initiated by the application acting as a client or another application using your application as a server, conversations can be created locally or thrust upon us by another application. Keeping track of when the conversation object can be deleted is simplified by giving the object a reference count. Hence, CDDEConv is derived from CDDECountedObject. The CDDEConvList class provides a list of CDDEConv objects.

CDDEItem and CDDEItemList

DDE revolves around services, conversations, topics, and items. The CDDEItem class encapsulates a single item. The CDDEItemList class provides for a list of CDDEItem objects in a topic.

CDDEStringItem

The CDDEStringItem isn't used in the DDE support classes but is provided as a convenient way of implementing an item that is simply a string of text. The item can be requested (peeked into) or poked. The DDEServ sample uses this class.

CDDESystemItem . . .

The CDDESystemItem, CDDESystemItem_TopicList, CDDESystemItem_ItemList, and CDDESystemItem_FormatList classes are all used to support the System topic in a server.

CDDETopic and CDDETopicList

The CDDETopic class encapsulates a DDE topic. The CDDETopicList class provides for a list of topics.

CDDEServerSystemTopic

This special topic is used to support the System topic in a server.

The classes of most importance when creating a server application are CDDEServer, CDDETopic, and CDDEItem. When you are creating a client application, the most important classes are CDDEServer and CDDEConv.

Creating a DDE Server Application

Let's look at what's required to build a simple DDE server. We'll be using the DDEServ sample as an example. The DDEServ sample doesn't really do very much, but it shows how to build a simple server framework upon which you can easily add your own functionality. Figure 2 shows a screen shot of the server.

Figure 2. A screen shot of the DDEServ sample application

DDEServ was build using the Visual C++ AppWizard. It's a simple single-document-interface (SDI) application with no bells or whistles. The document class has no real function, and the view class exists only to display status messages so that we can see what's going on inside the server as it operates. The area of most interest is the main frame window. This is where the DDE server object resides. The MAINFRM.H header file contains the declaration of the server object:

// Attributes
public:
    CMyServer m_Server;

OK, so you expected to see a CDDEServer object. CMyServer is derived from CDDEServer (in MYSERV.H):

class CMyServer : public CDDEServer
{
public:
    CMyServer();
    virtual ~CMyServer();
    virtual void Status(const char* pszFormat, ...);
    virtual BOOL OnCreate();

public:
    CMyTopic m_DataTopic;
    CMyStringItem m_StringItem1;
    CMyStringItem m_StringItem2;
};

Notice that CMyServer overrides the Status and OnCreate functions defined in CDDEServer. The Status function is really a debugging aid. It is used in the DDEServ sample to print progress and error messages in the main window, so you can see what's going on. The OnCreate member is called when the server object has been created, to allow any derived class to set up its own topics and items.

As you can see, CMyServer has three member variables: a topic and two items. CMyTopic is derived from CDDETopic:

class CMyTopic : public CDDETopic
{
public:
    CMyTopic();
    virtual BOOL Exec(void* pData, DWORD dwSize);
};

The Exec member function is overridden so that DDE execute commands will be handled by CMyTopic objects. (The default action in CDDETopic is to fail them.)

The CMyString objects are derived from CDDEStringItem:

class CMyStringItem : public CDDEStringItem
{
protected:
    virtual void OnPoke();
};

The OnPoke member is overridden so that the object will be notified of any change in the object's data caused by a DDE Poke command.

Let's see how these derived classes are implemented now in MYSERV.CPP. Let's begin by looking at what happens when the server object is created in the CMainFrame::OnCreate function (from MAINFRM.CPP):

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    ... Code ommitted here

    //
    // Start the DDE server.
    //

    if (!m_Server.Create(AfxGetAppName())) {
        char buf[128];
        sprintf(buf,
                "Service failed to start. Error: %d",
                m_Server.GetLastError());
        AfxMessageBox(buf);
    }

   return 0;
}

The server object's Create member is called using the name of the service to be created. As is common practice in DDE applications, the service name is the same as the name of the application. CDDEServer::Create does the grunt work of establishing the new service and registering itself with the DDEML library. It then calls the OnCreate function to allow any derived class a chance to set itself up. Let's see how CMyServer handles this:

BOOL CMyServer::OnCreate()
{
    //
    // Add our own topics and items.
    //

    m_DataTopic.Create("Data");
    AddTopic(&m_DataTopic);

    m_StringItem1.Create("String1");
    m_DataTopic.AddItem(&m_StringItem1);

    m_StringItem2.Create("String2");
    m_DataTopic.AddItem(&m_StringItem2);

    //
    // Set up some data in the strings.
    //

    m_StringItem1.SetData("This is string 1");
    m_StringItem2.SetData("This is string 2");

    return TRUE;
}

The single topic is created and named Data. The topic is then added to the server's topic list. The two string items are created, added to the topic list, and then given initial values. Calling CDDEStringItem::SetData not only changes the data in the item, but also sends an advise message to any client that might be interested in the change of value.

If a client of this service requests data from one of the items, that data is supplied automatically by the service. There is no interaction with the application. If an external application pokes data to one of the items, the item data changes and the item object's OnPoke member is called to notify any derived object that the data has changed. (CDDEStringItem does nothing.)

If an external application attempts to send the Data topic an execute command, the object's Exec member is called. CMyTopic handles this by simply printing a message to the main window:

BOOL CMyTopic::Exec(void* pData, DWORD dwSize)
{
    STATUS("Exec: %s", (char*)pData);
    return TRUE;
}

Obviously, I've skipped over a lot of the details here, but you should be able to see that by simply deriving your own item and topic classes, it's very simple to implement a DDE server. In fact, because the CDDEServer has functions of its own to handle Request, Poke, and Exec commands, you need not implement any topics or items at all. You could override the server's functions and implement all the functionality right there. The only thing that would be missing from this implementation would be support for the DDE System topic. The CDDETopic and CDDEItem objects are normally kept in lists that the server can walk in order to satisfy System topic requests. To understand more about supporting the System topic, see "Supporting the DDE System Topic," by Herman Rodent in the Microsoft Development Library.

Creating a DDE Client Application

I chose to create the sample DDE client application as a dialog-type application, using the Visual C++ AppWizard. I didn't choose this over a more common SDI document/view application because it's better, but because I hadn't tried this before. So this is (as usual) just another way to skin the cat. Figure 3 shows a screen shot of the sample DDECli application.

Figure 3. A screen shot of the DDECli sample application

The DDECli sample application is a very generic DDE client. It allows you to connect to a named service and topic. Once connected, it can send Exec commands to the topic and peek and poke at specific items of the topic. A status window provides information on what's happening as the client talks to the server.

The client application actually shares a fair bit in common with a server, and in fact, it uses a CDDEServer object in the same way the DDEServ sample did. The main difference between the client application and the server is that the client deals mostly with conversations rather than discrete topics and items. So in building a client application, we will make use of the CDDEConv object.

The main window of the dialog contains the server object and a pointer to a current conversation object:

   CMyClient m_DDEClient;
   CMyConv* m_pConversation;

Notice here that we are dealing with classes derived from CDDEServer and CDDEConv. When the dialog is first initialized, the server object is created:

BOOL CDDECliDlg::OnInitDialog()
{
   ... Code omitted 
   
   Status("Initializing service");

    if (!m_DDEClient.Create(AfxGetAppName())) {
        AfxMessageBox("Failed to initialize DDE client");
        return FALSE;
    }
   
   Status("Service running");
    UpdateUI();
   return TRUE;  // Return TRUE unless you set the focus to a control.
}

Notice that the client application is initialized exactly the same way the server application was, and in fact, the client is also a server, although in this case it supports no topics other than the System topic.

Handling a Connection

When DDECli is running, type in the name of a service and a topic (such as DDEServ and Data). Then click the connect button to make the connection. Here's how the connection is handled in the code:

void CDDECliDlg::OnConnectBtn() 
{
    ASSERT(!m_pConversation);

    //
    // Get the current data set.
    //

    UpdateData(TRUE);
    if (m_strService.GetLength() == 0) {
        AfxMessageBox("You must enter a service name");
        return;
    }
    if (m_strTopic.GetLength() == 0) {
        AfxMessageBox("You must enter a topic name");
        return;
    }

    //
    // Try to connect to the specified service and topic.
    //

    m_pConversation = new CMyConv(&m_DDEClient, this);
    ASSERT(m_pConversation);
    m_pConversation->AddRef();
    if (!m_pConversation->ConnectTo(m_strService, m_strTopic)) {

        m_pConversation->Release(); // and it should get deleted
        m_pConversation = NULL;
        return;
    }

    Status("Connected to %s|%s",
           (const char*)m_strService,
           (const char*)m_strTopic);

    //
    // If an item is listed, get the current value and 
    // set up an advise request for changes.
    //
    
    if (m_strItem.GetLength() > 0) { 

        DWORD dwSize = 0;
        void* pData = NULL;
        m_pConversation->Request(m_strItem, &pData, &dwSize);
        if (dwSize) {
            m_strItemData = (char*)pData;
            UpdateData(FALSE);
            delete pData;
        
            //
            // Ask for notice if it changes.
            //

            m_pConversation->Advise(m_strItem);

        } else {

            Status("Failed to get data from %s",
                   (const char*) m_strItem);
        }
    }
    UpdateUI();
}

The name of the service and topic are obtained from the dialog variables, and a new CMyConv object is created. Because conversation objects are reference-counted, the object's AddRef member is called to increase its reference count. The conversation's ConnectTo member is called to make the connection, and if all goes well, a status message is displayed.

Once the connection is made, a check is run to see if the name of an item has been entered. If it has, the conversation object requests data from that item and sets up a DDE Advise request so that if the item data changes at the server, the conversation object will be notified.

Making Execute Requests

To send an execute request to the service topic, enter the command text and click the Exec button. The code does the following:

void CDDECliDlg::OnExecBtn() 
{
   ASSERT(m_pConversation);
    UpdateData(TRUE);
    if (!m_pConversation->Exec(m_strExecCmd)) {
        Status("Exec failed");
    }
}

The execute command is extracted from the dialog, and the conversation's Exec member function is called to send the command to the server.

Poking Data to an Item

Data can be poked to an item very simply by entering the text (only text transfers are supported in this example) in the item data window and clicking the Poke button. Again, the implementation is very simple:

void CDDECliDlg::OnPokeBtn() 
{
   ASSERT(m_pConversation);
    UpdateData(TRUE);
    if (!m_pConversation->Poke(CF_TEXT,
                          m_strItem,
                          (void*)(const char*)m_strItemData,
                          m_strItemData.GetLength() + 1)) {
        Status("Poke failed");
    }
}

Summary

I've very briefly presented here a set of C++ classes that will allow you to implement a DDE server or client. For more details on the DDE mechanism and how it functions, please search the rest of the Development Library. There are several articles on how DDE works, as well as several implementation examples in C. And finally, don't forget that Visual Basic makes a great test tool for DDE servers. Writing a Visual Basic test client can be as simple as writing a dozen lines of code.