oktober 14, 2010

Refresh Node children automatically

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.