Overview
Gumby is a XUL-like framework that allows creating Swing GUIs
through XML. Gumby uses Java's introspection capabilities and the
Confix
API to create objects from XML tags. Gumby does not
impose an abstract markup language independent from the underlying UI
implementation; Gumby IS Swing with an XML touch and some cool features
such as scripting and databinding that allow quickly building forms
such as this:.
|
There are mutiple examples that come with the source distribution. Having
a look at them will greatly help. The XML configuration files are in
the etc/test directory, and the corresponding
Java source files are in the java/intg directory.
|
Goals
Gumby's first goal is to elimitate the hassles that consume so much
time when building UIs in Java: the tedious code-compile-test cycle, the
harcodedness and verbose syntax (casting, static typing), the wiring Java code
that links UI components with business logic and that often becomes spaghettified...
Another goal is to eliminate the mandatory browser-based webapp - at
least in some scenarios: despite the myriad of web frameworks that are
attemping to address the problems stemming from building web applications,
none can really overcome the HTTP protocol's limitations when it comes
to managing complex user interfaces.
Features
Here are Gumby's features, at a glance:
- XML markup that is tightly bound to Swing: you use XML elements
and attributes to instantiate objects and call methods on these objects.
- As part of the toolkit, a couple of classes and interfaces that
helps structure applications and can easily be integrated with existing
lightweight containers.
- Scripting integration through
BeanShell
and Pnuts
- Support for databinding: build forms in a snap through a clean
MVC implementation.
- Some goodies such as the TablePanel
class that puts an HTML-ish face on the dreaded
GridBagLayout.
Non-Features
Here's what Gumby IS NOT:
- A generic XUL markup that allows transparently replacing the
underlying UI toolkit: we simply don't believe in this approach,
Swing is too different from SWT ('cause we know you want SWT, don't you?).
Over-generalizing would mean loosing the advantages and features
of the underlying toolkit. Plus, we think Swing can
do the job,
so no need to plan for alternatives. Furthermore, I work with Eclipe+Linux
on a daily basis, and let me tell you SWT does not perform better on Linux than Swing.
Too bad Netbeans does not yet have what I want...
- A kit that creates UIs with XML and does just that: what about
integration with business logic? what about databinding?
Example
Create the XML markup
The following corresponds to the Swing Tutorial's GridBagLayout
example.
<gumby:TablePanel xmlns:swing="java:swing"
xmlns:gumby="gumby:swing">
<tr>
<td weight="0.5">
<swing:JButton>
<text>Button 1</text>
</swing:JButton>
</td>
<td weight="0.5">
<swing:JButton>
<text>Button 2</text>
</swing:JButton>
</td>
<td weight="0.5">
<swing:JButton>
<text>Button 3</text>
</swing:JButton>
</td>
</tr>
<tr>
<td colspan="3" weight="0.0" pady="40">
<swing:JButton>
<text>Long-Named Button 4</text>
</swing:JButton>
</td>
</tr>
<tr weight="1.0">
<td></td>
<td colspan="2"
weight="0.0"
pady="0"
valign="bottom"
align="right">
<swing:Insets top="10"
left="0"
bottom="0"
right="0"/>
<swing:JButton>
<text>Button 5</text>
</swing:JButton>
</td>
</tr>
</gumby:TablePanel>
|
Instantiate
...
import java.io.File;
import javax.swing.JFrame;
import javax.swing.JPanel;
import org.sapia.gumby.RenderContextFactory;
...
public static void main(String[] args) {
try {
JFrame.setDefaultLookAndFeelDecorated(true);
JFrame frame = new JFrame(name);
JPanel panel =
(JPanel)RenderContextFactory
.newInstance()
.render(new File("etc/test/tablepanel.xml"));
frame.setContentPane(panel);
frame.pack();
frame.setVisible(true);
} catch(Exception e) {
e.printStackTrace();
}
}
...
|
Gumby From 30,000 Feet
The Example Explained
As the example above shows, Gumby creates objects corresponding to an XML
description file. As was mentioned, Gumby uses
Confix to achieve
such a task. Thus, your XML should respect a certain format (the one expected
by Confix). More specifically:
XML elements that are associated to a namespace (through a prefix) have
their corresponding objects created by pre-registered "object factories" - we
will show you in a minute how this is done.
XML elements that are not associated to a namespace (with no prefix) are
always nested within the ones that are "namespaced" (or, at least, the first
non-namespaced element in a hierarchy always as a namespaced element as a parent.
The non-namespaced elements correspond to the creator, adder or setter methods
that have the corresponding name on
the parent object (corresponding to their immediate ancestor's XML element) - more on that
later, but you should read the Confix
doc really,
this is not a plug.
Architecture
Internally, there's not much to it: everything is built around Confix.
From an application programming point of view though, there are a few
classes and interfaces to be familiar with:
The GuiEnv interface models
an "environment"; it represents a mapping of scoped values - an instance
of the interface holds values in terms of Scopes.
In addition, as the Javadoc explains, implementations of the interface
are expected to respect a delegation model where a given instance might
have another instance as a parent, and so on. When looking up a
value, an implementation will search its own scopes before delegating
to the parent. The interface is intended to allow integration between
Gumby and your applications.
A Scope is a kind of reduced
java.util.Map; in fact, an implementation
of the Scope interface that is part of
Gumby relies on the HashMap class. As
you might guess, scopes are meant to be held in GuiEnv
instances.
The RenderContext class is what
you use to "render" Gumby XML configurations - that is, create an object
(Swing component) from a Gumby configuration. A RenderContext
encapsulates a GuiEnv. If you eventually implement
your own Gumby tags, they can have access to their context by
implementing the ContextAware interface. Then,
your tags can have access to the "environment", and lookup arbitratry,
application-specific objects that are made available through that environment
- this is one way to integrate Gumby with your application.
A RenderContext context can have another context
instance as a child; such a child as its own environment, but in addition
inherits the one from its parent. This has been introduced so that
specific sections of a UI can have their own protected area, without
loosing touch with the global environment.
The RenderContextFactory is, well,
a factory of RenderContext. One good strategy
is to create the first RenderContext with the
factory, and then create other contexts with that first context instance - the
other contexts thus become children of that first context, which holds global values
that all sub-contexts can potentially access.
The View class is a special kind of
Scope: it corresponds to the view in MVC. A
view holds an arbitrary object that represents the model in MVC, as well as
Bindings (see next) that associate the model (or parts of it) to
the view (more on MVC with Gumby later on).
The Binding
interface specifies callbacks that are use to synchronize a model with its view.
The callbacks correspond to the different events that may occur in the model/view association.
The code that handles these callbacks (part of which may be generalized, part of which may
be application specific) consist of the controller role in the MVC pattern.
Gumby and XML
Tags in General
Gumby allows using XML to instantiate objects. Really, that's what it is. As was mentioned,
the Confix API is used to fullfill that task. The question is: how does Gumby create objects from
given tags - i.e.: how does the tag-to-object mapping works? Simple: internally, Gumby in fact maps XML
namespace prefixes to
ObjectFactory instances. When looking at the example further above, you sometimes see
the gumby prefix, and sometimes the
swing prefix. Both of these prefixes have been pre-registered with the
framework (that is, they are hard-coded). But nothing stops you from creating your own tags and configure
their so called "object definitions" in a configuration file that you can load within Gumby; your
definitions could be map to the prefix of your choosing. Thus, instantiating the objects corresponding to your
definitions could later be done through XML, in the following way: myPrefix:myTag.
|
Gumby comes bundled with default tags. They are documented in the tag reference.
|
Now, you might ask, how exactly are such tags configured? Default Gumby tags are kept in the
gumby_defs.xml file that is Gumby's jar, under the org.sapia.gumby
package. The file is loaded as part of a static initializer when you first use the RenderContextFactory.
An excerpt of the file is given below:
<gumby:config xmlns:gumby="gumby:swing">
<gumby:namespace prefix="gumby">
<def class="org.sapia.gumby.widgets.Icon"
name="Icon" />
<def class="org.sapia.gumby.tags.Import"
name="Import" />
<def class="org.sapia.gumby.tags.Constant"
name="Constant" />
<def class="org.sapia.gumby.tags.Register"
name="Register" />
<def class="org.sapia.gumby.tags.Ref"
name="Ref" />
...
</gumby:namespace>
</gumby:config>
|
Thus, similarly as above, configuring your own tags is straightforward:
<gumby:config xmlns:gumby="gumby:swing">
<gumby:namespace prefix="myPrefix">
<def class="org.foobar.MyTag" name="myTag" />
...
</gumby:namespace>
</gumby:config>
|
Then you would simply use your tag in XML as follows:
<gumby:config xmlns:myPrefix="my:namespace"
xmlns:swing="java:swing">
<swing:JPanel>
<component>
<myPrefix:myTag someAttribute="someValue"/>
</component>
</swing:JPanel>
...
</gumby:config>
|
Of course, it would be tedious to have to open Gumby's jar each time you
need to configure your own object definitions. Thus, new definitions can be
loaded through the RenderContextFactory, using its
loadDefinitions() method. When you load your
definitions this way, they become available to all RenderContext
instances that have been created with the factory.
If the above scope is too large (if you want to limit visibility of some definitions
to some given parts of your application), then you might use the
loadDefinitions() method of the RenderContext
from which these definitions should be visible - the delegation model explained earlier on will then
come into play.
Swing Tags
For all tags bound to the swing prefix, Gumby internally
uses a special object factory implementation. For any XML element passed to it, it will look within
its registered definitions if a tag has been defined; if not, it will try to load the class under
the javax.swing package (not child packages of that one)
that has the name corresponding to the XML element passed to it, and create an instance of it through
Java reflection.
Of course, the above only works for classes that have a no-args constructor. For creating objects
of classes that have no such constructor, a New tag is provided.
Gumby Tags
In addition to Swing widgets that can be loaded by using the proper XML tags,
Gumby comes built-in with specialized tags for different purposes. The example seen further
above demonstrates the use of the gumby:TablePanel tag - which
creates a TablePanel instance.
Other tags are provided with Gumby to facilate handling views, databinding, and so on.
Using Gumby
Using Gumby can be summed up as follows:
- Configure the XML corresponding to the Swing component you want to use.
- Create a RenderContext
- Set up the context with appropriate (app-specific) environment parameters.
- "Render" your XML using the context.
- Use the rendered object (usually a Swing component) in any way you please.
Normally, you render Swing components that you add to some container (a
JFrame, JPanel, etc. ).
The container might also have been created with Confix, or not. It's up to you;
that's the beauty of it.
Scripting
To accelerate development, Swing supports two scripting languages: Beanshell and
Pnut (the latter is an interesting one that sports great performance). A convenient
use of scripts is to implement listeners, such as java.awt.event.ActionListeners.
Implementing them in Java can be a tedious task, since they mostly serve as wiring widgets with backend code
(they have a role to play in MVC, as we'll see later on). The example below demonstrates how to add
action listener to a button:
...
<swing:JButton text="Click !!!">
<actionCommand>click</actionCommand>
<actionListener>
<gumby:Expr>
import javax.swing.JOptionPane;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
new ActionListener(){
public void actionPerformed(ActionEvent e){
frame =
RenderContext
.getEnv().acquire("Frame",
"application");
JOptionPane.showMessageDialog(
frame,
"Action command: " +
e.getActionCommand()
);
}
}
</gumby:Expr>
</actionListener>
</swing:JButton>
...
|
If you look at the example above, we use the gumby:Expr
tag assign a listener to our button. This built-in tag is used to evaluate expressions; an expression
is a script that evaluates to an object. In the example, the returned object will be an ActionListener.
In the actionPerformed() method, you can see that the scripts uses a
RenderContext; as you might guess, this variable corresponds to the
RenderContext instance that was used to render the XML of which the script is part. In the
above case, the script is assumed to be in Beanshell; to indicate that you use Pnuts, use the following notation:
...
<gumby:Expr lang="pnuts">
...
</gumby:Expr>
...
|
The above shows how to access the environment of the script's context. In this case, the script
acquires the Swing frame of the application - from the "application" scope. That is exactly why scopes exist:
to provide the snippets of code that make up your application with a handle to the context in which they
evolve. Scopes are further dealt with next.
Scopes
Scopes are act as isolated maps of values. Multiple hosts can coexist, and scopes have no kwownledge of
each other. Scopes are bound to the environment - a GuiEnv instance. Each scope
has a given name in that environment, therefore one must generally indicate in which scope to lookup in order
to retrieve a given value, or in which scope to bind a given value, using the name of the target scope.
Since Scope is an interface, it can be implemented in all sorts of ways. One could
implement a scope over JNDI's Context interface, and so on.
Thus, you use the environmenent to make "external" objects available to your Swing application, and you categorize
these external objects by scope within the environment, in order to avoid naming conflicts, and also because
environments (GuiEnv instances) in Gumby are hierarchical: as a consequence, scopes can therefore
be present at different levels: you could decide to create an "application" scope as part of the environment of the
root context; all children of that context would therefore see the "application" scope. The following shows how this
is done:
...
RenderContext root =
RenderContextFactory.newInstance();
root.getEnv().put(
"someGlobalObject",
someGlobalObj,
"application"
);
// menuBarContext will see the "application" scope
RenderContext menuBarContext =
root.newChildInstance();
jFrame.getContentPane().setJMenuBar(
(JMenuBar)menuBarContext.
render("foo/bar/myApp/menubar.xml"));
...
|
To add a custom scope implementation to the environment:
...
RenderContext root =
RenderContextFactory.newInstance();
root.getEnv()
.addScope("someCustomScope", someScopeImpl);
...
|
One convenient usage of scopes is to bind arbitrary objects to them from within the XML configuration,
or to retrieve objects from them - also from within the config, potentially. Gumby provides
the Register and Ref tags to do this,
respectively. For example, consider the following snippet:
...
RenderContext root =
RenderContextFactory.newInstance();
root.getEnv().put("someActionListener",
new someActionListener(),
"listeners");
// here render and use
...
|
And then:
...
<swing:JButton text="Click once more!!!">
<actionCommand>click</actionCommand>
<actionListener>
<gumby:Ref id="someActionListener:listeners" />
</actionListener>
</swing:JButton>
...
|
You get it? the Ref tag allows retrieving any object
from any arbitrary scope from within the XML configuration. In the above case, we
retrieve some global action listener that can thus be reused for different Swing components. The
tag expects an ID attribute whose content will have the following format: id:scope.
The scope part is optional; if omitted, the environment instance will search through all existing scopes - in
the order in which they were added.
Now, imagine that you want to bind a given Swing widget in order to retrieve it and manipulate
it from Java:
...
<swing:JButton text="Click once more!!!">
<actionCommand>click</actionCommand>
<gumby:Register id="someButton:widgets" />
</swing:JButton>
...
|
And then:
...
RenderContext root =
RenderContextFactory.newInstance();
JPanel panel = (JPanel)root
.render("some/resource.xml");
JButton button = (JPanel)root
.get("someButton", "widgets");
button.addActionListener(new SomeActionListener());
...
|
As you can see, the Register tag binds the
button to the specified scope (from the XML configuration). That button
can be retrieved and manipulated from Java, by using the appropriate identifier
and scope.
Mine you, you can put any object you want in a scope. Gumby makes no assumptions
as to what type of objects are in scopes. It is all up to you. But very often,
what you'll put in a scope are widgets that correspond to a view; that's part
of Gumby MVC, which we'll see next.
MVC
Model and View
Gumby offers you a base on which to build UIs that follow the MVC
pattern. We have explained scopes in the previous section; there is
a special kind of scope implementation in Gumby, which is the
View class. The class, in addition
to the methods required by the interface it implements, offers other
ones that deal with handling models and so-called "bindings". The class'
signature is as follows:
...
public synchronized void setModel(Object model)
public synchronized Object getModel();
public synchronized void addBinding(Binding binding);
public synchronized void removeBinding(String id);
public synchronized void fireUpdated(String id);
public synchronized void fireUpdated();
public synchronized void fireUpdateModel(String id);
public synchronized void fireUpdateModel();
...
|
All the methods above have been added for the context of MVC. So concretely,
what is a View? A View
holds a model (an arbitrary object) and a collection of
Bindings. Each binding must provide an identifier
that should be unique in the context of a given view. A binding handles
synchonization between the model (or parts of the model) and the user interface
(i.e.: the widgets that are part of the view) - we will see later on
where widgets come into play.
The model of a view has a set of events that can occur during its lifetime. The
framework categorizes these events as followings:
- The model is first bound (associated) to the view: this occurs when no
model is currently bound, and the setModel()
method is called on the view with the actual model object being passed in.
When that occurs, the view calls the onBound()
method on all the bindings it holds. At that point, bindings should initialize
the widgets they manage with the corresponding data in the model.
- The current model is replaced by another instance: this occurs when
a model is currently bound, and the setModel()
method is called. The view then calls the onChanged()
method on all the bindings it holds. Bindings should then update
the widgets they manage with the corresponding data in the new model.
- The current model has its state modified, but not in the context of the view -
for example, a background thread could synchronize the model with a database
periodically, even as the user is editing it. Of course, it is up to applications
to detect such a condition and notify the view accordingly, so that the
widgets can be updated to reflect the current state of the model. There are
two fireUpdate() methods for this: one that will
internally call onUpdated() on all the bindings,
and one that will invoke that callback only on the binding whose identifier
was given.
- Another event that can occur is when the user has entered some
data in the UI, and then performs an operation that tells the UI that
the model should be updated with the data that the widgets hold. This is typical
in the case of forms, where an "OK" or "Submit" button is provided. In
Swing parlance, an ActionListener would be
responsible for detecting the button click and updating the model. In Gumby,
the listener would actually call the fireUpdateModel()
method. Internally, the method calls updateModel()
on all the registered bindings. Bindings are then responsible for updating their
part of the model with the data held by the UI widgets they handle. In this
scenario, listeners thus delegate the bulk of their work to bindings - which
we delve further into in the next section.
Bindings as Controllers, and the Rest
As explained in the previous section,
Bindings
handle synchronizing the state of the model with the state of the UI - or, more precisely,
with the state of widgets in the UI. Bindings are thus the "C" in MVC (or at least
part of it). On their own, bindings are passive; that is: they can't detect
what happens to the model; and they can't detect what happens to the UI. To that
end, your Swing listeners and the rest of your application code come into play - your
code has to notify the view, as explained in the previous section.
At this point, what you're probably interested in is how, concretely, all
of this works. Here are the main steps - we'll take the example of a
data entry form:
- You design your form (writing the XML config, running your usual test
to adjust your UI).
- Next, you have implement your model.
- Then you implement your bindings.
- In your XML, you make sure that all widgets that will hold data intended
for the model are registered with a scope (you use the Register)
tag to do so.
- Next, you implement the code that will actually relate the whole thing:
- You create a RenderContext;
- you create a view in the context: you call createView(String) on
the context; this method internally registers a View instance
with it. In fact to Gumby, remember that views are scopes like any other. So the
method actually registers the view as a scope, using the parameter that you pass in as the
scope name. Now, this is a very important step: the name of the scope here should correspond
to the one under which you have registered your widgets in the XML configuration. This actually
how your bindings will be able to retrieve their widgets. The createView()
returns a View instance that you use in the next steps.
- You register your bindings with the view;
- you render the XML;
- you create an instance of your model and set it on the view.
So here's how it looks:
The XML
...
<gumby:TablePanel xmlns:swing="java:swing"
xmlns:gumby="gumby:swing">
<tr>
<td weight="0.5">
<swing:Insets top="10"
left="10"
bottom="10"
right="0"/>
<swing:JLabel>
<text>User Account</text>
</swing:JLabel>
</td>
</tr>
<tr>
<td weight="0.5">
<swing:Insets top="3"
left="30"
bottom="3"
right="5"/>
<swing:JLabel>
<text>First Name: </text>
</swing:JLabel>
</td>
<td weight="0.5">
<swing:Insets top="3"
left="0"
bottom="3"
right="0"/>
<swing:JTextField columns="15">
<! -- *** Registering widgets with
'forms/UserAccount' scope;
that is our view!!! *** -- >
<gumby:Register
id="firstName:forms/UserAccount" />
</swing:JTextField>
</td>
</tr>
<tr>
<td weight="0.5">
<swing:Insets top="3"
left="30"
bottom="3"
right="5"/>
<swing:JLabel>
<text>Last Name: </text>
</swing:JLabel>
</td>
<td weight="0.5">
<swing:Insets top="3"
left="0"
bottom="3"
right="0"/>
<swing:JTextField columns="15">
<gumby:Register
id="lastName:forms/UserAccount" />
</swing:JTextField>
</td>
</tr>
<!-- ========== Bindings ========== -->
<gumby:View scope="forms/UserAccount">
<binding>
<gumby:Expr>
import org.sapia.gumby.view.Binding;
import org.sapia.gumby.view.View;
new Binding(){
public String getId(){
return "UserAccount";
}
public void onBound(View v,
Object model){
firstName = v.get("firstName");
lastName = v.get("lastName");
firstName.setText(model.getFirstName());
lastName.setText(model.getLastName());
}
public void onUpdated(View v,
Object model){
onBound(v, model);
}
public void onChanged(View v,
Object model){
onBound(v, model);
}
public void updateModel(View v,
Object model){
firstName = v.get("firstName");
lastName = v.get("lastName");
model.setFirstName(firstName.getText());
model.setLastName(lastName.getText());
}
};
</gumby:Expr>
</binding>
</gumby:View>
</gumby:TablePanel>
...
|
|
We could have implemented our bindings in Java, register them with the
view programmatically, from Java also. But we wanted to demonstrate
the use of the Gumby View tag. As can
be seen, the tag allows registering bindings with the view straight
from the configuration (here, we use Gumby's scripting feature to
dynamically create a Binding instance).
|
The Model
package org.foobar;
public class User{
private String firstName, lastName;
public String getFirstName(){
return firstName;
}
public void setFirstName(String firstName){
this.firstName = firstName;
}
public String getLastName(){
return lastName;
}
public void setLastName(String lastName){
this.lastName = lastName;
}
}
|
Putting it together
...
public static void main(String[] args) {
try {
JFrame frame = new JFrame();
RenderContext ctx =
RenderContextFactory.newInstance();
ctx.getEnv().put("Frame", frame, "application");
View userAccountForm =
ctx.createView("forms/UserAccount");
JPanel panel =
(JPanel)ctx.render(
new File("path/to/config.xml"));
frame.getContentPane()
.setLayout(new FlowLayout(FlowLayout.LEFT));
frame.getContentPane().add(panel);
frame.setSize(400, 400);
frame.setVisible(true);
User user = new User();
user.setFirstName("John");
userAccountForm.setModel(user);
} catch(Exception e) {
e.printStackTrace();
}
}
...
|
| Note that the View class gives you
access to the RenderContext; therefore,
bindings have access to the environment. |
Databinding with JXPath
Hard-coding your own bindings for handling JTextFields
and other such conventional widgets could be tedious. That's why Gumby comes built in with
binding implementations that rely on JXPath. Here's how our example
above could be adjusted:
...
<gumby:View scope="forms/UserAccount">
<binding>
<gumby:BindText id="firstName"
attribute="firstName" />
</binding>
<binding>
<gumby:BindText id="lastName"
attribute="lastName" />
</binding>
</gumby:View>
...
|
In the above BindText elements, attribute
attributes consist of a JXPath expression that should evaluate to a getter of the model (or of a sub-object
thereof). The id attribute identifies the widget to which the tag is related.
|
There are other reusable bindings that come with Gumby and handle JLists,
JComboBoxes, etc. See the tag reference manual for more
details.
|
Conclusion
So that's it. For further details, having a look at the examples
that come with the source distribution is certainly a good idea. The
tag reference manual is also a logical stop.
|