Hide and show JTable columns

Swing is getting old, but is still widely used. To my surprise, it turns out that its JTable doesn’t support hiding and showing columns at user’s whim. Well, it’s time to fix that!

Note that a similar work has already been done. So the purpose of this post is mainly educational. I’m going to do it using TDD and keeping the code as clean as I can. But first, it’s time for some design.

What we need is a menu. So it looks reasonable to extend JPopupMenu. However, we need more than that. We also need some boilerplate logic that will bind that menu together with JTable. We can extend JTable to do that, but that doesn’t sound like a good idea because that would prevent anyone with their own derivatives of JTable from using our code.

So it looks like ideally we would like to have a class that we could instantiate and install on a JTable to handle all that logic. Perhaps it will use a separate class for a menu, perhaps some other classes. Let’s not bother with these details for now. Instead, we should think of a name. JTableColumnSelector sounds fine: the J hints that it’s a Swing class, and it openly tells us that it is used to select columns. Maybe it is not very clear that it hides or shows columns, but JTableColumnHiderShower just doesn’t sound right, and besides, shower is something entirely different.

Before I begin, I should mention that for TDD I’m using TestNG, AssertJ and Mockito. That’s my usual set of tools.

The first TDD iteration looks rather stupid: write a single line test with new JTableColumnSelector(), then make it compile by creating an empty class. At this point I’m making an important design decision: by choosing to use a no-args constructor, I’m making life easier for anyone willing to extend my class. Because I’m going to have a separate install method instead of passing a JTable directly to the constructor, it is guaranteed that install will only be called after the object is fully initialized.

Speaking of install method, we need another test:

What I like about Swing here is that it doesn’t really care that we’re calling its methods from a random testing thread. As long as it’s just one random thread, it runs just fine. What I don’t like about it, though, is that this very same feature makes it fail mysteriously at random moments when you call its methods from a wrong thread, thus violating the rule of repair terribly. Useful for testing, dangerous in production, as it often happens. Ideally, there should be a way to control this behavior, something like -Djavax.swing.allowCallsFromAnyThread.

But let’s get back to TDD. To make the test above pass, we need the appropriate method. And I also correct the constructor javadoc while I’m at it:

Now we need to test that it does what it should do. What should it do? Well, for one thing, it must create a popup menu on the table header, so let’s test it:

It fails. Good! Now let’s fix it:

Now we need to check that the menu contains… what? Obviously, a list of items. There should be as many of them as there are columns in the model. Wait, our table doesn’t have a model yet. So maybe here is where we should start using Mockito:

Here, I set A_REASONABLE_COLUMN_COUNT to 10. The test fails, but isn’t terribly readable, so I’m going to refactor it a bit first.

This looks a bit better. Now we need to make it pass.

OK, what next? I’m worried about two things now. First, the model might be null. Will getColumnCount() properly return zero or will it just throw a NullPointerException? And is it even a good idea to ask the table about column count? Shouldn’t we ask the model instead? What if some columns are already hidden by some other code? Should we display them in our menu? Let’s assume for now that we want to list all model columns. But then the code is wrong and we need a test that shows it.

Another thing I’m worried about is that we incorrectly created JMenuItems, while we should have used JCheckBoxMenuItem or whatever it’s really called. But that should become apparent later, when we start selecting menu items. So let’s deal with column counts now.

It fails. Cool. Let’s fix:

Now we have a real problem if model is null. We need another test for that:

Hmm… It passes! Why? Oh, I forgot that the model can’t be null! The table creates a default empty model for that case. Good. I hate nulls. But then we need to rename our test. installsProperlyWhenTableHasDefaultEmptyModel is a bit too long, but descriptive enough, so I’ll keep it.

Cool. Now let’s get back to install test. We need to check that all menu items have the right labels. But first we need to make our mock return that labels. Unfortunately it isn’t terribly easy to do with Mockito. No, wait, it’s actually easy, but not very elegant:

Maybe I should have used a real model instead of the mock. But it doesn’t that bad, so I’ll keep it like this for now. Only refactor this ugly class into a nested static class.

Now we need a couple of helper methods to extract column names from both the model and the menu. The lists should be the same. I’m feeling functional, so these methods turned like this:

And the new test is:

This is appended to the end of install, but I’m not repeating everything again and again. The proponents of the one-assert-per test idiom are probably cursing me now, but I think I’m doing the right thing here: I’m still testing that this thing installs properly. If I need three asserts for that, so be it!

Of course the test fails, and with a clear message too except that it’s too long. So I’ve changed A_REASONABLE_COLUMN_COUNT to 3. Now we have to fix it the test. Just one line has to be changed:

And now for the last piece of installation. We need to check that all of the menu items are selected. We need another helper method for that. Or maybe I’ll refactor this one:

And the test is now:

Oops! Looks like JMenuItem has isSelected method too, just like JCheckBoxMenuItem or whatever. Well, for now let’s just fix the test:

This goes into the loop of the install method. OK, what about the wrong class? We could just continue and then let it surface later, perhaps during manual testing. But I find it rather silly. Since I’ve noticed it already, why not fix it now? Changing JMenuItem to JCheckBoxMenuItem everywhere in the test seems to do the trick. Now the test fails with a clear message: javax.swing.JMenuItem cannot be cast to javax.swing.JCheckBoxMenuItem. Cool.

That’s it for today, except that I want to see how it looks on the screen, so I create a very simple demo:

Aaaaaand… it works!

menu

Next time we will add some logic to make it really work and do what it’s supposed to.

Leave a Reply