"What do you have there Carl? Oh," Linda asked, glancing at the schematic on the screen.
"I want to get the signal here", Carl pointed, "over to this other part here, but I'm not sure the best way with all this stuff in between."
"Why don't you route it this way?" Linda suggested, drawing a rough path along the schematic, "this should be the most effective."
"Oh and then I can move this chip over to here instead," Carl replied as he dragged the chip to its new location.
Linda moved a couple of other components over. "That looks like it should pretty much do it."
"Great, that'll work. Thanks for your help."
They both turned back to their own work. A pretty typical situation that repeats itself thousands of times daily in workplaces everywhere.
Except that Linda and Carl are thousands of miles apart.
Groupware actually refers to any technology that lets people work together. So things like email and Usenet news are also rudimentary groupware technologies. The difference is that email and Usenet don't allow people to work together at the same time or "synchronously", but instead support different time or "asynchronous" work. Another distinction between types of groupware is whether the system supports people working in the same place (such as a team meeting room) or at a distance, such as different sites on the Internet. We're mostly concerned with groupware for geographically distributed groups working together synchronously.
Unfortunately, building groupware applications can be extremely difficult. Implementing even the simplest system is a lengthy and tedious process. Every application must worry about creating and managing socket connections, parsing and dispatching inter-process communication, locating other users on a network and connecting to them, keeping shared resources consistent between users, and so on. Using conventional programming tools, a lot of low level code must be written before getting to the specifics of the application. Groupware is an application domain crying out for better programming tools.
GroupKit grew out of our frustrations in building groupware systems without proper tools to support the job. By moving the common elements of groupware into a Tcl/Tk extension, we can now create programs in three days that originally took three months to write, and whose complexity shrunk from several thousand lines of code to only a few hundred lines. We've also found that it is often easy to take an existing single-user Tcl/Tk program and convert it to a multi-user program. With GroupKit, we're finding that writing groupware is only slightly harder than writing an equivalent single-user program.
The GroupKit distribution you'll find on the CD consists of the GroupKit extension itself (which is implemented as a mixture of Tcl and C), documentation, and approximately thirty example groupware applications.
We'll assume that GroupKit has already been compiled and installed on all participating systems, with a registrar process running. If GroupKit has not yet been installed, you can install it yourself by following the step by step installation instructions in the README file in the software distribution. In your own account, you'll need to make sure that your Unix PATH variable points to where the GroupKit binaries have been installed (type "which open.reg" at your shell prompt to check).
open.reg &
The first time you start up, the session manager may ask you some questions about yourself, such as your name, and store this in a file called ".groupkitrc" in your home directory. It should then show the window in Figure XX (if you have problems, make sure that you have a registrar process running). The "Conferences" pane on the left shows the names of any running conferences. Selecting one will show who is in the conference in the "Participants" pane on the right. The "Conferences" menu contains a list of known GroupKit applications and lets you start up new groupware sessions.
Figure XX. Open Registration session manager.
To create it, pull down the "Conferences" menu in the session manager and select "Simple Sketchpad". A dialog box will appear, shown in Figure XX. This dialog box allows you to give your conference a name (by default it is just the name of the application) which will identify it to other users. When you've picked a name, click the "Create" button.
Figure XX. Conference naming dialog.
At this point, you'll see the name of the conference added to the "Conferences" pane in the session manager, and the Simple Sketchpad program will come up in its own window, as shown in Figure XX. You can draw on the canvas using the left mouse button. The menus let you clear the canvas, exit the program, find out about other participants in the conference, and get information about both GroupKit and the Simple Sketchpad conference. Try it!
Figure XX. Simple Sketchpad conference.
To join the conference from their session manager, the other participant can double-click the name of the conference. This will add their name to the list of participants, and also bring up a window with their own copy of the Simple Sketchpad program. You'll also see that any drawing that was done in the first copy of the program appears in the new copy.
You'll now find that both people can draw at the same time, and that any drawings made by one user immediately appear on the screens of the other user. You'll also see a small cursor called a telepointer, which tracks the location of the other user's mouse cursor as they move around the window. More than two people can be in this conference and others can join and participate through their own session managers.
When done, you can select "Quit" from the "File" menu of the Simple Sketchpad program to leave the conference. Your copy of the program will disappear, and you'll see your name removed from the list of conference participants in the session manager. When the last person leaves, they are asked to either save the contents of the conference (which gets stored in a background GroupKit process and would allow restarting at a later time, with the drawing intact), or to delete the conference.
Its also possible to invite other users into a GroupKit conference. In your session manager, select a conference, and then choose "Invite..." from the Collaboration menu. This will give you a list of all users who are currently running a session manager of their own. Choosing from that list will popup a message on their screen, asking if they would like to join the conference. If they decide to join, their session manager will automatically join them to the conference.
Figure XX. GroupKit process and communications architecture.
Figure XX. Some example GroupKit conference applications.
The program we'll build is a brainstorming tool called "Note Organizer." This tool can help a group generate and organize ideas, for example to plan a paper, software project, advertising campaign, etc. As a brainstorming tool, the program will allow users to enter ideas (a few words), each which will be represented as a text item in a Tk canvas widget. To organize the ideas, users will be able to drag them around on the canvas, grouping related ones and so on. Being groupware, everyone in the group will be able to generate and move ideas around at the same time, and see what everyone else is doing. The program will look like the one in Figure XX.
Figure XX. Screen dump of Note Organizer.
Here is how we'll approach building this example. We'll start off by creating a single-user version of the program that allows you to create ideas, but not to move them around. This will run as a normal "wish" script, without using GroupKit at all. We'll then take that program and modify it slightly so that it will run within GroupKit, before putting any code in to "share" the ideas. The next step will be to share ideas, so that when one participant creates an idea on their canvas, it appears on other participants' canvases also. We can then begin to get more sophisticated, by letting users drag ideas around the display, which will introduce some techniques that are useful when building groupware systems. Finally, we'll have the program do appropriate things when people enter and leave the session, and also add some groupware widgets that will help keep track of what people are doing when they are in the session with us. That will complete the application as illustrated in Figure XX. If you'd like to skip ahead and see what the entire program will look like, the complete listing of the final version appears at the end of this chapter.
set notes(x) 20 set notes(y) 20 set notes(notefont) -adobe-helvetica-medium-r-normal--17-* proc buildWindow {} { frame .main canvas .notepad -scrollregion "0 0 800 3000" -yscrollcommand ".scroll set" scrollbar .scroll -command ".notepad yview" frame .controls entry .newidea button .enteridea -text "New Idea" -command addNewIdea pack .main -side top -fill both -expand yes pack .notepad -side left -fill both -expand yes -in .main pack .scroll -side right -fill y -in .main pack .controls -side top -fill x pack .newidea -side left -fill x -expand yes -in .controls pack .enteridea -side left -in .controls } proc addNewIdea {} { set idea [.newidea get] doAddIdea $idea .newidea delete 0 end } proc doAddIdea {idea} { global notes .notepad create text $notes(x) $notes(y) -text $idea -anchor nw \ -font $notes(notefont) incr notes(y) 20 if {$notes(y)>600} { set notes(y) 20 incr notes(x) 100 } } buildWindow
You should be able to run that program just fine under Tk's normal wish. Type ideas in the entry box and press the button to add them to the canvas.
gk_initConf $argv
The second thing you should do is add GroupKit's standard menubar, which contains menu items to exit the program, find out what other users are working on the program with you, and display an about box. Add these lines before creating the main interface of your program, i.e. just before creating the canvas widget:
gk_defaultMenu .menubar pack .menubar -side top -fill x
As before, we'll start our conference application using the Open Registration session manager. But first we have to tell the session manager about our new program. To do this, add the following line to the bottom of your .groupkitrc file (which was created in your home directory the first time you ran the session manager). Of course, change the "/home/you" to the name of the directory where you put the note organizer script file of course. Note that each user has their own .groupkitrc file, which is initially copied from a template set up by whoever installed GroupKit on your system.
userprefs prog.NoteOrganizer.cmd "exec gkwish -f /home/you/noteorg.tcl"
Either quit and restart your session manager, or choose "Re-initialize" from the "File" menu. You should now find an item named "NoteOrganizer" under the Conferences menu. Use it to create the Note Organizer conference as before, and then join the conference from another session manager.
If you run multiple copies, you'll quickly find that if you enter ideas in one copy of the program they don't appear on remote copies. Thats because we haven't actually told the program to display ideas on all screens - GroupKit won't take care of that automatically. You will find though that items in the menubar work fine, such as the "Show Participants" item which provides information on other users in the conference.
doAddIdea $idea
to:
gk_toAll doAddIdea $idea
and then re-start the program from the first session manager. After joining the conference from the second session manager, you should find that ideas entered in one copy of the program now appear in the other copy as well. That wasn't so bad!
So at this point, you've seen what is involved in getting a simple groupware program up and running in GroupKit. You've been exposed to GroupKit's session manager, learned how to tell the session manager about your new program, and how to initialize a GroupKit program. Finally, you've seen the gk_toAll command, which is one of GroupKit's programming constructs that can make it easy to turn a single-user program into a multi-user one.
So what does the gk_toAll do? That command arranges for the Tcl command following it (i.e. doAddIdea $idea) to be executed not only in the local program (where the idea was typed in), but also in every other copy of the program running in the conference. So, if you had not just two, but three, four or ten people joined in the conference, all of their conference processes would execute that same command. This is illustrated in Figure XX.
Figure XX. The gk_toAll command.
The gk_toAll command is an example of a Remote Procedure Call (RPC) that GroupKit provides. GroupKit has other RPC's that differ in who the commands get sent to, as shown in the sidebar. RPC's are a fairly straightforward but effective way to turn a single-user program into a multi-user one.
You'll find when you're using RPCs that you often need to "factor" your code, splitting a routine into two parts, as you saw in the "addNewIdea" and "doAddIdea" procs. This separates the code that invokes a routine (usually called from the user interface, and which is executed only by a single user) from the code that actually performs the operation (which is executed by everyone in the conference). Not only is this a useful style to adopt for groupware, but it is good coding practice generally. Factoring your code makes it easier to change your user interface, or invoke the core of your program in entirely new ways such as from a Tcl script.
GroupKit Remote Procedure Calls
GroupKit's Remote Procedure Calls (RPCs) execute a Tcl command in the conference processes of different users in your conference session. They differ in which processes the commands are sent to.
- gk_toAll cmd args.
- Execute a Tcl command on all processes in the session, including the local user.
- gk_toOthers cmd args.
- Execute a Tcl command on all remote processes in the session, but not the process of the local user.
- gk_toUsernum user cmd args.
- Execute a Tcl command on only a single conference process, which may be one of the remote users or the local process. The process is identified by its user's unique user number, which can be extracted from the users environment, described shortly.
set canvasid [.notepad create text $notes(x) $notes(y) -text $idea \ -anchor nw -font $notes(notefont)] .notepad bind $canvasid"noteDragged $canvasid %x %y" proc noteDragged {id x y} { .notepad coords $id [.notepad canvasx $x] [.notepad canvasy $y] }
As before, we could use gk_toAll to execute that callback in all users' programs by changing the line in noteDragged to:
gk_toAll .notepad coords $id [.notepad canvasx $x] [.notepad canvasy $y]This will work most of the time, but is very fragile and can break if users are entering ideas very quickly. Here's why. The code above assumes that copies of an idea on different users' screens all have the same canvasid. But that is not always true.
Tk assigns canvasid's in the order in which items are created. But if two users create ideas at almost the same time, they may end up being created in a different order on each users' screens. If the first user enters "foo", the gk_toAll adds the idea to the local canvas and then sends it across the network. If at the same time a second user enters "bar", their gk_toAll puts it on their canvas and sends it across the network. This will result in the first user adding "foo" then "bar", while the second user adds "bar" and then "foo." The two items will be created in different orders on the different systems, and therefore the canvasid's won't match. This will be a problem when it comes to moving the items around, because we need to correctly refer to the item to move. This is shown in Figure XX.
Figure XX. Ideas created in different orders on different machines.
To make matters worse, because the doAddIdea routine (which creates the text items on the canvas) also chooses the position on the canvas, if the ideas cross paths, not only will they have different canvasid's, but they will appear in different places on different users' screens.
Problems like these are fairly common in most groupware applications. They can be solved, but only if the programmer is aware of the nuances of these situations. Lets address these problems one at a time.
What we'll do is slightly modify that scheme, so that each conference process will keep a global counter, but those counters will be unique from those of other conference processes. We will do this by prefacing each counter with a special GroupKit id number guaranteed unique for that conference process.
GroupKit generates a user number for every user in a conference process. It stores that number (and a lot of other information) in a data structure called an environment. We'll discuss environments in a moment, but for now just assume that the following code fragment will return the user's unique id number:
set myid [users get local.usernum]
We can now generate a unique id for every idea, by combining our user number (held in myid) with a global counter. We can then send that combined id to doAddIdea, and tag the canvas item with the combined id. Finally we can use this unique id when we're moving the idea around. This is shown below.
set notes(counter) 0 proc addNewIdea {} { global notes set idea [.newidea get] set myid [users get local.usernum] set id ${myid}x$notes(counter) incr notes(counter) gk_toAll doAddIdea $id $idea .newidea delete 0 end } proc doAddIdea {id idea} { global notes .notepad create text $notes(x) $notes(y) -text $idea -anchor nw -font \ $notes(notefont) -tags $id .notepad bind $id"noteDragged $id %x %y" ... } proc noteDragged {id x y} { gk_toAll .notepad coords $id [.notepad canvasx $x] [.notepad canvasy $y] }
proc addNewIdea {} { ... global notes gk_toAll doAddIdea $id $notes(x) $notes(y) $idea ... } proc doAddIdea {id x y idea} { ... .notepad create text $x $y -text $idea -anchor nw \ -font $notes(notefont) -tags $id ... }What happens now when ideas are entered at the same time by different users? If you trace through the order that messages will get executed, you'll see that both ideas will end up entered in exactly the same place, overlapping each other! When you think about it, this actually makes some sense. Imagine the real-life setting where users place real paper notes on a large board. If two people place notes at the same time, they'll both reach for the same place. Seeing their problem, they can then choose to move one of the notes somewhere else. In our computer version, users can do the same thing - see the overlapping ideas and move one of them out of the way. While its probably possible to build very elaborate algorithms into software to recover from situations such as these, it is often best just to let people deal naturally with these situations.
To illustrate relaxed-WYSIWIS, we'll add to our program the notion of a "selected" idea, just as you'd select text in an editor or an object in a drawing program. We'll display the selected idea in a larger font. For this application, we'll decide that each participant has their own selection. Users can select an idea by clicking on it. The code that actually manipulates the selection will keep track of the selected object by adding a "selected" tag to the canvas item. Here is the code:
set notes(selectedfont) -adobe-helvetica-bold-r-normal--20-* proc doAddIdea {id x y idea} { ... .notepad bind $id <1> "selectNote $id" ... } proc selectNote id { global notes catch {.notepad itemconfig selected -font $notes(notefont)} catch {.notepad dtag selected selected} .notepad addtag selected withtag $id .notepad itemconfig $id -font $notes(selectedfont) }In this example, we made the conscious design decision to not share selection between users. However, it would of course be possible to have the selection shared, so that there is a single selection, shared among all users. This could be built by adding the appropriate gk_toAll RPC when changing selection.
We now successfully have dealt with the problems of coordinating multiple users. Generating unique id's based on the users' unique user number let us refer to objects uniquely, while letting the sender decide on the idea's position resulted in ideas appearing in the correct location on all screens. Finally, we showed that it may be desireable to have some information, such as selection, that is not shared between all users.
GroupKit stores each user's color (as well as other information) in a data structure called an environment. Environments are structured as a tree, where each node can contain either other nodes or a value. A node is referred to by its position in the tree, with each level of the tree separated by a dot. So for example, local.usernum refers to the node usernum which is a child of local which is a child of the root node of the environment. This is similar to how Tk refers to windows in the window hierarchy.
As you've gathered, information about the local user is stored underneath the local key in the environment called users, so local.color is the node holding your own color. You can retrieve its value by the call [users get local.color]. Information about remote users is stored under a remote key, and further divided up by each remote user's unique user number, as shown in Figure XX. So for example, you can retrieve the color of the remote user having unique id 3 by specifying users get remote.3.color* . More examples of how to use environments are described in the sidebar "Using Environments".
Figure XX. Users environment.
Using Environments
Environments have a number of different commands used to manipulate them. If you're familiar with Extended Tcl's keyed lists, you'll find some similarities. Some of the commands include:
- gk_newenv envname. Create a new environment, which also creates a Tcl command called "envname" used to access the environment.
- envname set key value. Set the value of the node located at key.
- envname get key. Get the value of the node located at key.
- envname keys key. List the children of the node located at key.
- envname delete key. Delete the node located at key (and all nodes below it).
- envname exists key. Check if a node located at key exists in the environment.
- envname destroy. Destroy an environment, all data in it, and its Tcl command.
GroupKit maintains a number of environments internally. You've seen the users environment which holds information about all the users in the conference, including their color (the key "color"), their full name ("username"), their userid ("userid"), and host ("host"). You can also store your own information in the users environment, to be used by your application. The other environment you've encountered briefly is the userprefs environment, which is used within the .groupkitrc file to specify where different conferences exist. Environments are also used extensively throughout the session management subsystem in GroupKit.
Lets add this into our program now. When we create an idea, we'll send our user number along with the idea. When creating the text item to display the idea, we'll change its color to be that of the user who created it. We'll also add a canvas tag to the text item identifying the user who created it, which we'll come back to later. Here are the changes in the code:
proc addNewIdea {} { ... gk_toAll doAddIdea $id [users get local.usernum] $notes(x) $notes(y) $idea ... } proc doAddIdea {id usernum x y idea} { ... if {$usernum==[users get local.usernum]} { set color [users get local.color] } else { set color [users get remote.$usernum.color] } .notepad create text $x $y -text $idea -anchor nw -font $notes(notefont) \ -fill $color -tags [list $id user$usernum] ... }Environments provide a convenient way to find out information on other users. They also have other features that can help in building groupware, which we'll return to later. In the meantime however, we'll look at another GroupKit feature called conference events.
Conference events are similar to bindings that you can attach to widgets. But rather than executing a piece of callback code when something happens to a widget (e.g. the user presses a button), conference events execute a piece of code you provide as users join and leave the conference.
To specify handlers for conference events, you use the gk_bind command, which works very similarly to Tk's bind command, right down to its use of "percent substitutions" to pass event parameters to your callback code. The general format of the gk_bind command* is:
gk_bind event-type callback-code
gk_bind newUserArrived { set newuser %U set name [users remote.$newuser.username] toplevel .new$newuser pack [label .new$newuser.lbl -text "$name just arrived."] pack [button .new$newuser.ok -text Ok -command "destroy .new$newuser"] }
gk_bind userDeleted { catch { .notepad itemconfig user%U -fill black } }
Figure XX. Updating latecomers.
This information is normally sent via the gk_toUserNum RPC, described earlier. This call needs the user number to send the message to, again extracted from the event via the "%U" substitution.
gk_bind updateEntrant { foreach item [.notepad find all] set x [lindex [.notepad coords $item] 0] set y [lindex [.notepad coords $item] 1] foreach tag [.notepad gettags $item] { if {[string range $tag 0 3]=="user"} { set usernum [string range $tag 4 end] } else { if {$tag!="selected"} {set id $tag} } } set idea [.notepad itemcget $item -text] gk_toUserNum %U doAddIdea $id $usernum $x $y $idea } }The updateEntrant event is also used to make conferences persist (that is, make them stick around after the last user has left, so that they can be rejoined later). You've noticed that when you quit the last program in the conference, you're asked if you'd like to delete the conference or have it save its contents (persist). If you ask it to persist, GroupKit sends an updateEntrant event to your program, but rather than asking it (via the %U parameter) to update a new user, GroupKit passes a special user number that causes your program to send messages to a special persistence server that records them. When a user next joins the conference, the messages stored in the server are played back, exactly as if they were sent from the last user.
It is also possible to create your own events, specific to your application. This can be useful as your programs get large, as a way of communicating changes between different parts of your program. See the gk_notifier(n) manual page for more information.
As a conference participant, you may sometimes find it hard to follow when several people are working at the same time. Ideas are being rapidly moved around, and its unclear who's doing what when. Also, because the canvas is quite tall, it can become hard to track where people are working.
GroupKit provides a number of different awareness widgets to help with these sorts of problems. Keeping track of where people are working and what they are doing is a common problem in a number of different groupware programs, so our widgets can be easily added to many different applications.
Telepointers, also known as multiple cursors, are used to provide very fine-grained information about where other users are working in the application. As other users move around the application, you'll see a small cursor which follows their mouse cursor. Just tracking those cursors - which you're usually just peripherally aware of - provides a surprising amount of information about who's doing what in an application, what objects people are working with, and how active different people are (Figure XX). Telepointers can be attached to any Tk widget.
Figure XX. GroupKit's telepointers.
Adding telepointers to our example is quite straightforward, requiring only two lines of code. The first line initializes the telepointers, and the second attaches the telepointers to our canvas widget.gk_initializeTelepointers gk_specializeWidgetTreeTelepointer .notepad
Figure XX illustrates GroupKit's multi-user scrollbars. They consist of two parts: a conventional Tk scrollbar on the right, and a set of bars showing where users are located in a document to its left. The conventional scrollbar is used normally, to scroll your own view in the document. As you scroll, you'll also notice one of the bars to the left scrolling with you. That bar is showing your position in the document. As other users scroll their own views, the other bars on their display will change to show their new positions. Clicking on each bar will display a popup reminding you which user the bar is associated with. Multi-user scrollbars allow you to quickly find where others are working in a large document and - by aligning your view with theirs - join them.
Figure XX. Multi-user scrollbars.
Multi-user scrollbars are added to applications in exactly the same way that conventional scrollbars are. In fact, the only difference is that instead of using the Tk scrollbar command, you use the GroupKit gk_scrollbar command.
proc buildWindow {} { ... gk_scrollbar .scroll -command ".notepad yview" ... }GroupKit also comes with a number of other widgets. You've already seen the menubar, which includes items such as an Exit command, an about box for GroupKit, and an item which brings up a dialog box giving you information about the different users in the conference with you. There's also a widget called the "mini view", which is similar to the multi-user scrollbar, but when combined with a text-based program can show a miniature of the document along with the scrollbar. There's also a widget that is included to make it easier to build online help into your application.
At this point we've completed our Note Organizer program; you'll find the complete program listing at the end of this chapter. The next section will go on to talk about a very different approach to building groupware applications with GroupKit.
To understand how environments can be used to build groupware programs, we'll have to first describe a paradigm called "Model-View-Controller" (MVC), which was first introduced in Smalltalk as a way to construct (single-user) user interfaces. The MVC paradigm suggests that an application should be divided into three distinct components:
Under this paradigm, the controller never changes the onscreen view directly. Instead it changes the model, and the changes to the model are picked up by the view which does cause a change onscreen. This is illustrated in Figure XX.
Figure XX. Model-View-Controller paradigm.
This approach can be a bit hard to follow at first, since input events only indirectly affect what is displayed onscreen, but has a number of advantages. First, by clearly separating out your underlying data from the view of that data and how it is manipulated, it encourages you to build a very clear data model, which can help you understand exactly what it is your program is manipulating. By doing this separation, you also find it easier to modularize your code, rather than trying to do everything in one place. Finally, the paradigm makes it easier to have multiple views on the same data structure, for example allowing you to view a table of numbers as either a spreadsheet or a graph.
So how does this relate back to GroupKit? Recall that we can use GroupKit's environments to store a fairly arbitrary set of data (such as names and colours in the users environment), arranged in a tree structure, where we can associate a string value with any node in the tree. This can serve as an underlying data structure for a wide variety of applications, and so is an appropriate choice for our Model under the MVC paradigm. Environments can also generate events (similar to the conference events used for announcing new users etc.) when they are changed. These events can be received by other portions of your program, such as a View which updates the screen to match the change in the Model. Finally, parts of your program that handle user input (the Controller portion) can react to these changes by modifying the environment.
We've taken environments one step further, and allowed them to be shared between all the users in a conference. When any user makes a change to their own copy of the environment, that change is automatically propagated to all other users' copies. So while a user's input event may change their own environment, which generates an event to update their onscreen view, the change also gets sent to every other user's environment, generating the same event and updating their view - instant groupware.
These shared environments and the MVC paradigm really shine on larger applications, but here we'll just present a toy example to illustrate the mechanisms. Our data model will be held in an environment called stocks and consist of a single piece of information, the value of the fictional GroupKit Inc. stock. Our view will consist of an onscreen label showing the value, and an entry widget will allow us to change the value.
After initializing GroupKit, we begin by creating the stocks environment, using the gk_newenv command. The -share flag tells GroupKit to create this as a shared environment* , and the -bind flag specifies that events should be generated when the environment is changed. We'll then give our GroupKit stock an initial value.
gk_initConf $argv gk_defaultMenu .menubar pack .menubar -side left -fill x gk_newenv -share -bind stocks stocks set groupkit 1Next, we'll create our view, using a label widget. The stocks bind changeEnvInfo attaches a binding to the environment, so that whenever a change is made to it, the following code will be executed. There are also events for when a piece of information in the environment is first added (addEnvInfo), and when an item is removed (deleteEnvInfo)* . The event parameter %K refers to the name of the key in the environment that changed; we could have omitted they key here since there is only one key in the environment.
label .view -text "GroupKit Inc. value is [stocks get groupkit]" pack .view -side top stocks bind changeEnvInfo { if {%K=="groupkit"} { .view config -text "GroupKit Inc. value is [stocks get groupkit]" } }Finally, we create the controller, using an entry widget. When we hit the "Return" key in the entry, its value is retrieved and stored in the stocks environment. The view code should then notice the change and update the display. Finally, because it is a shared environment, the change will show up in all other users' environments, updating their views as well.
pack [entry .controller] -side top bind .controllerWe could easily replace either the controller or view with ones that behaved differently, without changing any other parts of the program (or they could even display several views, since multiple bindings on an event are allowed). Different users could also use different versions of the programs in the same conference. This works because only the models (the shared environment stocks) of different users directly communicate with each other, and not the views or controllers. So for example here is a view that uses a canvas widget to display a horizontal bar whose length depends on the value of the stock held in the environment:{stocks set groupkit [.controller get]}
pack [canvas .barview] -side top .barview create rectangle 0 0 [expr [stocks get groupkit]*5] 10 -anchor nw -tags bar model bind changeEnvInfo { if {%K=="groupkit"} { .barview coords bar 0 0 [expr [stocks get groupkit]*5] 10 } }In summary, GroupKit's shared environments can serve as an easy way to implement the Model-View-Controller paradigm, and extend it to support multi-user applications. Environments can take care of the housekeeping chores in updating and synchronizing multiple copies of a data structure (such as the multiple copies of the canvas widget used in the Note Organizer - though the canvas data structure had the advantage of having its view built-in!), and can help to modularize your application. Its a judgement call whether to use shared environments or the more direct RPC's in your application (they can be mixed for different parts of course). In general, if your application becomes larger, if you're dealing with information that can be changed in various ways by your program, or if it can be viewed in different ways, then environments are usually an appropriate choice.
One school of thought in the groupware world is that people are remarkably good at recovering from their own errors. If you make it obvious in your program when people are "fighting over" particular objects, they can generally back off and figure things out themselves using normal "social protocols", as we do in the real world. The key is providing this awareness of others actions, through things like immediate feedback, so that people realize they are interfering with each other.
There's a lot to be said for not providing system support for concurrency unless its really warranted. And as a bonus you don't have to debug any code...
GroupKit supports locking through a lock manager. This locking layer serves as a mechanism to manage locking data for clients in a GroupKit conference. In most applications, one lock manager is created per conference. The lock manager is a very simple environment that stores the lock name and related information. All locks are managed through the lock manager. In addition to the standard operations on locks, a conflict detection procedure for lock names can be specified on a per manager basis.
There are several demonstration applications included with GroupKit that demonstrate the use of the lock manager, including use of both pessimistic locking (where you wait until you get the lock before starting the operation) and optimistic locking (where you start the operation immediately on your local machine, but are prepared to undo any changes if the lock request is denied).
gk_lockmanager lockMgrwill create a lock manager named "lockMgr", and provide a new Tcl command by the same name.
lockMgr request myLockName myCallbackThe request command returns to the caller immediately, before the lock request is completed. The result is obtained through the callback, which is a Tcl command, typically a procedure, that takes as its last parameter the word "Succeeded" or "Failed". Additionally, if the lock request is not granted within the timeout period, the callback is invoked with the "Failed" parameter.
The request command also returns a transaction identifier for the pending request that can be used by the status command (below) to query the state of the request.
lockMgr release myLockNameThis command returns immediately. Your program will not receive any notification when the lock has actually been released.
set lockid [lockMgr request myLockName myCallback] ... puts [lockMgr status $lockid]
The "setconflictproc" accepts a single parameter, the name of a Tcl command to call to resolve lock conflicts. This command must accept two parameters, the name of the lock manager and the name of a newly requested lock. The procedure should return "1" if the newly requested lock does conflict with an existing lock (e.g. by inspecting the result of "lockMgr locks"), or "0" if the requested lock will not conflict with any current lock.
Note that GroupKit uses this routine internally for some of its own messages as well.
We've found the usual culprit is screen updates, which are fairly expensive. While frequent "update idletasks" commands can provide smooth motion (for example, when dragging objects around the screen), they can easily lead to this problem. You'll have to balance your need for smooth updates against the possibilities of considerable lag.
To do this in GroupKit, when you receive a message you can choose to not do all the processing immediately, but schedule some of it for a later time. The "gk_schedule" command allows you to do this. It takes two parameters, a tag and a command, as well as an optional timer parameter.
If there is another command already scheduled (i.e. pending execution) with the same tag, the command is thrown away. If there is no pending command with the same tag, the new command is added to a queue, and an "after" command will ensure the command is executed at some point in the future (default 100 milliseconds, but specified by the timer parameter).
The example below shows the use of gk_schedule. The procedure "doItemChanged" is called when an object's coordinates are changed. Rather than move the object onscreen immediately (which could be an expensive operation), the program uses gk_schedule to schedule the screen update later, but to happen no more often than once every 500 milliseconds.
gk_newenv objects objects bind envChangeInfo {doItemChanged} proc doItemChanged {} { # object changed.. gk_schedule screen_update doScreenUpdate 500 } proc doScreenUpdate {} { # look in environment for current state of object set coords [objects get ...] # update screen ... }Note that the screen update command looked at the current state of the data structure, rather than having this information passed as a paramter. This is important, because if five object moves happened, only the first command would get scheduled by gk_schedule, and the coordinates passed to the screen update would be from the first object move, whereas we'd want the most current.
You can also choose to cancel a previously scheduled command if it hasn't already run. To do so, call "gk_cancelScheduled", passing it the same tag you passed to gk_schedule.
The most up-to-date information about GroupKit can be found on our World Wide Web page, accessible at http://www.cpsc.ucalgary.ca/projects/grouplab/groupkit. There you'll find information on current projects, and pointers to various papers that have been written about GroupKit. The latest version of the software can be found on our ftp site, ftp.cpsc.ucalgary.ca, in the directory /pub/projects/grouplab/software. Finally, we run a mailing list, which you can join by sending email to groupkit-users-request@cpsc.ucalgary.ca. The sidebar "Further Reading" describes a number of sources for more information on groupware in general.
Further Reading
There are a number of excellent souces of information about groupware, as well as Computer Supported Cooperative Work (CSCW), which is the academic discipline that encompasses groupware. A recent collection is "Readings in Groupware and Computer-Supported Cooperative Work: Assisting Human-Human Collaboration", written and edited by Ron Baecker and published by Morgan-Kaufmann in 1993. It includes not only technical groupware information, but also case studies of cooperative work, information on the psychology of groups, and other foundational areas whose understanding is critical to building successful groupware. Other good collections include "Computer-Supported Cooperative Work and Groupware" (edited by Saul Greenberg; published by Academic Press in 1991) and "Intellectual Teamwork: Social and Technical Foundations of Cooperative Work" (edited by Jolene Galegher, Rob Kraut, and Carmen Egido, published by Lawrence Erlbaum Associates in 1990).The premier academic conference is the CSCW conference, sponsored by the ACM, which has been held every second year since 1986. Besides the conventional proceedings (available from ACM Press), recent years (since 1992) have also included a video proceedings. The European CSCW conferences have been held in odd years since 1989. Other conferences having a large concentration of groupware or CSCW papers include the annual ACM SIGCHI conference (dealing with user interface and human factors issues), as well as the ACM Organizational Computing Systems conference (formerly Office Information Systems). Communications of the ACM has also run special issues devoted to CSCW (December 1991, January 1993, January 1994). Dedicated journals include Computer Supported Cooperative Work (Kluwer) and Collaborative Computing (Chapman and Hall), though groupware articles can also be found in Transactions on CHI (ACM) and Transactions on Information Systems (ACM).
Finally, a couple of pointers on the Internet. Yahoo maintains a page of groupware information you can access via "Computers and Internet:Software:Groupware". Pal Malm has collected together a number of systems and projects into the "unofficial CSCW Yellow Pages". Another good page of pointers is maintained by Tom Brinck.
- http://www.yahoo.com
- ftp://gorgon.tft.tele.no/pub/groupware/cscw_yp.ps.Z
- http://www.crew.umich.edu/~brinck/cscw.html