Thin Client Framework (TCF) is a design, development, and deployment approach for easily and rapidly developing high-functioning, extendable, and responsive e-business clients in the Java language. TCF builds on the Model-View-Controller design pattern and raises it to the level of an application architecture. Through consistent implementation of an event-based communication model, TCF provides a highly pluggable, component-based structure for client-side application development.
TCF defines best practices for writing Java client applications, with emphasis on the separation of coding concerns and responsibilities. Its high degree of modularity provides for maximum reuse. Because it is compatible with multiple data, server, and network models, TCF is extremely flexible, enabling Java clients to be as thin, or as thick, as needed. Finally, TCF supports a formula-based approach to estimating development costs, and it enables parallel component development on the client while supporting concurrent development of clients and servers.
In this two-part series, we will provide a beginner's introduction to TCF. Part 1 of the series will familiarize you with the basic elements of the architecture. We'll explain the concepts behind TCF and describe the components of the architecture. We'll also provide a working example application, so you can see for yourself how the components are coded. In Part 2, we'll provide a more detailed discussion of TCF's design and implementation.
Developing distributed application clients involves many interrelated design considerations. Some of these considerations are illustrated in Figure 1. A successful thin-client development framework must address these considerations in a manner that is simple and straightforward.
Figure 1. Client-side design considerations

In the scope of end-to-end systems development, the mid-tier and back-end servers are generally the more difficult and time-consuming pieces. They, not the client, should consume the majority of your design effort. Figure 2 is one example of an end-to-end system design:
Figure 2. End-to-end system complexity

By providing a tested pattern for client-side development, TCF ensures that the most important aspects of the client are addressed, while greatly reducing the time spent on client-side design considerations.
One important design consideration is whether the client is to be thin or thick (also known as fat). A client is defined as thick or thin based on the amount of function (or business logic) it holds. Application clients can be implemented across a spectrum from purely thin to fully fat, and hybrids are not uncommon.
TCF applications are characterized by having application business logic split between the client and the server. Generally, in TCF applications, message sizes are small and efficient, resulting in good interactivity without the need for high-speed links. TCF apps also are known for loose coupling between the client and the server, allowing each side to be developed separately. The split functionality of TCF's design is most practical for highly distributed applications. Figure 3 shows a typical hybrid TCF configuration:
Figure 3. Hybrid thin-client configuration

Once you've decided that there will be some business logic on the client, you have to decide how to implement it. The two most common approaches are:
- Use some form of browser-activated scripting language, such as JavaScript/Dynamic HTML (DHTML)
- Write the client as an applet or application in a robust programming language such as the Java language
In this article, we'll be working with a Java-based client implementation. The Java language is the preferred (or required) implementation when:
- The application must accommodate users who are not connected to a network
- The application calls for a richer or more responsive GUI than DHTML can provide
- Client-side data caching is required or the client must be able to manipulate large amounts of data
- Client scripting is insufficient
- The server's session state is so large it must be distributed to the client side
- The application requirements state that it must be written in the Java language
Listing 1 shows a typical Java application coding style. See if you can figure out what's wrong with the code.
Listing 1. Typical Java application code
import java.util.*;
import com.xyz.*;
Customer customer = createCustomer();
:
Socket socket = createSocket();
customer = (Customer) socket.readObject();
String name = customer.getName();
TextField tf = new TextField();
Frame frame = new Frame();
tf.setText(name);
frame.add(tf);
LogFile log = createLogFile();
log.writeStatus("updated");
|
What we have here is the beginning of an unmanageable, thick client.
- Why? Because it employs multiple code responsibilities mixed
throughout the application.
- What's the fix? Separate the code by responsibilities.
The Model-View-Controller (MVC) pattern is a classic and very well understood design pattern. As shown in Figure 4, the MVC pattern can be implemented at numerous levels throughout an application. At the finest level, the MVC pattern determines how GUI controls work. At the middle level, it structures application components. At the broadest level, the MVC pattern partitions multiple tiers of the application, representing the popular three-tier application model. TCF is an example of the middle level of MVC use.
Figure 4. TCF's relationship to the MVC pattern

At this point, you should have a basic understanding of the Thin Client Framework's contribution to the distributed application development process; what a typical TCF client looks like; how not to code a distributed application client in Java code; and the underlying role of the MVC pattern in TCF. From here, we're ready to delve into the TCF architecture.
The entire TCF architecture is shown in Figure 5. As you can see, it's fairly small and easy to understand. The names in ovals represent major TCF interfaces or classes, while the names in rectangles or diamonds represent TCF support classes. Communication between components is handled by events, as shown in Figure 5. Event-based communication is a fundamental characteristic of TCF's architectural pattern.
Figure 5. The TCF architecture

The roles of the principle TCF components are:
- The ViewController interface defines a reusable
user interface component that is part of an overall
client application GUI.
ViewControlleris often implemented by an AWT (or Swing) component, such as aPanel. - The PlacementListener interface manages the
placement of
ViewControllers on the screen.PlacementListeneris often implemented by the application. - The ApplicationMediator interface defines the control
logic of an application.
- The Transporter interface selects the appropriate
destinations to process a
RequestEvent. - A Destination is an abstraction of any service the TCF client
needs.
- The TopListener interface is used to separate business- or
environment-specific responsibilities from other TCF componentry.
TopListeneris often implemented by the application. - A ViewEvent is an abstraction of a GUI event.
ViewEventis the mechanism for communication betweenViewControllers andViewListeners (typicallyApplicationMediators). - A RequestEvent is a lightweight transaction request.
RequestEventis the mechanism for communication betweenApplicationMediators andRequestListeners (typicallyTransporters). - PlacementEvent is the mechanism for communication between
ApplicationMediators andPlacementListeners (typically the application). - TopEvent is the mechanism for communication between
ApplicationMediators andTopListeners (typically the application).
In Figure 6, you can see the major TCF interfaces. Recall that the TCF partitions the development process into multiple coding concerns and responsibilities. Each TCF interface represents a different area of responsibility. Developers can become skilled in one or more interfaces, as appropriate to their role on the development team.
Figure 6. TCF's major interfaces/classes

TCF provides a default or abstract implementation of each of its interfaces. Thus, it provides a partial implementation that you can build on. Many of the common tasks required to use TCF are built into the default interface implementation. Events fired by the source interface implementation handle all interactions between interfaces.
Because the TCF architecture is component-based and event-driven, it is easy to develop and unit test each component of a TCF system in isolation. Once the events and their major, minor, and command values have been defined, the components can be tested through simple scaffolding code. Thus, definition of events between senders and receivers is the only strong tie between components.
Because the components in a TCF system are loosely coupled, they can be inserted and removed at runtime. Among other things, this enables easy support for flow tracing and data filtering/conversion.
Because it uses a loosely coupled, highly scalable development paradigm, TCF makes development sizing very straightforward. Once calibrated, as shown in Figure 7, simple multiplication-based sizing will suffice for most TCF projects.
Figure 7. Example component estimation

The above estimates are based on low-level design and code estimates, and the assumption that you are working with experienced Java programmers. The estimates shown are examples; your values will likely differ.
For the remainder of this article, we'll work with an example application to illustrate the concepts behind TCF. Example04 is a very simple customer information system that maintains a list of customers. The application is a panel. It can be wrapped in either a frame or an applet. The applet form is shown in the figures below. For the purposes of this article, we'll only show select parts of the application.
Once again, keep in mind that this article serves mainly as an overview; we'll go into greater detail in the second part of the series.
Example04 consists of the following components:
- Four
ViewControllers:VC1,VC2,VC3,StatusVC - An
ApplicationMediator:AM4 - A
Transporterand aDestination - Akey-value pair-based data model
- A main application (Example04)
- A
Codesinterface that defines symbolic constants
We'll discuss each of the components, with the exception of
the Codes interface, in the sections that follow.
A simple key-value pair (that is, Map) structure is used as
the application data model.
It is passed around the application and is updated as appropriate based on user input.
Listing 2 shows the two convenience methods to reset the information normally obtained
from the Login and Customer Information screens:
Listing 2. Data model class definition
public class Data extends Hashtable {
public static final String FAMILY = "Example04";
public void initCustomer() {
put(Codes.TITLE, Codes.TITLE);
put(Codes.FIRSTNAME, Codes.FIRSTNAME);
put(Codes.LASTNAME, Codes.LASTNAME);
put(Codes.FULLNAME, Codes.FIRSTNAME + " " +
Codes.LASTNAME);
put(Codes.WWW, Codes.WWW);
put(Codes.OFFICE, Codes.OFFICE);
put(Codes.PHONENUMBER, Codes.PHONENUMBER);
put(Codes.EMAIL, Codes.EMAIL);
}
public void initLogin() {
put(Codes.USERID, Codes.USERID);
put(Codes.PASSWORD, Codes.PASSWORD);
}
public Data() {
initLogin();
initCustomer();
}
}
|
Example04 has four view controllers:
VC1is the Login screenVC2is the Customer Selection screenVC3is the Customer Information screenStatusVCshows which of the previous threeViewControllers is currently active
The example application always shows two ViewControllers at once.
The StatusVC is always shown at the top of the screen, while one of the
other three is shown in the center of the screen. Figure 8 lets you view the Login screen:
Figure 8. VC1: The Login screen

Normal Java event processing is used to handle GUI events. Each
ViewController translates one or more AWT events
(for example, button presses) into a ViewEvent. Event data parameters
are passed as data objects.
In the following code samples, we'll look at the ViewEvents
for each ViewController. Each section of code is wrapped in a
xxxxButton_actionPerformed method, where xxxx is
the button name. To keep things brief (and simple) the wrappers are not
shown.
StatusVC displays the Java class name of current
ViewController.
StatusVC is shown on all screens but its buttons are only active when
VC2 is shown.
Save fires the SAVE ViewEvent:
// save customer records to a file fireViewEvent( new ViewEvent(this, ViewEvent.SAVE)); |
Load fires the LOAD ViewEvent:
// load customer records fireViewEvent( new ViewEvent(this, ViewEvent.LOAD)); |
VC1 inputs login values, typically a userid and password.
Its ViewEvents are shown below.
Login adds the input fields to the data model and fires
the LOGIN ViewEvent:
// grab textfields, update data model
Data d = this.data;
d.put(Codes.USERID, getNameField().getText());
d.put(Codes.PASSWORD, getPasswordField().getText());
setEnabled(false);
fireViewEvent(
new ViewEvent(this, ViewEvent.LOGIN, d));
|
Note that the ViewController is disabled.
This prevents it from accepting new button presses until the
ViewEvent has been completely processed.
The ViewEvent will be re-enabled by
the refresh() method once the event
has been processed.
Done fires the CANCEL ViewEvent:
setEnabled(false);
fireViewEvent( new ViewEvent(this, ViewEvent.CANCEL));
|
When login is complete, VC2 is shown, as illustrated in Figure 9:
Figure 9. Customer Selection screen

As you can see, VC2 presents a list of existing users,
along with several buttons to manage and update the list. Since the
processing of ViewEvents on this screen is very similar to VC1, we'll
only show the Edit event.
Edit creates or updates user information:
Data d = this.data;
d.put( Codes.FULLNAME,
getAccountList().getSelectedValue());
fireViewEvent( new ViewEvent(this, ViewEvent.DETAILS, d));
|
The selected user name is placed in the FULLNAME data model entry.
When either the Edit or the New button is clicked, VC3 is shown, as illustrated in Figure 10:
Figure 10. Customer information screen

VC3 is used to view or edit a customer entry. Its
view events are shown below.
OK grabs the data from the GUI, updates the model,
and fires an UPDATE ViewEvent:
Data d = this.data(); d.initCustomer(); String fn, ln; d.put(Codes.FIRSTNAME, fn = getFirstNameField().getText()); d.put(Codes.LASTNAME, ln = getLastNameField().getText()); d.put(Codes.FULLNAME, fn + " " + ln); d.put(Codes.TITLE, getTitleField().getText()); d.put(Codes.PHONENUMBER, getPhoneField().getText()); d.put(Codes.WWW, getWwwField().getText()); d.put(Codes.EMAIL, geteMailField().getText()); d.put(Codes.OFFICE, getOfficeField().getText()); fireViewEvent( new ViewEvent(this, ViewEvent.UPDATE, d) ); |
Cancel fires a DONE ViewEvent:
fireViewEvent( new ViewEvent(this, ViewEvent.DONE) ); |
The ApplicationMediator processes the ViewEvents
generated by the various ViewControllers.
It sequences the ViewControllers and generates
RequestEvents to preform the actions
requested by the various ViewControllers.
We'll look at how the ApplicationMediator handles
two typical event requests below.
The ApplicationMediator is initialized as shown below.
This initialization sequence creates the four ViewControllers
and issues a placement request, making VC1 the active ViewController.
Listing 3 shows how the ApplicationMediator handles these requests:
Listing 3. ApplicationMediator request handling
public void init() {
super.init();
String[] classes = {
"com.ibm.jtc.examples.ex04.VC1",
"com.ibm.jtc.examples.ex04.VC2",
"com.ibm.jtc.examples.ex04.VC3",
"com.ibm.jtc.examples.ex04.StatuVC"};
try { initViewControllers(classes); }
catch (Exception e) { return; }
firePlacementEvent(
new PlacementEvent(this, getVC(0), PlacementEvent.ADD, 0) );
}
|
Dispatch ViewEvents to handlers
The ApplicationMediator can process requests several different ways.
Two of the more popular solutions, processing by event source or processing by event code,
are shown below (note that both examples use synchronous event dispatching).
Option 1: Process ViewEvents by the event source
In Listing 4, the method getVC() returns the
ViewControllers, as listed in the classes
variable of the above init method.
Listing 4. Processing by event source
public void processViewEvent(ViewEvent ve) {
RequestEvent re = new RequestEvent(this, MY_FAMILY);
ViewController vc = (ViewController)ve.getSource();
if (vc == getVC(0)) doVC1(re, ve);
else if (vc == getVC(1)) doVC2(re, ve);
else if (vc == getVC(2)) doVC3(re, ve);
else if (vc == getVC(3)) doStatusVC(re, ve);
}
|
Option 2. Process ViewEvents by code values
As Listing 5 shows, the code values are major and minor. The detailed
behaviors shown in Listing 5 are typically contained in the above doxxx methods.
Listing 5. Processing by code values
public void processViewEvent(ViewEvent ve) {
RequestEvent re = new RequestEvent(this, MY_FAMILY);
int major = ve.getMajor(), minor = ve.getMinor();
try {
switch ( major ) { // process major code
case ViewEvent.LOGIN :
re.setCommand(Codes.LOGIN); // do LOGIN
re.setData(ve.getData());
fireRequestEvent(re);
re.setCommand(Codes.GETNAMES); // do GETNAMES
re.setData(ve.getData());
fireRequestEvent(re);
firePlacementEvent( // remove VC1
new PlacementEvent(this, getVC(0),
PlacementEvent.REMOVE) );
firePlacementEvent( // add VC2
new PlacementEvent(this, getVC(1),
PlacementEvent.ADD) );
getVC(3).refresh(getVC(1).toString()); // update status
getVC(1).refresh(re.getData()); // update VC2 fields
getVC(3).setEnabled(true); // enable user actions
break;
case ViewEvent.DETAILS :
re.setCommand(Codes.GETDETAILS); // do DETAILS
re.setData(ve.getData());
fireRequestEvent(re);
firePlacementEvent( // remove VC2
new PlacementEvent(this, getVC(1),
PlacementEvent.REMOVE) );
firePlacementEvent( // add VC3
new PlacementEvent(this, getVC(2),
PlacementEvent.ADD) );
getVC(3).refresh(getVC(2).toString()); // update status
getVC(2).refresh(re.getData());
break;
case ViewEvent.CANCEL :
fireTopEvent( // end execution
new TopEvent(this, TopEvent.EXIT) );
break;
case ViewEvent.REFRESH :
re.setCommand(Codes.GETNAMES); // do GETNAMES
fireRequestEvent(re);
getVC(1).refresh(re.getData()); // update VC2 fields
break;
case ViewEvent.DELETE :
re.setCommand(Codes.DELETE); // do DELETE
re.setData(ve.getData());
fireRequestEvent(re);
re.setCommand(Codes.GETNAMES); // do GETNAMES
re.setData(ve.getData());
fireRequestEvent(re);
getVC(1).refresh(re.getData()); // update VC2 fields
break;
case ViewEvent.UPDATE :
re.setCommand(Codes.UPDATE); // do UPDATE
re.setData(ve.getData());
fireRequestEvent(re);
refresh(re.getData()); // update all VC fields
firePlacementEvent( // remove VC3
new PlacementEvent(this, getVC(2),
PlacementEvent.REMOVE) );
firePlacementEvent( // add VC2
new PlacementEvent(this, getVC(1),
PlacementEvent.ADD) );
getVC(3).refresh(getVC(1).toString()); // update status
break;
case ViewEvent.DONE :
firePlacementEvent( // remove VC3
new PlacementEvent(this, getVC(2),
PlacementEvent.REMOVE) );
firePlacementEvent( // add VC2
new PlacementEvent(this, getVC(1),
PlacementEvent.ADD) );
getVC(3).refresh(getVC(1).toString()); // update status
break;
}
}
catch(Exception e) {
e.printStackTrace();
return;
}
}
|
Listing 5 demonstrates how a typical ApplicationMediator is coded: first it decodes the major and minor values, then it processes the request. Request
processing typically consists of creating and firing one or more
RequestEvents to process the input action,
refreshing the components with the results of the event, and changing
to a new screen. To switch screens, the ApplicationMediator instructs the PlacementListener to remove the current screen
and add a new one.
Acting indirectly through the Transporter,
Destinations
implement RequestEvents fired from the
ApplicationMediator. Note that
RequestEvents go from the
ApplicationMediator through the
Transporter and then on to
the Destination.
The Transporter acts as a router to select one or more
Destinations to process the
RequestEvent.
Many implementations of the simple Destination
provided with Example04 are possible. For simplicity, this example uses a
Destination that decodes command-request values to routines
that provide access to canned data. The decoding logic is shown in Listing 6:
Listing 6. Decoding logic for an example Destination
String cmd = request.getCommand();
if (cmd.equals(Codes.GETNAMES))
doGetNames(request);
else if (cmd.equals(Codes.GETDETAILS))
doGetDetails(request);
else if (cmd.equals(Codes.UPDATE))
doUpdate(request);
else if (cmd.equals(Codes.NEWRECORD))
doNewRecord(request);
else if (cmd.equals(Codes.DELETE))
doDelete(request);
else if (cmd.equals(Codes.LOGIN))
doLogin(request);
else if (cmd.equals(Codes.NEWLOGIN))
doNewLogin(request);
else if (cmd.equals(Codes.SAVE))
doSaveData();
else if (cmd.equals(Codes.LOAD))
doLoadData();
|
We'll close with a look at the example application. From the simple code samples below, you should be able to see how the various described components of the TCF architecture work together in a distributed application.
The Example04 application class is shown in Listing 7:
Listing 7. Public class Example04
public class Example04 extends JPanel
implements JTC, PlacementListener, TopListener {
LocalDestination dest = null;
AM1 am = null;
Transporter trans = null;
Data data = null;
List jtcs = new Vector(); // all JTC implementers
String filename = null";
JPanel cp = null;
public Example04 app = null; // convenient ref to me
:
}
|
Listing 8 shows the application's main() method:
Listing 8. Example04's main() method
public static void main(java.lang.String[] args) {
new Example04("Example04",
args.length > 0 ? args[0]
: "example04.db");
}
|
Listing 9 shows the application's constructor method:
Listing 9. Example04's constructor method
public Example04(String title, String filename) {
// setup overall GUI
this.filename = filename;
JFrame frame = new JFrame(title);
frame.addWindowListener(this); // not shown
cp = (JPanel)frame.getContentPane();
cp.setLayout(new BorderLayout());
cp.add(this, BorderLayout.CENTER);
frame.pack();
frame.setSize(525, 450);
frame.setVisible(true);
app = this; // remember for JTC lists
// setup Transporter and Destination mapping
trans = new DefaultTransporter();
jtcs.add(trans);
dest = new LocalDestination();
jtcs.add(dest);
trans.addRequestListener(Data.FAMILY, dest);
// setup ApplicationMediator
am = new AM4();
jtcs.add(am);
am.addTopListener(this);
am.addPlacementListener(this);
am.addRequestListener(trans);
//indirectly cause a PlacementEvent
am.init();
am.refresh(data = new Data()); // reset the data model
}
|
And the application's PlacementListener, which processes PlacementEvents, is shown in Listing 10:
Listing 10. Placement support for Example04
// the cp variable is the JFrame's ContentPane
public void placementEventPerformed(PlacementEvent pe) {
ViewController vc = (ViewController)
pe.getViewController();
final Component c = pe.getComponent();
if (vc instanceof StatusVC) {
cp.add(c, BorderLayout.NORTH);
}
else {
switch (pe.getMajor()) {
case PlacementEvent.ADD :
cp.add(c, BorderLayout.CENTER);
validate();
repaint();
break;
case PlacementEvent.REMOVE :
cp.remove(c);
break;
}
}
}
|
Although it is extremely simple, Example04 demonstrates each of the major
components and interactions of a TCF-based application. Far more complex
applications are possible. For example, multiple ApplicationMediators
and destinations can be used. A TopListener and
TopDestination
could provide access to system services. A single PlacementListener
could be deployed to control the application's screen real estate.
By combining several simple TCF applications, we can easily create a more
complex application, as shown in Figure 11:
Figure 11. A more complex application

In this first part of the beginner's introduction to TCF, you've learned about the conceptual underpinnings of TCF, how its architecture is laid out, and how each of the major components of the architecture works. In Part 2, we'll talk more about each of these components, but with a focus on the framework's design and programming model.
- Find hundreds of articles about every aspect of Java
programming in the developerWorks Java technology
zone.
- See a complete listing of free Java technology tutorials from developerWorks on the developerWorks Java technology
tutorials page.
Dr. Peter Bahrs is an IBM Distinguished Engineer and IT Architect in the IBM Software Services organization. Dr. Bahrs specializes in large e-business systems development in the financial services industries. He holds several patents and has spoken at industry conferences such as JavaOne. Dr. Bahrs is currently on assignment to IBM Zurich, Switzerland. He can be reached at tcfhelp@us.ibm.com.

Dr. Barry Feigenbaum is a member of the IBM Worldwide Accessibility Center, where he serves as a member of a team that helps IBM make its products accessible to people with disabilities. Dr. Feigenbaum has published several books and articles, holds several patents, and has spoken at industry conferences such as JavaOne. He serves as an Adjunct Assistant Professor of Computer Science at the University of Texas, Austin. You can contact Dr. Feigenbaum at feigenba@us.ibm.com.




