Building a DDE server that does not use default processing
To build a DDE server application that does not use default processing:
• Create a DdeServerManager.
• Hook the necessary DdeServerManager callbacks (DdeNcoldlinkCallback, DdeNhotlinkCallback, DdeNinitiateCallback, DdeNrequestCallback, DdeNwarmlinkCallback).
• Write the callback methods for each hooked callback.
• When the application is finished, send the free message to the DdeServerManager.
If you follow this procedure, the DdeServerManager will not handle any requests from DDE clients. Methods must be written and callbacks hooked in order to provide service. This technique of providing DDE server services takes more work but allows for more flexibility.
Suppose, for example, that a user wants an application to make data available through DDE but there is so much data, or the data is so large, that it must be stored on secondary storage. When a DDE client requests data, the server must read the data from secondary storage and return it to the client. To do this the application must hook the DdeNrequestCallback callback and write a method to handle requests for data from a DDE client.
Note:
It is possible to have callbacks handled by a combination of default processing and the DDE server application. If a callback is hooked, then the application takes responsibility. If the callback is not hooked, then default processing occurs.
An example DDE server that does not use default processing
This example builds a DDE server that provides the time only in the String format.
Creating a timer
First, create a class called
DdeTimeServer2 that is a subclass of
Object. It has the two instance variables described in
Table 44.
Table 44. Instance variables for DdeTimeServer2
| |
ddeServerManager | The instance of the DdeServerManager class that you are creating. |
timerOn | A flag indicating whether the timer is on or off. Turning the timer off is important when the server shuts down. |
Thus, our new class looks like this:
Object subclass: #DdeTimeServer2
instanceVariableNames: 'ddeServerManager timerOn '
classVariableNames: ''
poolDictionaries: 'DdeConstants '
Second, create a class method new to create a new server. Also send the initialize method to the new object in order to set it up.
new
"Create a new timer server and initialize it."
^super new initialize
Third, create the initialize method. In the initialize method you first create the DdeServerManager and name it Timer. Next you hook up to the callbacks and turn the timer on.
initialize
"Private - Create and initialize a DdeServer. Hook all callbacks."
ddeServerManager := DdeServerManager name: 'Timer'.
ddeServerManager
addCallback: DdeNcoldlinkCallback
receiver: self
selector: #coldlink:clientData:callData:
clientData: nil.
ddeServerManager
addCallback: DdeNhotlinkCallback
receiver: self
selector: #hotlink:clientData:callData:
clientData: nil.
ddeServerManager
addCallback: DdeNinitiateCallback
receiver: self
selector: #initiate:clientData:callData:
clientData: nil.
ddeServerManager
addCallback: DdeNrequestCallback
receiver: self
selector: #request:clientData:callData:
clientData: nil.
ddeServerManager
addCallback: DdeNwarmlinkCallback
receiver: self
selector: #warmlink:clientData:callData:
clientData: nil.
ddeServerManager
addCallback: DdeNterminationCallback
receiver: self
selector: #termination:clientData:callData:
clientData: nil.
self timerOn: true.
At the end of initialization, turn the timer on by sending the timerOn: message, which in turn sends the timerSet message. These methods are implemented below. The timer uses the Common Widgets timer functionality so that when the timer goes off, the message timerProc: is sent to the application.
timerOn: aBoolean
"Start/stop the timer depending on aBoolean."
timerOn := aBoolean.
self timerSet.
timerOn
"Answer whether the timer is on or off."
^timerOn.
timerSet
"If the timer is on, reset it to go off in one second."
self timerOn ifTrue: [
CwAppContext default
addTimeout: 1000
receiver: self
selector: #timerProc:
clientData: nil
]
Implementing server behavior
Next, create the callback methods that implement the behavior of this server. First you implement the callback for the DdeNinitiateCallback. The initiate:clientData:callData: method checks that the application name and the topic to which the client is attempting to connect match what the server is looking for. If they do match, then the application sends the notifyClientOfSupportFor: message to the DdeServer and sets the return value of the DdeCallbackData object to true. To reject the connect attempt, set the return value of the DdeCallbackData object to false. Notice that you are not attempting to handle an empty string for the application name or the topic name. The code should be embellished to handle this possible situation.
initiate: aDdeServer clientData: unused callData: ddeCallbackData
"Initiate callback - check the application name and topic
and set the returnValue to true if they match."
| app top |
app := ddeCallbackData application asLowercase.
top := ddeCallbackData topic asLowercase.
((app = 'timer') and: top = 'time'])
ifTrue: [
self message:(
'Connect. App = %1 Topic = %2'
bindWith: app with: top).
aDdeServerManager notifyClientOfSupportFor: 'Time'.
ddeCallbackData returnValue: true]
ifFalse: [
self message: (
'Reject. App = %1 Topic = %2'
bindWith: app with: top).
ddeCallbackData returnValue: false]
Handling links
The code that follows is for the DdeNhotlinkCallback method. The method checks the item name and the format. It sets the DdeCallbackData object to true if the item is supported and false otherwise.
hotlink: ddeServer clientData: unused callData: ddeCallbackData
"Hotlink callback - Check the item name and the format.
Set the returnValue to true if the item is 'time' and
the format is 'String', false otherwise."
| item format string |
item := ddeCallbackData item asLowercase.
format := ddeCallbackData format asLowercase.
string := 'Item = %1 Format = %2' bindWith: item with: format.
(item = 'time' and: format = 'string'])
ifTrue: [
self message: ('Hotlink. %1' bindWith: string).
ddeCallbackData returnValue: true]
ifFalse: [
self message: ('Hotlink rejected. %1' bindWith: string).
ddeCallbackData returnValue: false]
The method for handling the warmlink and coldlink has the same form as the method for the hotlink. Instead of implementing the code shown below, the method could test the reason value of the DdeCallbackData object (by way of the reason message) and print out different messages based on the value that is returned.
warmlink: ddeServer clientData: unused callData: ddeCallbackData
"Warmlink callback - Check the item name and the format.
Set the returnValue to true if the item is 'time' and
the format is 'String'."
| item format string |
item := ddeCallbackData item asLowercase.
format := ddeCallbackData format asLowercase.
string := 'Item = %1 Format = %2' bindWith: item with: format.
(item = 'time' and: format = 'string'])
ifTrue: [
self message: 'Warm link. %1' bindWith: string.
ddeCallbackData returnValue: false]
ifFalse:
self message: 'Warm link rejected. %1' bindWith: string.
ddeCallbackData returnValue: true]
Note:
The Excel spreadsheet program sends coldlink messages with an empty string as the format.
coldlink: ddeServer clientData: unused callData: ddeCallbackData
"Coldlink callback - Check the item name."
| item format string |
item := ddeCallbackData item asLowercase.
format := ddeCallbackData format asLowercase.
string := 'Item = %1 Format = %2' bindWith: item with: format.
item = 'time'
ifTrue: [
self message: 'Cold link. %1' bindWith: string.
ddeCallbackData returnValue: true]
ifFalse: [
self message: ('Cold link rejected. %1' bindWith: string).
ddeCallbackData returnValue: false]
Handling client requests
The next callback you implement is DdeNrequestCallback. This callback is run when the client requests data from the server. As in previous examples, you check whether the item and the format are what you are looking for, then send the data to the client by sending the message sendItem:value:format: to the DdeServer that was passed to the callback method. The return value of the DdeCallbackData object should be set to true if the data is sent to the client and false if the data is not sent to the client.
request: ddeServer clientData: unused callData: ddeCallbackData
"Request callback - Check the item name and the format. If they
match what you are looking for then send the data to the client."
| item format string |
item := ddeCallbackData item asLowercase.
format := ddeCallbackData format asLowercase.
string := 'Item = %1 Format = %2' bindWith: item with: format.
(item = 'time' and: format = 'string'])
ifTrue: [
self message: 'Request. %1' bindWith: string.
ddeServer
sendItem: ddeCallbackData item
value: Time now printString
format: 'String'.
ddeCallbackData returnValue: true]
ifFalse: [
self message: ('Request rejected. %1' bindWith: string).
ddeCallbackData returnValue: false]
Ending
The last callback you implement is the DdeNterminationCallback. This method should not attempt to disconnect. It only prints to the Transcript that the server dropped a connection.
termination: ddeServer clientData: unused callData: ddeCallbackData
"Termination callback - Print out a message that the server is
disconnecting."
self message: 'Connection terminated'.
Now all initialization has been completed and all the callbacks have been set up. The main work of the server is now contained in the method that is run whenever the timer goes off. When the timer goes off and the timerProc: method is run, the one supported item is updated. As the item is updated the DdeServerManager looks for any hot- or warmlinks to the data and updates the clients that are linked to that piece of data.
timerProc: clientData
"The timer has gone off so update the item database."
ddeServerManager
updateItem: 'time'
topic: 'Time'
value: Time now printString
format: 'String'.
self timerSet.
Note:
After the updateItem:topic:value:format: message is completed, the data passed as the value parameter is lost. The DdeServerManager does not keep a copy of this data. When the default database is used, the data is kept so that when a client requests the data the DdeServerManager can give the data back without intervention from the application. The implication is that if a server has many items or lots of data, it might not be able to use default processing and the default database because of memory restrictions. In that case the server has to hook the callbacks and process the events itself.
The message: method that has been used throughout the example follows:
message: aString
"Print a message on the Transcript."
Transcript cr; show: 'Timer Server: ', aString
The final method that you need to implement is the free method. This method stops the timer and frees up the DdeServerManager.
free
"Close down by stopping the timer and closing down the DdeServer."
self timerOn: false.
ddeServerManager notNil ifTrue: ddeServerManager free]
You now have a DDE server that provides the current time in String format.
An improvement to the code might be to support the empty application name (server name) and an empty topic name as well as both the date and time in other formats.