layer.xml

a netbeans rcp blog

Refresh Node children automatically

Posted by Nicklas Löf on October 14, 2010

Hello followers!

Todays problem to solve is how to refresh your Node children when there is a change in the data model. For example if you have a list of Customers and a delete button. When it’s pressed you have added code that are removing the selected customer from the list of customers but the node will still be visible in your list view. How can this be resolved?

In my basic of nodes post I did use a ChildFactory to create my nodes. Childfactories has a createKeys() method. In createKeys you populate a list with all the data objects you want to create nodes from and createNodeForKey() is called automatically for each object. Childfactories also have a nice method called refresh(). What refresh does is that it calls the createKeys() method again but it only calls createNodeForKey() for objects that doens’t already have a node. This is great. The old nodes (with current cookiesets and other states) are still there, only the new ones are added (or removed in case there is data that has been removed).

So lets call this refresh method then? well.. you can’t.. it’s protected and can only be called from the class that are extends Childfactory which is our nodechildfactory. At first I created a public method called update() which then called the refresh() method but then I started to realize that I need to keep an instance of my childfactory public in my TopComponent and call it when my data was updated. This works but isn’t a good way to do it. You know.. decoupling and all that. The lesser dependencies the better.

What can we do instead? Once again the power of lookups saves us! This solution is very simliar to the one I used in my post about using Lookup to change views in your application but lets look at some code again and lets start with creating a total empty class.

public class NodeRefreshEvent {
}

Phew.. that was a complicated one. Now lets look at our class that provides us with the model data objects. It’s the same class that I used in Basic for nodes. It’s a class with a static method that returns a list of Customer. Lets say that we have a deleteCustomer() method now and when a customer is deleted I want my node view to reflect this change.

public class CustomerService implements Lookup.Provider{
    private InstanceContent content = new InstanceContent();
    private Lookup lookup = new AbstractLookup(content);

    @Override
    public Lookup getLookup()
    {
        return lookup;
    }
}

So what have been added? I’m implemeting Lookup.Provider that contains the method getLookup(). And I need a Lookup to return so I create a lookup using the InstanceContent class which is a List where I can put instances of any object. So what do you say.. should I be crazy and put my small and silly NodeRefreshEvent class into this list when my data has been updated? Let’s try that!

public void deleteCustomer(int id){
    customers.delete(id);  // Lets pretend customers is a map with id as key
    content.add(new NodeRefreshEvent());
}

(See the Important section at the end of this post for some important information about this method)

Was that everything? So easy!.. No.. you need to do one more thing. Remember the childfactory?

class CustomerNodeFactory extends ChildFactory<Customer> implements LookupListener{
    private final Result<NodeRefreshEvent> lookupResult;
    public CustomerNodeFactory() {
        lookupResult = CustomerService.getLookup().lookupResult(NodeRefreshEvent.class);
        lookupResult.addLookupListener(this);
    }
}

So.. in my ChildFactory I’m now implementing a LookupListener. It does excatly what it says. It listens to changes on a Lookup. Which Lookup? Thats what I define in the constructor. I say that I want to listen for changes in the Lookup of CustomerService but only ones that are of the type NodeRefreshEvent.

Now we need to react when a change is done. How can that be done? Trough the resultChanged method that the LookupListener have added for me off course. This is where we will do our magic.

    @Override
    public void resultChanged(LookupEvent le)
    {
        Lookup.Result r = (Lookup.Result) le.getSource();
        Collection c = r.allInstances();

        for (Object object : c)
        {
            if (object instanceof NodeRefreshEvent){
                refresh(true);
            }
        }
    }

So here we check if the event we got in the Lookup is an instance of NodeRefreshEvent. If it is then do refresh() and boom.. the magic happens.. The createKeys() method is called and together with createNodeForKey() the nodes list is updated with new entries or removal of deleted entries!

And once again we have done some decoupling. Our CustomerService doesn’t have any idea who is interested in the addition or removal of customers. And remember we aren’t limited to just one listener. We could have 20 of them that listen to a NodeRefreshEvent and do things.

If you need to send a message together with the NodeRefreshEvent it can easily be done. Just create a field and a getter. When you creates your new instance of NodeRefreshEvent add the message to the constructor and pick it up in the Listener like this:

for (Object object : c)
{
    if (object instanceof NodeRefreshEvent){
        String message = ((NodeRefreshEvent)object).getMessage();
        refresh(true);
        doSomethingWithMessage;
    }
}

Message is offcourse not limited to a String. It can be anything you want. An enum, another object….

One last important thing

As Darrin pointed out in the comments section I forgot to include one important thing. The InstanceContent list is never emptied. We just add new things into the Lookup but we never removes them. And since we loop over all content in the Lookup we gonna update the nodes multiple time. So this is something we must prevent and that is to remove our previous NodeRefreshEvent instance before adding a new one.

Unfortuneatly there is no removeAll or clear method that can be called. The only way to remove an object from the InstanceContent is by using the instance itself. So we would need to save our created instance of NodeRefreshEvent and then call content.remove(ourInstance); This is how I did in a previous blog post but a few days ago I found this post that showed me another way to do it. BeanDev

So lets use a modified version of this example in our removeCustomer method

public void deleteCustomer(int id){
    customers.delete(id);  // Lets pretend customers is a map with id as key
    content.set(Arrays.asList(new NodeRefreshEvent()), null);
}

So instead of adding one object we use set which wants a list that we simply can create with Arrays.asList. And now the whole content of the InstanceContent is replaced.

Advertisements

11 Responses to “Refresh Node children automatically”

  1. Darrin said

    Hey Nicklas, as someone new to Netbeans RCP, I’ve enjoyed your blog alot.

    Quesion on this post – won’t NodeRefreshEvent instances stay within the Lookup for the life of the application? There is nothing that will perform the cleanup.

  2. jkost said

    Couldn’t this also be done with PropertyChangeListeners? I.e. the ChildFactory could implement PropertyChangeListener and then in its propertyChange() method we could call refresh(true).

    • Hi Jkost,

      hmm.. I’m not sure.. since this isn’t a properychange but an addition of content into the Collection from where the nodes was built from. It’s probably possible to do with some kind of custom listener but I guess doing it the Netbeans way with Lookup is nicer. (Lookup is a kind of advanced listener)

  3. jkost said

    Hi Nicklas,

    here is the solution with the property change listeners, maybe not that elegant as the one you provide, but just for food for thought. Again the trick lies with the refresh(true) method of the ChildFactory (I wonder how can you access the ChildFactory from your TopComponent for example).

    So,

    public class CustomerService {
    private PropertyChangeSupport pcs;

    public void addPropertyChangeListener(PropertyChangeListener listener)
    {
    pcs.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener)
    {
    pcs.removePropertyChangeListener(listener);
    }

    public void deleteCustomer(int id){
    customers.delete(id); // Lets pretend customers is a map with id as key
    pcs.firePropertyChange(PROP_CUST_DELETE, new Integer(id), null);
    }
    }

    Then, your ChildFactory:


    class CustomerNodeFactory extends ChildFactory implements PropertyChangeListener{
    private final Result lookupResult;
    public CustomerNodeFactory() {
    lookupResult = CustomerService.getLookup().lookupResult(CustomerService.class);
    for (CustomerService cs : lookupResult.allInstances()) {
    cs.addPropertyListener(this);
    }
    }

    public void propertyChange(PropertyChangeEvent e) {
    for (CustomerService cs : lookupResult.allInstances()) {
    if (cs.PROP_CUST_DELETE.equals(e.getSourceName()))
    this.refresh(true);
    }
    }
    }

    I don’t know where should I unregister the property change listener from the ChildFactory though (I register it in the constructor). It would have been nice if I could register and unregister the propertychangelistener in the componentOpen() and componentClosed() methods of the TopComponent, however, how can I access the ChildFactory to call its refresh() method from it?

  4. You really made my day with this blog entry!

    Very well written with lots of detailled information. Thanks!

  5. Thank you for elaborating this in such detail. Do you have an idea how to set selected nodes in an ExplorerManager once the nodes have been added or deleted?

  6. dobri7 said

    Hi all ! I have little bit different problem : my outlIneView component gets updated even I didn’t implement your solution, through repeatedly node factory call and creation. However, last two columns of the outLineView do not get updated, unlike first four columns which do ! If I restart app. outlineView correctly displays content of all columns.
    Could someone please, help with some suggestion ?

  7. Bottom-line: Good to use for small files particularly if
    are not at your usual computer. Of course, you’ve got options like Fox – Torrent for Firefox and also the Opera Browser. You can enjoy the DVDs on the large screen LCDs using DVD players.

  8. Excellent beat ! I would like to apprentice while you amend your website, how could i subscribe
    for a blog site? The account helped me a acceptable deal.
    I had been a little bit acquainted of this your broadcast offered bright clear idea

  9. This design is spectacular! You certainly know how to keep a reader
    amused. Between your wit and your videos, I was almost moved to
    start my own blog (well, almost…HaHa!) Excellent job. I really loved
    what you had to say, and more than that, how you presented
    it. Too cool!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: