John is president of AJR Co., a Cambridge, MA consulting firm. He can be contacted on CompuServe at 72607,3142.
Easel and Enfin are two GUI builders from the same company, offering similar capabilities via radically different programming languages. Easel, the elder statesman of the two, first appeared nearly a decade ago, long before the current object-oriented programming craze. Enfin, on the other hand, was developed from the ground up as an object-oriented system based on the Smalltalk environment.
In its early days, Easel was a stand-alone 4GL for DOS application development. Today, it is just one component of Easel Corp.'s Enterprise Workbench client/server development environment, which also includes the DB/Assist point-and-click visual tool for SQL access. The Easel-language compiler supports OS/2 2.1, Windows, and DOS. This article focuses on the OS/2 implementation referred to as "Easel/32" (the Windows version is "Easel/Win").
For its part, Enfin (available for both OS/2 and Windows) is an object-oriented, visual development environment that lets you select display objects (fields, buttons, list boxes, menus, windows, dialog boxes, and the like), position them on the screen, and define their attributes. Enfin then automatically generates source code. As you might expect, the Enfin environment also provides an SQL front end, class browser, inspector, profiler, interactive debugger, and DLL/DDE support. You can also write custom routines to be written in C, Cobol, assembler, or other low-level languages.
To examine the strengths and weaknesses of Easel/32 and Enfin, I've designed a personal, total-health tracking and management application which exercises, in some depth, the main features of both toolkits. The package allows an individual to enter and analyze personal-health data, a Quicken for the body.
The heart of the application is a database of personal data: meals, workouts, illnesses, treatments, weigh-ins, and checkups. The theory is that, by looking at the data over time, you can get a better feel for how to manage your health. Of course, no total-health package would be complete without drug, anatomy, and general-health references--all areas where CD-ROM publishers have provided strong products, so all I provide for them are hooks to start up external applications. Figure 1 shows a simplified data model for the application.
Easel tries hard to be English-like and, up to a point, succeeds. Statements such as make GenericList_DialogBox visible and change IngredientName_EditField text to StringVar are typical Easel. While this makes Easel extremely readable, there are problems. For example, the statements change text of IngredientName_EditField to StringVar and Copy StringVar to text of IngredientName_EditField are both made up of valid Easel constructs and mean the same thing as change IngredientName_EditField text to StringVar in English. In Easel, however, they don't compute. A minor gripe, admittedly, but unlike with other languages, no matter how long I work with Easel, I still find myself making those kinds of mistakes.
Easel supports a number of basic data types (string, integer, float, and boolean), arrays of each type, variable-length Easel structures, and fixed-length structures suitable for use by external C-language DLLs. It also supports a large number of built-in object types--dialog boxes, sense, graphical and dialog regions, fonts, image maps, and the like. All of the standard Presentation Manager (PM) controls also get their own object type: button, entry field, list box, and so on. Easel is an explicitly event-driven language; thus, all the usual PM suspects--window open and close, button click, as well as program start and end--have their own events. Easel supports four different procedure types: actions, subroutines, functions, and response blocks. Actions, subroutines, and functions are stand-alone pieces of code, roughly corresponding to C's macros, call-by-reference functions, and call-by-value functions. Response blocks are explicitly attached in their declaration to specific events from specific objects. Whenever the event occurs, the response block executes. References to built-in object types can be either hard-wired object names or string variables containing a valid object name. Most of the power of Easel comes from combining this bit of abstraction with response blocks. Every possible event in the system can, but doesn't have to, have a response block associated with it.
Easel supports a suite of familiar control structures: If/Then/ Else, Switch, For, and While. You can also get outside Easel with relative ease by invoking external executables, DDE messaging, or by calling external DLLs. The flow of control in an Easel program is roughly: object creationevent generationprocedure call. The procedure calls in turn can create new objects, which generate more events, and so on. Figure 2 shows the flow in a typical Easel program.
Going to Enfin Smalltalk, I feel a little like the American in Paris who is startled to find that "they have a different word for everything over here!" Enfin is a Smalltalk variant, and knowing something about that language gives you a head start. Though neither a Smalltalk V nor Smalltalk-80 variant, Enfin promises to converge on ANSI Smalltalk whenever that arrives.
Enfin Smalltalk is object oriented (perhaps "object obsessed" is more appropriate) from the ground up. Things you might consider basic--data types, integer, and float, for example--are mere subclasses of more abstract classes. There are really only four basic elements to think about in Enfin: classes, methods, objects, and messages. A class implements one or more methods. An object provides an instance of a class. A message sent to that object selects a method for that object to execute. The statement System beepFrequency: 400 duration 100. sends the message beepFrequency:duration: to the object named System. As an object-obsessed language, Enfin is almost completely untyped. For instance, the statements:
X := 1.
X beepFrequency: 400 duration: 100.
compile, even though X is a number. X's object type isn't determined until run time, at which point it generates the error "instance method beepFrequency:duration: not found in class smallInteger." Figure 3 shows the flow of control for a Smalltalk program.
Since Enfin Smalltalk is nothing but objects and methods, the power of the language lies in the supplied classes and the methods they support. Most of the usual basic data types (bool, int, float, and so on) are supplied, and they support the usual operators. Some of your favorite control flow is available (conditional exec, loop-on-expression, and loop-on-index), as well as loop-over-collection. It's important to remember that control structures are implemented as messages with blocks of code as arguments. Thus, the statement X ifTrue: ['yes' out]. is more properly read as send the ifTrue message to X with ['yes' out] as the code-block argument. This means you can easily implement your own control flow by writing a method with a block
argument.
I chose to use different-style interfaces for the two implementations to reflect the strengths and Zen of the tools. The Easel implementation has a traditional PM/Windows style, with a hierarchy of menus available from a menu bar off the main screen. The Enfin implementation implements a drag-and-drop interface. Figures 4 and 5 show the first two levels of the app in both versions. In the Enfin version, any of the icons in the top two lines can be dragged and dropped onto the Edit or New icons in order to create a new item, or edit an existing item.
This choice has important implications in Enfin. To implement the traditional, hierarchical model, I would have created a Controller, with a single Form containing a menu and multiple Subforms. Instead, I have a Controller, with a single Form containing only a series of WorkplaceObjects. These WorkplaceObjects appear within the main form as icons and have forms attached to them. The WorkplaceObjects are direct substitutes for the menu items that would populate the menu bar of a hierarchical app. When one WorkplaceObject is dropped on another, the open method for the receiver is called. The default WorkplaceObject>>open method opens the Form associated with the receiving WorkplaceObject.
In the Easel version, you pick a type of item to work on from the menu. Then, from the second- level dialog, you either select an existing item to edit or create a new one. Each menu item gets its own response block which gets called automatically when the user selects the item. The response block queries the database, formats the displayable list of items, then calls a generic-list dialog box. When the user selects New or Edit from the generic-list box, the response block written for these buttons loads the dialog named by the parameter.
The flow of the application from main menu to edit-item dialog combines many of the most powerful features of Easel: parameters, object access-by-name, and item classes. All menu items are collected into a single class (it's probably easier to think of Easel classes as arrays of objects rather than OOP classes). This class gets a single response block. Choosing one of the items in the class causes the class response block to run. Each menu item also has a unique parameter. Calling parameter of object within the response block gets the parameter of the menu item that triggered the response. The response block constructs the names of the actions (unique to each menu item) to call using the parameter. The unique action blocks format the data for the list box. Listing One (page 92) shows the primary region, menu bar, menu item, item class, and response block code for a new Workout item.
External database access is central to both Easel and Enfin. For this application, I chose IBM's DB2/2 32-bit relational database system, which is designed for both LANs and single-user systems, and which supports application clients on DOS, Windows, and OS/2. I used the single-user OS/2 version supported by both Easel and Enfin, and I wrote a C program for database creation and initialization (with sample data).
Though previous versions of the Easel Workbench relied on local-application access to databases to start up a command-line OS/2 shell and read/write its standard I/O, both Easel and Enfin now support seamless DLL access to a number of different SQL databases as well as ODBC. The Easel Workbench provides automated SQL database linkage through the DB/Assist point-and-click visual tool, a separate executable bundled with Easel. DB/Assist allows you to construct and test SQL statements and link them to Easel variables. These statements are turned into Easel action blocks and inserted into the project. The Easel program calls OPEN_XXX, FETCH_XXX, and CLOSE_XXX (where XXX is the name of the SQL statement) and the data from the select appears in the linked variables. From there, you have to write code to enter these values into the dialog box. There is no autocreation of data-entry forms.
Enfin, on the other hand, provides automatic creation of data-entry forms via SQL Editor, a database tool included with the Enfin package. You pick a database table, choose Data Entry Form and voil--a pretty good first hack at the dialog box. Enfin also comes with a lightweight, internal SQL database that allows you to develop and test your application without actually running the external database.
Enfin provides automatic links into the database, which allow a view into the database to be automatically updated whenever the data behind it changes. Listing Two (page 92) shows the initialization code for the list box containing the list of workouts; you can choose one to edit or delete. Two lines of code link the TabularListBox object to the database table. Two more lines link the edit field in the dialog box to the field in the record. Rows fetched from the database appear as Record objects, and the fields within a Record object can be obtained by name. HEALTH never needs that kind of access. All database operations are handled by the provided classes.
In Enfin, the three external applications--Anatomy, General Health, and Drug references--are set up by subclassing the WorkplaceObject with my own ExecWPO class. The default WorkplaceObject behavior is to open the object associated with the icon, so ExecWPO provides setSessionName and setFileName methods that set up the session title and executable filename, and a new open method that uses the String class method startSessionProgram to execute the program. Listing Three (page 92) shows the new ExecWPO class. The Easel version uses a straightforward start application call from the menu-item response block.
The Easel Enterprise Workbench keeps the source for your project in a binary .EWB file. Using a third-party editor to change source involves exporting it from the integrated development environment (IDE), then importing it back in--an awkward process. The IDE does maintain a map of source-to-source file (project views) so that exporting is relatively painless. You can include Easel source files through the use of a simple include statement. Easel applications compile into an Easel binary (.EBI) file. In order to run the application, users need a run-time version of Easel installed. Easel run-time licenses carry a substantial price tag.
Enfin operates directly on ASCII-text .CLS files. You can edit outside the Enfin environment as long as you remember to reload a changed source file. You include Enfin-Smalltalk files in your application by adding the filename to the application file. Figure 6 shows the source-file organization of a three-form Enfin application. Designer, the interface source-code generator, gets you through the look-and-feel phase of program building. Like most code generators, Designer writes code its own way, and if you modify files outside the system, you have to be careful where you do it. The Controller method (initialize) seems to be reserved for Designer-generated code and if you throw something in there, then modify an object through Designer, it can get lost. The best policy is to not modify Designer-generated methods but to write your own and simply call them from the Designer-generated method. Enfin applications compile into an image (.IMG) file. Again, users need the run-time Enfin file (a single executable) but the Enfin run-time can be distributed royalty-free.
The final Enfin application comes out to about 1700 lines (including white space) of original code. Of that, approximately 1400 is instantiation of Form objects (essentially, dialog-box definitions). The Form initializations are mostly uninteresting, except for the code setting up links to the database.
I created a separate form for each list of database records, with a hardwired link to a database table, simply because it was so easy using Enfin Data Manager's automatic-default data-entry form facility. I could have used a single-list form by subclassing Form, as I did in the Easel version. This subclass would have to maintain variables for the table name and the edit form that follows when the user selects Edit or double-clicks an item in the list box. The subclass open method would take the new table name, remove any existing table link, then add a link to the new table and update it.
The Easel application comes out to about 2500 lines of original code. Of that, about 1400 consist of form initialization, 800 of DB/Assist-generated SQL, and 300 of manually entered code. This is a little deceptive, though. The only way I got away with so little manual code in Easel was by taking care in how I named the objects and actions. If I'd created a different form for each list of items (as I did in Enfin) the line count would have inflated exponentially.
Both Easel and Enfin are highly capable, complete application- production environments. The classes provided with Enfin, in general, provide more functionality than the built-in objects of Easel. For example, Form fields can have validation attached, and database-item attachment to dialog controls is handled automatically by the TableLink class, both of which require you to write more code in Easel. The power of Enfin is somewhat muted, though, by the IDE. Though you can subclass interface classes, when you do so, you lose the ability to modify them within the Enfin visual designer. When I tried to modify one of my ExecWPO objects through the designer, the code generator turned it back into a WorkplaceObject. The choice between them comes down to whether you're object obsessed or object tolerant. While object-obsessed Enfin is clearly the wave of the future, Easel provides similar functionality in a more traditional package.
# Definition of the menu bar. Note the parameter attached to every menu item.
action bar MainMenu_AB is
enabled pulldown Activity_PD text "~Activity"
enabled choice Workout_MC text "~Workout"
parameter is "Workout"
enabled choice Sleep_MC text "~Sleep"
parameter is "Sleep"
enabled choice Meal_MC text "~Meal"
parameter is "Meal"
# the rest of the menu bar def. would be here
...
...
...
# This item class contains all the menu choices.
item class EditNewDelete_CL is
Workout_MC Sleep_MC Meal_MC Weighin_MC Injury_MC
Treatment_MC Checkup_MC Ingredient_MC Recipe_MC Diary_MC
# This is the main window definition for the application.
# It contains the menu bar MainMenu_AB.
enabled visible color 26
primary graphical region Main_GR size 475 179
at position 83 112
in desktop
window size 475 179 at position 0 0
color 27 foreground
size border
title bar "Total Health"
system menu
horizontal scroll bar scroll by 6
vertical scroll bar scroll by 2
action bar MainMenu_AB
minimize button
maximize button
# This response block is called whenever an item in
# class EditNewDelete_CL is chosen.
response to item EditNewDelete_CL
copy parameter to Thread
copy "OPEN_" Thread to OpenAction
copy "FETCH_" Thread to FetchAction
copy "CLOSE_" Thread to CloseAction
copy Thread "ListAction" to ListAction
make GenericList_DB visible
change GenericList_DB text to Thread
action FetchThemAll
# Loops over FetchAction until there are no more items in the table.
action FetchThemAll is
clear GenericList_LB in GenericList_DB
action OpenAction
while(( Esqlca.Sqlcode != END_OF_DATA ) and
(Esqlca.Sqlcode != CURSOR_CLOSED ))
loop
action FetchAction
if(( Esqlca.Sqlcode != END_OF_DATA ) and
(Esqlca.Sqlcode != CURSOR_CLOSED )) then
action ListAction
end if
end loop
action CloseAction
# This action formats a single line for the WorkoutList dialog box.
# There is an action like this one for every type of list: Meals, Injuries, ...
action WorkoutListAction is
if( Description != "" and DateTime != "" ) then
copy Description " " DateTime to TS
add to GenericList_LB in GenericList_DB
insert TS
end if
# Response to the edit button in the single, generic list dialog box. It uses
# the variable Thread that we filled in earlier from parameter of menu item.
response to Edit_PB in GenericList_DB
change GenericList_DB text to ""
make GenericList_DB invisible
copy Thread "EditAction" to LoadEditDBAction
copy Thread "_DB" to DialogBoxName
action LoadEditDBAction
# This is the action that fills in the edit item dialog box.
action WorkoutEditAction is
make DialogBoxName visible # This brings up the dialog
change Description_EF in Workout_DB text to LastName
change DateTime_EF in Workout_DB text to DateTime
change Duration_EF in Workout_DB text to Systolic
change Intensity_EF in Workout_DB text to Diastolic
change Comments in Workout_DB text to LDL
"The initialization code for the workout list and workout item dialog boxes."
"Some of the control definitions have been left out for brevity."
"The workout list form."
form := Form
name: #WorkoutList
rect: {22 431 2043 862}
controller: controller.
form setInitialFocusTo: #OKButton.
cItem := controller add: #OKButton
class: FormButton
rect: {511 611 350 90}
options: {#Return #Tab #Up #Down #Backtab #Left #Right}
form: form
text: '~Edit'.
".... Other control definitions would be here ..."
cItem := controller add: #ListBox
class: FormLinkedTabList
rect: {49 41 1676 469}
options: {#Return #Tab #Backtab}
form: form.
cItem setSplitPositionTo: nil.
"The following 2 lines link the TabularListBox cItem to the"
"database table #WORKOUT."
tLink := TableLink newIdentifier: #WORKOUT attribute: #ALL.
controller setLinksTo: cItem link: tLink.
temp := AcceleratorTable new.
temp at: #AltC put: #cancelList.
temp at: #Altc put: #cancelList.
temp at: #AltD put: #deleteWorkout.
temp at: #Altd put: #deleteWorkout.
temp at: #AltE put: #editItem.
temp at: #Alte put: #editItem.
form setAcceleratorTableTo: temp.
"Here is the initialization code for the edit workout item dialog box. Note"
"there is a link for every entry field to a field in the database record."
form := Form
name: #Workout
rect: {135 7 1837 851}
controller: controller.
form setGridTo: false.
form setSnapTo: false.
form setXGridResTo: 82.
form setYGridResTo: 82.
temp := IdentityDictionary newEntries: 2.
temp at: #cancelButton1 put: #cancelWorkout.
temp at: #OKButton1 put: #saveWorkout.
form setReturnActionsTo: temp.
form setInitialFocusTo: #Description.
".... All the static control definitions would be here ..."
cItem := controller add: #Description
class: FormString
rect: {514 50 420 60}
options: {#HResize #Return #Tab #Backtab #Return
#Return #Return}
form: form.
cItem setMaximumLengthTo: 20.
tLink := TableLink newIdentifier: #WORKOUT attribute: #DESCRIPTION.
controller setUpdateLinksTo: cItem link: tLink.
cItem := controller add: #DateTime
class: FormString
rect: {514 135 420 60}
options: {#HResize #Return #Tab #Backtab
#Return #Return #Return}
form: form.
cItem setMaximumLengthTo: 26.
tLink := TableLink newIdentifier: #WORKOUT attribute: #DATETIME.
controller setUpdateLinksTo: cItem link: tLink.
cItem := controller add: #Location
class: FormString
rect: {514 220 420 60}
options: {#HResize #Return #Tab #Backtab
#Return #Return #Return}
form: form.
cItem setMaximumLengthTo: 20.
tLink := TableLink newIdentifier: #WORKOUT attribute: #LOCATION.
controller setUpdateLinksTo: cItem link: tLink.
cItem := controller add: #Duration
class: FormNumber
rect: {514 305 420 60}
options: {#Return #Tab #Backtab #Return
#Return #Return}
form: form.
tLink := TableLink newIdentifier: #WORKOUT attribute: #DURATION.
controller setUpdateLinksTo: cItem link: tLink.
cItem := controller add: #Intensity
class: FormNumber
rect: {514 386 420 60}
options: {#Return #Tab #Backtab #Return
#Return #Return}
form: form.
tLink := TableLink newIdentifier: #WORKOUT attribute: #INTENSITY.
controller setUpdateLinksTo: cItem link: tLink.
cItem := controller add: #Comments
class: FormString
rect: {514 469 420 60}
options: {#HResize #Return #Tab #Backtab
#Return #Return #Return}
form: form.
tLink := TableLink newIdentifier: #WORKOUT attribute: #COMMENTS.
controller setUpdateLinksTo: cItem link: tLink.
".... The rest of the dialog control definitions would be here ..."
WorkplaceObject subclass: #ExecWPO
instanceVariableNames: '
FileName
SessionName
'
classVariableNames: '
'
poolDictionaries: '
' !
!ExecWPO class methods !
!"End of class methods block"
!ExecWPO methods !
open
"Override the default open method which would open the form associated with"
"this WorkPlaceObject. This call starts an external application. Must call "
" setSessionName and setFileName before opening this object!"
SessionName startSessionProgram: FileName inputs: 'new'.
!"end open "
setFileName: newFileName
"Set the filename of the executable we're going to run"
FileName := newFileName.
!"end setFilename"
setSessionName: newSessionName
"Set the title this session will have when 'opened'"
SessionName := newSessionName.
!"end setSessionName"
!"End of method block"
Copyright © 1994, Dr. Dobb's Journal