Recommended: Sing it, brah! 5 fabulous songs for developers
JW's Top 5
Java Tutor is my platform for teaching about Java 7+ and JavaFX 2.0+, mainly via programming projects.
| Note: Data binding can be classified as unidirectional or bidirectional. With unidirectional data binding, data source B is bound to data source A, but A is not bound to B. Changes made to A are reflected by B, but changes made to B are not reflected by A. In contrast, bidirectional data binding implies that A is bound to B and B is bound to A. Changes made to either data source impact the other data source. |
Unlike the discontinued JavaFX Script language, which supports data binding, Java has yet to officially support this technology. For this reason, I present a simple data binding framework in this post. Perhaps this framework will prove useful to you.
Binder utility class whose static methods let you bind one Swing component to another
component, determine if a Swing component is bound to another component, and unbind one Swing component from another
component:
public static void bind(JComponent jc1, JComponent jc2) binds jc2 to jc1. If either component is not supported, no binding occurs and this method returns without reporting an error.
public static boolean isBound(JComponent jc1, JComponent jc2) returns true if jc2 is bound to jc1 or false if there is no binding.
public static void unbind(JComponent jc1, JComponent jc2) unbinds jc2 from jc1. If either component is not supported, no unbinding occurs and this method returns without reporting an error.
Listing 1 presents Binder's source code.
// Binder.java
import java.util.HashMap;
import java.util.Map;
import javax.swing.JComponent;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.Document;
public class Binder
{
private static Map<String, Entry> map = new HashMap<String, Entry>();
// Bind jc2 to jc1.
public static void bind(JComponent jc1, JComponent jc2)
{
if (isBound(jc1, jc2))
return;
if (jc1 instanceof JTextField)
{
final JTextField jtf1 = (JTextField) jc1;
if (jc2 instanceof JTextField)
{
final JTextField jtf2 = (JTextField) jc2;
Document doc = jtf1.getDocument();
DocumentListener dl;
dl = new DocumentListener()
{
public void changedUpdate(DocumentEvent de)
{
Runnable r = new Runnable()
{
public void run()
{
String text = jtf1.getText();
if (jtf2.getText().equals(text))
return;
jtf2.setText(text);
}
};
SwingUtilities.invokeLater(r);
}
public void insertUpdate(DocumentEvent de)
{
Runnable r = new Runnable()
{
public void run()
{
String text = jtf1.getText();
if (jtf2.getText().equals(text))
return;
jtf2.setText(text);
}
};
SwingUtilities.invokeLater(r);
}
public void removeUpdate(DocumentEvent de)
{
Runnable r = new Runnable()
{
public void run()
{
String text = jtf1.getText();
if (jtf2.getText().equals(text))
return;
jtf2.setText(text);
}
};
SwingUtilities.invokeLater(r);
}
};
doc.addDocumentListener(dl);
Entry entry = new Entry(jc1, dl);
String key = jc1.getName()+jc2.getName();
map.put(key, entry);
}
}
}
// Return true if jc2 is bound to jc1.
public static boolean isBound(JComponent jc1, JComponent jc2)
{
String key = jc1.getName()+jc2.getName();
return (map.get(key) != null) ? true : false;
}
// Unbind jc2 from jc1.
public static void unbind(JComponent jc1, JComponent jc2)
{
String key = jc1.getName()+jc2.getName();
Entry entry = map.get(key);
if (entry != null)
{
if (entry.jc instanceof JTextField)
{
JTextField jtf = (JTextField) entry.jc;
Document doc = jtf.getDocument();
doc.removeDocumentListener((DocumentListener) entry.o);
}
map.remove(key);
}
}
private static class Entry
{
JComponent jc;
Object o;
Entry(JComponent jc, Object o)
{
this.jc = jc;
this.o = o;
}
}
}
Listing 1: Binder.java
Listing 1 reveals a simple framework for binding one Swing component to another. This framework depends upon a single java.util.Map<String, Entry> instance that stores bindings in terms of a String key and an Entry entry.
The key is a combination of the two components' names; each name must be unique and is established by calling the component's public void setName(String name) method. The entry stores a reference to the component and a related object (typically a listener).
bind() takes care of binding one component to another. It first makes sure that these components are not already bound, and returns if this is the case. If bind() didn't return, it would create a new binding that would replace a previously stored binding, which would cause problems.
bind() is limited to binding one textfield to another. It obtains the binding textfield's document object and adds a document listener to this object. The listener's changedUpdate() and similar methods invoke the bound textfield's setText() method to notify it of changed text.
After adding the document listener to the document, bind() instantiates Entry, saving the binding component's and listener's references in this object. It then creates a key consisting of the components' names and stores the key and entry objects in the map.
When unbind() is called, it creates a key from its component arguments, and uses this key to retrieve the corresponding Entry object from the map. If this object exists, its binding component and document listener objects are retrieved and used to remove the document listener.
Binder class is fairly straightforward. I've created a BinderDemo application that demonstrates its ease of use. Listing 2 presents this application's source code.
// BinderDemo.java
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
public class BinderDemo extends JFrame
{
public BinderDemo(String title)
{
super(title);
setDefaultCloseOperation(EXIT_ON_CLOSE);
final JTextField jtf1 = new JTextField(40);
jtf1.setName("jtf1");
JPanel pnl = new JPanel();
pnl.add(new JLabel("txt1"));
pnl.add(jtf1);
getContentPane().add(pnl, BorderLayout.NORTH);
final JTextField jtf2 = new JTextField(40);
jtf2.setName("jtf2");
pnl = new JPanel();
pnl.add(new JLabel("txt2"));
pnl.add(jtf2);
getContentPane().add(pnl);
ActionListener al;
pnl = new JPanel();
JButton jb = new JButton("Bind txt2 to txt1");
al = new ActionListener()
{
public void actionPerformed(ActionEvent ae)
{
Binder.bind(jtf1, jtf2);
}
};
jb.addActionListener(al);
pnl.add(jb);
jb = new JButton("Bind txt1 to txt2");
al = new ActionListener()
{
public void actionPerformed(ActionEvent ae)
{
Binder.bind(jtf2, jtf1);
}
};
jb.addActionListener(al);
pnl.add(jb);
jb = new JButton("Unbind txt2 from txt1");
al = new ActionListener()
{
public void actionPerformed(ActionEvent ae)
{
Binder.unbind(jtf1, jtf2);
}
};
jb.addActionListener(al);
pnl.add(jb);
jb = new JButton("Unbind txt1 from txt2");
al = new ActionListener()
{
public void actionPerformed(ActionEvent ae)
{
Binder.unbind(jtf2, jtf1);
}
};
jb.addActionListener(al);
pnl.add(jb);
getContentPane().add(pnl, BorderLayout.SOUTH);
pack();
setResizable(false);
setVisible(true);
}
public static void main(String[] args)
{
Runnable r = new Runnable()
{
public void run()
{
new BinderDemo("Binder Demo");
}
};
EventQueue.invokeLater(r);
}
}
Listing 2: BinderDemo.java
BinderDemo creates a user interface consisting of two textfields and four buttons. By default, neither textfield is bound to the other, and entering text in either textfield does not cause this text to appear in its counterpart.
Click the leftmost button to bind the lower textfield to the upper textfield, so that text entered in the upper textfield appears in the lower textfield. Similarly, click the next-to-leftmost button to bind the upper textfield to the lower textfield.
Click one of the rightmost pair of buttons to perform an unbind operation. The next-to-rightmost button unbinds the lower textfield from the upper textfield, whereas the rightmost button unbinds the upper textfield from the lower textfield.
changedUpdate(), insertUpdate(), and removeUpdate() listener methods includes the following code fragment: if (jtf2.getText().equals(text)) return;. What is the purpose of this code fragment?
changedUpdate(), insertUpdate(), and removeUpdate() listener methods creates a Runnable to execute its code and uses SwingUtilities.invokeLater(r); to execute this runnable at a later time. Why defer execution?
Binder is far from perfect. One shortcoming is that you cannot choose the property on which to bind. For example, you cannot bind two textfields to keep their fonts in sync. How would you modify Binder to make it more flexible from a property perspective?
Binder imperfection is that it's limited to binding textfields. Extend this class to also support binding labels to textfields. Create a suitable demonstration program that demonstrates the binding and unbinding of these component combinations.
You can download this post's code and answers here. Code was developed and tested with JDK 7u2 on a Windows XP SP3 platform.
* * *
I welcome your input to this blog, and will write about relevant topics that you suggest. While waiting for the next blog post, check out my TutorTutor website to learn more about Java and other computer technologies (and that's just the beginning).