Windows


Encapsulating DDE

Giovanni Bavestrelli

The less you have to fool with the details of Dynamic Data Exchange, the more likely you are to get it right.


Dynamic Data Exchange (DDE) is an interprocess communication mechanism that has been part of Windows operating systems for a long time. At first, DDE was simply based on the messaging system built into Windows, then came DDEML (DDE Management Library) to provide a higher-level function call layer on top of the messaging system. Network DDE (NetDDE) is the network support layer that enables remote DDE connections to DDE servers on different machines across a network.

DDE has been used extensively in many Windows applications, and even though it is not the most modern interprocess communication protocol, it is still used in many fields, such as automation, and by Windows operating systems themselves. The mechanism is simple and efficient in concept, but the complexity remains in the details, in the somewhat unfriendly API, and in the memory-management burden. All in all, DDE has always been considered hard to program in C/C++. Unfortunately, MFC does not provide any wrapper classes to ease the job of DDE programmers.

This article presents a small object-oriented framework that encapsulates the DDEML API with its details and its complexities and provides a neater object-oriented interface. The framework implements default DDE functionality and allows users to extend it by redefining appropriate virtual functions, in much the same way as MFC does with the Windows API.

If you are not familiar with DDE terms and concepts, see the sidebar, "A Brief Summary of DDE Concepts." You can also refer to [1], [2], or to your compiler's documentation.

Why DDE Seems Difficult

DDEML's C API consists of 27 functions, 16 transaction types, over ten structures, and just too many #define macro constants to remember. To specify or compare Services, Topics, and Items, you cannot use strings, you must create DDE String Handles and remember to free them. To pass and receive data to and from DDE, you must work with DDE Data Handles. The rules for creating, accessing, unaccessing, and freeing these handles are not trivial and they are easy to get wrong. To start the five types of client transactions, be they synchronous or asynchronous, including hot and cold advise links, you must use a single API function, DdeClientTransaction. All messages (transactions) that you receive from DDEML are sent to a unique callback function (DDECallback) that you must define and register in your application, and that must handle all client and server conversations in your application. The meanings of the parameters to DdeClientTransaction and DDECallback often change with each transaction type, and you must cast to compensate. You can look at the online documentation for DdeClientTransaction and DDECallback to get a feeling for the complexity of such an interface.

All this convinced me that if I ever had to tackle these problems, I would only want to do it once, and then forget about them. And that is what I did by writing this framework.

Aims for a DDE Framework

When I decided to encapsulate the DDEML API inside an object-oriented interface, my main aims were:

An Object-Oriented Approach

The way I encapsulated the DDECallback within a DDEClient class is very similar to the way a window procedure is encapsulated into a CWnd class in MFC. In MFC, there are static maps that implement a correspondence between Window Handles (HWND) and CWnd pointers; I use the same method to map Conversation Handles (HCONV) to DDEClient pointers. For the DDEServer class, the mapping is even simpler since Microsoft recommends using only one DDE Service per application. So you should have only one DDEServer object, just like you can have only one CWinApp object in an MFC application.

Just like in MFC, where window messages received by the window procedure inside the framework are routed through virtual functions of the corresponding CWnd objects, DDE transactions received by the DDECallback inside my framework are routed to the corresponding DDEClient object and/or to the only DDEServer object. The DDEClient and DDEServer classes will respond to such transactions by implementing default behavior and by calling virtual functions that can be redefined to implement more specific functionality. Similarly, just as a CWnd class provides wrapper functions for most Windows API functions that take a window handle as a first parameter, DDEClient and DDEServer provide wrappers for many DDEML API functions that take a DDE handle or a DDE conversation handle as parameters. The only place the parallelism between MFC and my DDE framework breaks down is in the use of message maps. I don't need to use message maps; since the transaction types are relatively few compared to window messages, I can just define virtual functions without ending up with huge vtables. Figures 1 and 2 show the main interface of the DDEServer and DDEClient classes, respectively.

There are a few other classes central to the implementation of the framework. The first is class DDEString, which encapsulates character strings and DDE string handles (HSZ). Then there is class DDEDataIn, which represents data received from other applications, and class DDEDataOut, which is data going to other applications. These two latter classes encapsulate DDE Data Handles (HDDEDATA) and take care of memory management, data creation, and access. With the aid of these classes, you will never have to create or remember to free any DDE string handle or data handle.

The DDEServer Class

To use the DDEServer class (Figure 1), you must derive a class from it, using single or multiple inheritance, and redefine at least the AcceptConversation function to return TRUE to accept the conversations on the topics you want to support. (See the sidebar on DDE concepts for information about conversations.) Then you must redefine the virtual member functions relative to the transaction types you want to handle.

For instance, if you want to support the Request transaction on any item, you must redefine the HandleRequest virtual function:

virtual BOOL
HandleRequest(
    const DDEString & Topic,
    const DDEString & Item,
    const DDEDataOut & Data,
    HCONV hConv);

The framework will pass this function the requested topic and item, and a DDEDataOut object that you can query for the data format. Your HandleRequest function must fill this object with the actual data. The last parameter to this function is the conversation handle, which you probably don't care about if you use the framework. Returning TRUE will ensure that the data is sent to the client.

To start your server, you must call OpenServer, and to close it you must call CloseServer (the destructor will also close the server). DDEServer declares a few other useful functions, like PostAdvise, to notify the clients that a data item has changed; it will cause the server's PrepareAdviseData virtual function to be called once for each client that started an advise link on that data item. Most of these functions have names that are self-explanatory if you are familiar with DDE terminology.

Note that you can have only one DDEServer object in your application, but it can handle any number of conversations on one or more topics with any number of clients.

The DDEClient Class

Whereas a DDEServer class is useless unless you inherit from it and extend its behavior, a DDEClient object can be useful as it is. More effort is required if you want to use asynchronous transactions or advise links, or you want to be notified when you get disconnected or when a server registers a service in the system. In these cases, you must inherit from DDEClient and override the relevant virtual functions.

Let's see how to start a client transaction, for example, a request transaction. You start it with the Request function, whose prototype is:

BOOL  Request(const DDEString & Item,
              DDEDataIn * Data,
              UINT format=CF_TEXT,
              DWORD AsyncUserID=SYNC_TRAN,
              LPDWORD pdwResult=NULL);

The arguments to this function are explained as follows. In the first argument, you must pass the name of the data item you want. As always in the DDE framework, you can pass a CString, a LPCSTR, an HSZ, or a DDEString, as the DDEString class provides the necessary conversion operators. If the transaction is synchronous, you must pass in a pointer to a DDEDataIn object that the function will fill with the received data. You usually create such a DDEDataIn object on the stack before calling Request, so that it will still be alive when the Request function will return, but will be deleted automatically when it goes out of scope. The next two parameters are a data format and, very important, a transaction identifier. The transaction identifier can either be SYNC_TRAN, which is the default and indicates a synchronous transaction, or any other value to indicate an asynchronous transaction ID. In this latter case, when the transaction completes asynchronously, the virtual function TransactionCompleted will be called:

virtual void TransactionCompleted(const DDEString & Item,
                                  const DDEDataIn * const Data,
                                  UINT Format,DWORD UserID,
                                  BOOL Ok);

When my framework calls this function, it passes in a reference to the DDE item and a pointer to the DDEDataIn object, which you can query for the data. Not all transaction types send back data, so the pointer may be NULL. The remaining arguments are the data format, the transaction ID that you specified when you started the transaction, and a boolean indicating if the transaction succeeded or not. Thus, you have all the information you need. Note that the last parameter passed to the Request function, pdwResult, can be used to get back the result of the transaction as retrieved by the last parameter of the DDE API's DdeClientTransaction function.

The other four functions that start client transactions (Poke, Execute, StartAdvise, and StopAdvise) work in a similar way, each with its own parameters. They all return TRUE if a synchronous transaction completes successfully or if an asynchronous transaction started successfully, and FALSE otherwise. These member functions have several main advantages over the DDE API functions. Each transaction type gets its own function and default arguments; each member function takes care of its own details; each function updates counters that count how many synchronous and asynchronous transactions are waiting to complete (often useful to know); and each function handles errors.

Advise links are different from other types of client transactions. If you start an advise link with a call to StartAdvise, the DDEClient's virtual ReceiveAdvise function will be called when the data changes:

virtual BOOL
ReceiveAdvise(
    const DDEString & Item,
    const DDEDataIn * const data,
    UINT format);

passing you the item and, in case of a hot link, a DDEDataIn object that you can query for the data. In case of a warm link, the DDEDataIn object pointer passed will be NULL, and it will be your responsibility to request the data if you need it. Call StopAdvise to stop the advise link.

Note that you can have any number of DDEClient objects in your application, but each will be able to open only one DDE conversation at a time. In each conversation, the client can make any number of Request, Poke, or Execute transactions and start any number of advise links.

Sample Applications

To show the architecture at work, I implemented two small sample applications, a DDE Client and a DDE Server. Both are simple, around 100 lines of code each, but they implement complex DDE functionality, including advise links and asynchronous transactions. Figures 3 and 4 show the complete applications.

The easiest way to use the framework is with multiple inheritance. In the sample applications, I inherited the CMainWindow class from MFC's CFrameWnd and from my DDEClient or DDEServer, so that all functionality is defined in CMainWindow.

The Server application (Figure 3) opens a DDE service named "DDEServerTest" when the left mouse button is pressed in its client area and closes the service when the right mouse button is pressed. The server application accepts conversations on topic "Mouse" by returning TRUE in the redefined AcceptConversation function. Once the conversation is established, a data item named "Position" can be exchanged within a CPoint structure, representing the position of a circle in the client area of the server application. The server responds to Request transactions in HandleRequest by putting the Position structure (a CPoint) into the DDEDataOut object and returning TRUE. Similarly, it responds to a Poke transaction in HandlePoke by putting its circle in the position received from the client, which is found in the DDEDataIn object. The server also accepts advise links on item "Position" by returning TRUE in AcceptAdvise and will notify clients when the position of the circle changes by calling PostAdvise. This position changes in two circumstances: when the mouse moves in the server's client area, and when a Poke transaction is received from a client. To show additional functionality, the server counts and displays the number of clients connected by redefining the BeginConversation and EndConversation notification functions, and responds to a DDE execute Maximize command by maximizing its window.

The Client application (Figure 4) tries to connect to the server when the left mouse button is pressed in its client area, and disconnects from the server with a right mouse button click. When the connection succeeds, the Client application first requests the current "Position" of the circle in the server's client area, and then starts a hot advise link on the item "Position" to be notified through the ReceiveAdvise function when such a position changes. Whenever the client receives the position of the circle from the server application (either with the first Request or when ReceiveAdvise is called later), it places a square in the same position in its own client area. When the mouse is moved over the Client window, the Client application Pokes such a position to the server, which will respond by placing its circle in that position and by notifying all its clients that the position changed by calling PostAdvise. Each client will therefore be notified that the position changed and will place its square in that position. To show the execute transaction, a double click on the Client window will send the server a command to maximize its window. When the client gets disconnected, the client's Disconnected virtual function is called, and the client just invalidates its client area to display its new status.

Note that all client transactions are asynchronous (a transaction is made asynchronous by passing an AsyncUserID different from SYNC_TRAN to the function that starts the transaction). This is because asynchronous transactions are more efficient and seem to work better. If, for example, the Poke transaction were synchronous, it would probably cause a reentrancy error by being called too often as the mouse moves, before the previous transactions had completed. In TransactionCompleted, the client is notified when each asynchronous transaction is completed. The client cares only about the result of a Request transaction (which was started with ID 111), because this result brings back the position of the circle from the server; the client gets that position and redraws the window. Just to show you how a synchronous transaction works, I implemented ShowYouSynchronousRequestTransaction to make a synchronous request transaction.

To see the samples at work, I suggest you start one server and two or more clients, tile them across the screen, and start clicking the left and right mouse buttons over them. Also move the mouse across them, and try double clicking the left mouse button on the clients.

Note that I had to define the following function in both client and server or the programs would not link:

void ReportDDEErrorToApp(UINT Code, LPCSTR message);

This is part of a rather primitive error-handling technique. All DDE errors will cause a code and description of the error to be processed by the ReportDDEErrorToApp function, where you can log it or show it or whatever. This part would be easy to improve, but it serves my purposes.

Note also that function DDEInitialize is called in the application object's InitInstance, and DDEUninitialize is called in ExitInstance. These are necessary as they take care of the framework's initialization and cleanup.

Conclusion

The presented framework has been used and tested in many applications in the last few years. It does not cover all aspects of DDEML, but it certainly covers all the most important ones, and can be extended further. The approach to the problem has very definite advantages, as is so often the case with object-oriented technology. Most significantly, the DDE functionality is already tested and debugged. Many common DDE errors are made impossible by this framework and some misuses of the framework can be caught by debugging checks. (For more information on DDE pitfalls, see the sidebar, "DDE Hints and Guidelines.") The framework makes the development of complex DDE servers and clients simple and efficient.

Acknowledgement

Thanks to my friend Jurgen Leschner for reviewing the article.

References

[1] Charles Petzold. Programming Windows 95 (Microsoft Press, 1996), Chapter 17.

[2] Microsoft Windows 3.1: Programmer's Reference, Volume 1, Chapter 5.

[3] Microsoft Developer Network CD-ROM (MSDN).

Giovanni Bavestrelli lives in Milan and is a software engineer for Techint S.p.A., Castellanza, Italy. He has a degree in Electronic Engineering from the Politecnico di Milano, and writes automation software for Pomini Roll Grinding machines. He has been working in C++ under Windows since 1992, specializing in the design and development of reusable object-oriented libraries. He can be reached at automation@pomini.it.