In a netbeans platform project with more than one module there is a TopComponent object. One of the private properties of this TopComponent refers to another object. One of the components of this TopComponent is a button with event handling code in the TopComponent.
Before the button is pressed the property refers to the correct object, but as soon as the button is pressed (at the start of the event handling code) the value of this property is null!
What happened to my object reference?
As far as I can tell I haven't written any code that would set it to null. All my code seems to be working fine according to the debugger. I don't know how to make the debugger stop at the point where the variable is cleared. I used a fixed watch on the TopComponent object to confirm that just before pressing the button the object reference is there, and a break point at the start of the button's event handling code to see that it's gone there. But what happens in between that? I don't know, but whatever it is it's messing up my code.
The property is initialised when declared as null, it is given a value when the lookup from another module triggers a method that sets it. That method works fine.
I'm pretty sure it's the same instance of the TopComponent because the debugger gives the same id for the object and I used a fixed watch.
What am I missing?
Here is the relevant class
package org.sil.jflavourviewer;
// <snip> import statements
/**
* Top component which displays something.
*/
@ConvertAsProperties(dtd = "-//org.sil.jflavourviewer//JFlavourViewer//EN",
autostore = false)
@TopComponent.Description(preferredID = "JFlavourViewerTopComponent",
//iconBase="SET/PATH/TO/ICON/HERE",
persistenceType = TopComponent.PERSISTENCE_ALWAYS)
@TopComponent.Registration(mode = "explorer", openAtStartup = true)
@ActionID(category = "Window", id = "org.sil.jflavourviewer.JFlavourViewerTopComponent")
@ActionReference(path = "Menu/Window" /*, position = 333 */)
@TopComponent.OpenActionRegistration(displayName = "#CTL_JFlavourViewerAction",
preferredID = "JFlavourViewerTopComponent")
public final class JFlavourViewerTopComponent extends TopComponent implements LookupListener
{
public JFlavourViewerTopComponent()
{
initComponents();
setName(NbBundle.getMessage(JFlavourViewerTopComponent.class, "CTL_JFlavourViewerTopComponent"));
setDisplayName("Project Viewer");
setToolTipText("Here are the items in the active project");
categoriesListModel = new DefaultListModel<String>();
categoryList.setModel(categoriesListModel);
lookupContent = new InstanceContent();
lookup = new AbstractLookup(lookupContent);
associateLookup(lookup);
}
/** The content of this method is
* always regenerated by the Form Editor.
*/
private void initComponents() {
// <snip> Some swing components initialised
btnAddItem = new javax.swing.JButton();
// <snip> component settings and layout
org.openide.awt.Mnemonics.setLocalizedText(btnAddItem, org.openide.util.NbBundle.getMessage(JFlavourViewerTopComponent.class, "JFlavourViewerTopComponent.btnAddItem.text")); // NOI18N
btnAddItem.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
btnAddItemActionPerformed(evt);
}
});
// <snip> more component settings and layout
}
// here is the event handling code for the "add" button
private void btnAddItemActionPerformed(java.awt.event.ActionEvent evt)
{
// At this point this.activeProject is already null
JFlavourItemBean newItem = new JFlavourItemBean();
this.activeProject.addItem(newItem);
// <snip> the rest of the method is not executed because the NullPointerException is thrown
}
// <snip> swing variable declerations
private javax.swing.JButton btnAddItem;
private Lookup.Result<JFlavourProjectBean> result = null;
private DefaultListModel<String> categoriesListModel;
private JFlavourProjectBean activeProject = null;
private InstanceContent lookupContent;
private AbstractLookup lookup;
@Override
public void componentOpened()
{
result = Utilities.actionsGlobalContext().lookupResult(JFlavourProjectBean.class);
result.addLookupListener (this);
}
@Override
public void componentClosed()
{
result.removeLookupListener(this);
}
void writeProperties(java.util.Properties p)
{
//<snip>
}
void readProperties(java.util.Properties p)
{
//<snip>
}
// this is the code that sets this.activeProject via listening to the global Lookup
// after this method runs this.activeProject is successfully set
@Override
public void resultChanged(LookupEvent le)
{
Collection<? extends JFlavourProjectBean> allProjects = result.allInstances();
if (!allProjects.isEmpty()) {
JFlavourProjectBean project = allProjects.iterator().next();
this.activeProject = project;
SortedSet<String> categories = project.getCategories();
categoriesListModel.clear();
for (Iterator<String> it = categories.iterator(); it.hasNext();) {
categoriesListModel.addElement(it.next());
}
tmpLabel.setText(project.getName());
} else {
activeProject = null;
// TODO what to display when no project is loaded
}
}
}
If you want you can see the whole file without snips here JFlavourViewerTopComponent.java The thing that adds the JFlavourProjectBean object to its lookup is JFlavourProjectManagerTopComponent.java
If you like you can browse the code for the whole JFlavour project as it is so far. I'd give you the link if I had 10 reputation, but you can probably work it out from the above 2 links.
I was going to post an image of the debugger, but since I don't have enough rep I'll just have to describe it.
I halt the execution near the end of the resultChanged method and make 'this' a fixed watch. A watch on this.activeProject shows it populated correctly. I continue execution until it's waiting on input. The fixed watch still shows the activeProject property correctly set. Then I press the 'Add..." button and halt the execution at the first line in the event handler. now a watch on this.activeProject shows 'null', and the fixed watch also shows activeProject to be null.
I have made certain using the hashCode() method of Object that am am dealing with the same instance of the TopComponent throughout.
I worked out how to add a breakpoint to a variable that triggers when it gets modified (Ctrl+Shift+F8
-> Breakpoint Type: Field
)
That showed me that the execution re-enters the resultChanged method when the button is clicked (why? I don't know) and this time there isn't a JFlavourProjectBean in the lookup so it goes into the else clause and sets the activeProject to null.
Now I have changed that method to this:
@Override
public void resultChanged(LookupEvent le)
{
Collection<? extends JFlavourProjectBean> allProjects = result.allInstances();
if (!allProjects.isEmpty()) {
JFlavourProjectBean project = allProjects.iterator().next();
this.activeProject = project;
SortedSet<String> categories = project.getCategories();
categoriesListModel.clear();
for (Iterator<String> it = categories.iterator(); it.hasNext();) {
categoriesListModel.addElement(it.next());
}
tmpLabel.setText(project.getName());
} else {
// do nothing if no projects are in the lookup
}
}