What I am trying to do is to implement a custom control for editing of list attributes (1:n relationships). As I do not have enough reputation to post a screenshot, I will do my best to describe it. It contains a list of records as value, each record is represented by a RecordPanel, which is basically an edit form for the record.
The control menu offers the possibility to add, delete and copy records, as well as browse the RecordPanels. Only the active RecordPanel is shown. To the left of the active RecordPanel, a JXTreeTable utilizing my custom TreeTableModel (which is the cause of my problem) is used to show a list of the records contained in the control. The value records make up the leaves of the model, the parents can be derived from an attribute of the corresponding child. The tree can be used to jump to a specific record directly.
For construction of the TreeTableModel, I use an XML template. Example:
<?xml version="1.0" encoding="UTF-8"?>
<treetable prefix="Drug" columns="name">
<branch entity="JpaDrugCharacterization" children="drugs" displayFields="name"/>
<branch entity="JpaDrug" parent="drugCharacterization" displayFields="name"/>
</treetable>
The Model makes use of TreeTableNodes, which contain the actual records, as well as their parent nodes and child nodes. Thus, it is possible to build the complete tree structure from the nodes.
The branch definitions contained in the XML template will be used to create TreeTableBranch objects, which know how to retrieve the branches parents and children as well as which fields to display in the tree table.
Here are the crucial methods that are used to build the NodeTreeTableModel. I did not include the overridden methods of AbstractTreeTableModel.
/**
* Builds a tree table model based on {@link TreeTableNode}s and {@link TreeTableBranch}es.
* Can be initialized using an XML template or by using the root node of a TreeTableNode structure
* together with a branch definition created by other means (e.g. programmatically).
*
* @author Lellebebbel
*
*/
public class NodeTreeTableModel extends AbstractTreeTableModel implements DebugLogger {
/**
* The root node of the tree table serving as the master parent.
* Entry point for {@link JXTreeTable} to build the tree. Not shown in GUI.
*/
private TreeTableNode rootNode;
/**
* Contains a hashtable mapping each branch definition
* to the class name of the corresponding class in the tree.
*/
private Hashtable<String, TreeTableBranch> branches = new Hashtable<>();
/**
* List of the column names given in the template.
* Used for initialization of the {@link fieldMapPool}
* and creation of the {@link JXTreeTable}.
*/
private List<String> columnNames = new ArrayList<>();
/**
* Contains a hashtable mapping the id of the contained objects to the respective TreeTableNode.
* Used to prevent duplicate creation of TreeTableNodes containing the same object.
*/
private Hashtable<Integer, TreeTableNode> nodeList = new Hashtable<>();
/**
* Contains a hashtable mapping the child class name to the corresponding dummy parent.
*/
private Hashtable<String, NamedPositionable> dummyParents = new Hashtable<>();
NodeTreeTableModel(TreeTableNode rootNode, Document template, List<? extends NamedPositionable> valueList) {
super(rootNode);
this.rootNode = rootNode;
this.initialize(template);
this.createTreeStructure(valueList);
}
/**
* Create a tree table structure based on the values set for the tree table.
*
* @param valueList the values of the tree table
*/
private void createTreeStructure(List<? extends NamedPositionable> valueList) {
// browse through the valueList (containing the designated leaves for the tree)
for (NamedPositionable leaf : valueList) {
// create the tree table node for the leaf
TreeTableNode leafNode = new TreeTableNode(leaf);
// link all parents up to the tree rootNode to the leaf
TreeTableNode linkNode = leafNode;
while (!linkNode.equals(this.rootNode)) {
linkNode = this.linkToParent(linkNode);
}
nodeList.put(leaf.getId(), leafNode);
}
}
/**
* Evaluate the template, initialize translation prefix, column names and branch definitions.
*
* @param template The template to be evaluated
*/
private void initialize(Document template) {
// start parsing of template
Element root = template.getDocumentElement();
// initialize the translation prefix
this.prefix = XMLTools.getAttribute(root, "prefix", true);
// initialize the column names
String[] columnNameArray = XMLTools.getAttribute(root, "columns", true).split(",");
for (String columnName : columnNameArray) {
this.columnNames.add(Translator.get(this.prefix + "." + columnName));
}
// initialize the branches
NodeList branchNodes = root.getElementsByTagName("branch");
for (int i = 0; i < branchNodes.getLength(); i++) {
Node branchNode = branchNodes.item(i);
// retrieve the class name to be used as key
String className = XMLTools.getAttribute(branchNode, "entity", true);
// create the tree table branch to be used as value
TreeTableBranch branch = new TreeTableBranch();
String[] displayFields = XMLTools.getAttribute(branchNode, "displayFields", true).split(",");
for (int j = 0; j < this.columnNames.size(); j++) {
branch.addDisplayField(columnNames.get(j), displayFields[j]);
}
String parentField = XMLTools.getAttribute(branchNode, "parent", false);
branch.setParentField(parentField);
String childField = XMLTools.getAttribute(branchNode, "children", false);
branch.setChildField(childField);
this.branches.put(className, branch);
}
}
/**
* Links a given node to its parentNode an vice versa.
* Part of the tree table structure building routine.
*
* @param linkNode the childNode to be linked
* @return parentNode the linked parentNode
*/
private TreeTableNode linkToParent(TreeTableNode linkNode) {
// find the branch of the linkNode
TreeTableBranch branch = branches.get(linkNode.getContainedClassName());
// if branch does not specify a parent, use rootNode
if (branch.getParentField() == null) {
linkNode.setParent(this.rootNode);
this.rootNode.addChild(linkNode);
return this.rootNode;
}
// get the parent object
NamedPositionable parent =
(NamedPositionable) ReflectionTools.getValue(linkNode.getContainedObject(), branch.getParentField());
if (parent == null) {
parent = this.getDummyParent(linkNode.getContainedClassName());
}
TreeTableNode parentNode;
// if parent node has already been created
if (this.nodeList.containsKey(parent.getId())) {
// take it
parentNode = this.nodeList.get(parent.getId());
// establish the link
linkNode.setParent(parentNode);
parentNode.addChild(linkNode);
// return root node, as the parent is already connected to root
return this.rootNode;
} else {
// create it
parentNode = new TreeTableNode(parent);
// establish the link
linkNode.setParent(parentNode);
parentNode.addChild(linkNode);
// put the new parent in the nodeList and return it
this.nodeList.put(parent.getId(), parentNode);
return parentNode;
}
}
/**
* Returns the proper dummy parent for a given class name of a child.
* If the dummy parent does not exist, it will be created.
*
* @param childClassName name of the child class
* @return the dummy parent
*/
@SuppressWarnings("unchecked")
private NamedPositionable getDummyParent(String childClassName) {
NamedPositionable dummyParent = this.dummyParents.get(childClassName);
if (dummyParent != null) {
return dummyParent;
}
/*
* As we do not have a fitting dummy parent, we have to create it...
* We start by finding out, of which class the parent should be.
*/
TreeTableBranch childBranch = this.branches.get(childClassName);
/*
* As IdRecord should be the most common case for tree children,
* the short version of the class name may be used in the template.
*/
Class<? extends NamedPositionable> childClass;
if (!childClassName.contains(".")) {
childClass = ReflectionTools.getClassByEntityName(childClassName);
} else {
childClass = (Class<? extends NamedPositionable>) ReflectionTools.getClassByName(childClassName);
}
String methodName = ReflectionTools.getGetterName(childBranch.getParentField());
try {
Class<? extends NamedPositionable> parentClass = (Class<? extends NamedPositionable>) childClass
.getMethod(methodName, (Class<?>[]) null).getReturnType();
// as we know the class now, we can create the parent object
dummyParent = parentClass.newInstance();
// now we need to set the initial values of the parent object
dummyParent.initializeId();
TreeTableBranch parentBranch = this.branches.get(parentClass.getSimpleName());
for (String fieldName : parentBranch.getDisplayFields()) {
ReflectionTools.setValue(dummyParent, fieldName, Translator.get("NodeTreeTableModel.undefinedNode"));
}
// finally, we can add the parent to the dummy parent list and return it
this.dummyParents.put(childClassName, dummyParent);
return dummyParent;
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException e) {
Monitor.capture(e);
return null;
}
}
}
Finally, here are the methods used to draw the control and update the treetable.
/**
* Rebuilds the tree table. Should be called, when value has changed.
*/
public void buildTreeTable() {
this.treeTable = new RecordPaginationTreeTable(this.createTreeTableModel());
this.treeTable.setAutoResizeMode(JXTreeTable.AUTO_RESIZE_ALL_COLUMNS);
// Calculate max width of the tree table
this.treeTable.expandAll();
Dimension treeTableSize = this.treeTable.getPreferredSize();
Integer totalColumnWidth = TableTools.getCalculatedTableWidth(treeTable);
this.treeTable.setPreferredSize(new Dimension(totalColumnWidth, treeTableSize.height));
// Notify the tree updaters that the table has been rebuild
this.pagination.notifyTreeUpdaters();
NodeTreeTableModel nttm = (NodeTreeTableModel) this.getTreeTable().getTreeTableModel();
nttm.dumpTreeStructure();
}
/*
* (non-Javadoc)
*
* @see de.stada.pvapp.client.gui.interfaces.FormComponent#drawComponent()
*/
@Override
public void drawComponent() {
// Basic UI settings
this.removeAll();
this.setOpaque(false);
this.buildTreeTable();
// Set selection mode to single selection and opacity
this.treeTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
this.treeTable.setOpaque(false);
this.treeTable.setTableHeader(null);
Color alphaColor = new Color(0, 0, 0, 0);
treeTable.setBackground(alphaColor);
// Build pagination
this.pagination = new RecordPagination();
for (IdRecord record : this.value) {
RecordPanel rp = module.createRecordPanel(record, this.editMode);
rp.setOpaque(false);
pagination.addPanel(rp);
}
// Set the layout as follows
//
// ___________________________________
// | _____________| |
// | | | |
// | | | |
// | | | |
// | | tree | pagination |
// | | | |
// | | | |
// | |_____________| |
// |_______________|___________________|
//
String layout;
String[] layoutParams = this.getParameters("layout");
if (layoutParams != null && layoutParams.length > 0) {
layout = layoutParams[0];
} else {
layout = DEFAULT_LAYOUT;
}
this.setLayout(new FormLayout(LayoutConfig.getLayout(layout), "$vBorder,fill:d:grow,$vBorder"));
// Build split pane
this.add(this.treeTable, new CellConstraints(2, 2));
this.add(this.pagination, new CellConstraints(3, 1, 1, 3));
// add listeners
this.addDefaultListeners();
}
As long as I have a given value from the database, everything works just as intended. The model, the control and the GUI are properly build. But as soon as I am trying to add a new record to the control, a new factor comes into play:
As a mentioned above, the parents of a leaf in the tree are derived from the leaf's attributes. In the example given in the XML template above, the parent of a drug would be the drugCharacterization. However, when I create a new drug, the drugCharacterization is of course not known. In order to show it in the tree, I create dummy Parents (labeled -undefined-) for those new records. As soon as the drugCharacterization is defined during the editing process, the tree will be updated and the record will be placed under the proper parent. For creation of dummyParents see methods linkToParent and getDummyParent in the NodeTreeTableModel.
Now to my problem: As far as I can tell, the dummyParents are created and linked properly on creation of the new record, the new record is added to the value and the corresponding RecordPanel can be reached by browsing the panels. However, they are never actually shown in the tree in the GUI.
There should be something like:
-undefined-
new drug
but it just isn't there. I have absolutely no clue, why that should be the case.
2015-Sep-08: If I load an object with undefined parent from the database, it is shown properly in the GUI. If I add one during runtime, it is not.
I have several key listeners registered on the tree table nodes, that update the tree, if the name of a node changes. That works fine for existing nodes. Still, the new undefined nodes are not shown after those repaints triggered by the key listeners.
The problem seems to be located in the buildTreeTable method. I build a new tree table with a new treetablemodel as you can see in the code. But apparently the old structure is used for this new table.
How can I get the TreeTable to repaint properly using the structure of the new model. I tried to invalidate(), repaint(), updateUI() it. All futile.