This discussion is archived
1 2 Previous Next 16 Replies Latest reply: Oct 1, 2013 5:34 AM by JGagnon RSS

List as a table cell

JGagnon Newbie
Currently Being Moderated

I have a UI screen that needs to represent a "dashboard" of sorts that will display various collections of related information.  Part of the requirements is that all relevant information be visible.  For example, there will be a table of all users of a system and for each user the table must also show a listing of all "roles" assigned to that user.  The optimal solution would be to have a table, where each row represents a user, with "sub rows" or a list of roles.  The list of roles, of course, will vary by user.  To provide a visual example (although the inner list below should not have nor does it need a header):

 

UserRoles
User 1
Header 1
Role 1
Role 2
User 2
Header 1
Role 3
User 3
Header 1
Role 2
Role 3
Role 4

 

None of the information on these dashboard screens needs to be directly editable in the table views, it is intended to be read-only on these screens.  Editing of the information displayed is handled elsewhere.  The first and simplest idea that occurs is: why not make 2 tables (sort of master/detail), but that breaks the "everything must be visible" rule.

 

I've tried to make a TableCell extension that implements a list (ListView) as the "widget".  There is no current implementation for list view table cells in JavaFX 2.2, although there are for combo boxes and choice boxes.  I've looked at the source code for the ComboBoxTableCell class to try to get an idea how it does what it does.  I've come up with a version of a "ListViewTableCell" and have tried to use it.  It's not quite working, but I can't figure out what I'm doing wrong.

 

I'll include what source I can below.

 

public class ListViewTableCell<S, T> extends TableCell<S, T> {
     public static <S, T> Callback<TableColumn<S, T>, TableCell<S, T>> forTableColumn(final T... items) {
          return forTableColumn(null, items);
     }
     public static <S, T> Callback<TableColumn<S, T>, TableCell<S, T>> forTableColumn(final StringConverter<T> converter, final T... items) {
          return forTableColumn(converter, FXCollections.observableArrayList(items));
     }
     public static <S, T> Callback<TableColumn<S, T>, TableCell<S, T>> forTableColumn(final ObservableList<T> items) {
          return forTableColumn(null, items);
     }
     public static <S, T> Callback<TableColumn<S, T>, TableCell<S, T>> forTableColumn(final StringConverter<T> converter, final ObservableList<T> items) {
          return new Callback<TableColumn<S, T>, TableCell<S, T>>() {
               public TableCell<S, T> call(TableColumn<S, T> list) {
                    return new ListViewTable<S, T>(converter, items);
               }
          }
     }
     private ObservableList<T> items;
     private ListView<T> listView;
     public ListViewTableCell() {
          this(FXCollections.<T> observableArrayList());
     }
     public ListViewTableCell(T... items) {
          this(FXCollections.observableArrayList(items));
     }
     public ListViewTableCell(ObservableList<T> items) {
          this.items = items;
          listView = new ListView<T>();
          listView.setItems(items);
          setGraphic(listView);
     }
     public ObservableList<T> getItems() {
          return items;
     }
     public void updateItem(T item, boolean empty) {
          super.updateItem(item, empty);
          if (!empty) {
               setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
          } else {
               setContentDisplay(ContentDisplay.TEXT_ONLY);
          }
          if (isEmpty()) {
               setText(null);
               setGraphic(null);
          } else {
               setText(null);
               setGraphic(listView);
          }
     }
}

 

 

In the UI code:

 

TableView<User> userTable = new TableView<User>();
TableColumn<User, String> userCol = new TableColumn<User, String>("Name");
userCol.setCellValueFactory(new PropertyValueFactory<User, String>("userName"));
userTable.getColumns().add(userCol);
TableColumn<User, ObservableList<Role>> rolesCol = new TableColumn<User, ObservableList<Role>>("Roles");
rolesCol.setCellValueFactory(new PropertyValueFactory<User, ObservableList<Role>>("roleList"));  // Do I need this?
userTable.getColumns().add(rolesCol);
// The code below is definitely a mystery to me. I'm sure that I'm butchering it.
rolesCol.setCellFactory(new Callback<TableColumn<User, ObservableList<Role>>, TableCell<User, ObservableList<Role>>>() {
          public TableCell<User, ObservableList<Role>> call(TableColumn<User, ObservableList<Role>> col) {
               final ListViewTableCell<User, ObservableList<Role>> cell = new ListViewTableCell<User, ObservableList<Role>>();
               ListBinding<Role> binding = new ListBinding<Role>() {
                    {
                         super.bind(cell.tableRowProperty());
                    }
                    protected ObservableList<Role> computeValue() {
                         return FXCollections.observableArrayList();
                    }
               };
               cell.itemProperty().bind(binding);
               return cell;
          }
     });

 

The "user" class:

 

public class User {
     private StringProperty userName = new SimpleStringProperty();
     private ObjectProperty<ObservableList<Role>> roleList = new SimpleObjectProperty<ObservableList<Role>>();
     public StringProperty userNameProperty() {
          return userName;
     }
     public ObjectProperty<ObservableList<Role>> roleListProperty() {
          return roleList;
     }
     // other non-relevant code omitted
}

 

I've extended the TableCell once or twice, but it has been for simpler situations.  This is the first time I tried to make a cell that would be represented as a list of items that are intended to be bound to a list property of the object type associated with the table view.

 

The code as written above throws exceptions left and right complaining that a bound value cannot be set.  Obviously I'm doing something wrong.  A prior change displayed the table and did show a list view in the column, however, the view was empty.

 

I feel I'm close, but I don't know enough about how the guts of it works to figure out how to make it work.  Any ideas and suggestions on how to make this work would be appreciated.

  • 1. Re: List as a table cell
    James_D Guru
    Currently Being Moderated

    First thing that occurs to me is that you might not need the full complexity of a ListView for these Table cells. E.g. I think you probably don't need to be able to select values, etc. I wonder if you can simply get away with this:

     

    rolesCol.setCellFactory(new Callback<TableColumn<User, ObservableList<Role>>, TableCell<User, ObservableList<Role>>>() {
         @Override
         public TableCell<User, ObservableList<Role>> call(TableColumn<User, ObservableList<Role>> col) {
              return new TableCell<User, ObservableList<Role>>() {
                   @Override
                   public void updateItem(ObservableList<Role> roles, boolean empty) {
                        if (empty) {
                             setText(null);
                        } else {
                             StringBuilder sb = new StringBuilder();
                             for (Role role : roles) {
                                  sb.append(role).append("\n");
                             }
                             setText(sb.toString());
                        }
                   }
              };
         }
    });

     

    This will basically just use a default table cell implementation, but set the text of the cell to the concatenation of all the rows, with new lines between them. If you want more control over the appearance, you could create a VBox, add a Label for each row to the VBox, and then set the graphic as a VBox. Call setContentDisplay(GRAPHIC_ONLY) if you use this approach. This way you could set some styles on the individual labels (give them borders or alternating background colors, or some such).

     

    If you need the ListView for the cell, it looks like you are on the right track. The problems are caused by

    cell.itemProperty().bind(binding);

     

    The TableView rendering mechanism will call setItem(...) on the cell to tell it what data to display. Calling set on a bound property will throw a runtime exception.

    The cell value factory will actually cause the calls to updateItem(...) in your cell implementation, with the list of roles being passed in as the item. The updateItem method should take care of updating the items in the underlying list view.

     

    Fixing this will take a bit of thought. One thing to notice is that your generic types don't seem to be quite right. Your ListViewTableCell<S,T> extends TableCell<S,T>: you instantiate this as new ListViewTableCell<User, ObservableList<Role>>(). So T resolves to ObservableList<Role>. But your items property (and indeed the items property for the ListView itself) is of type ObservableList<T>, which is now ObservableList<ObservableList<Role>>. So I think you want something like

     

    public class ListViewTableCell<S,T> extends TableCell<S, ObservableList<T>> { ... }

    and that you want to instantiate it as new ListViewTableCell<User, Role>().

     

    If the first option doesn't work, experiment a bit more and post back if you need more help. 

  • 2. Re: List as a table cell
    James_D Guru
    Currently Being Moderated

    I guess one other comment here. The ComboBoxTableCell on which you're basing this uses the same list of possible values for all rows in the table. The value passed to the updateItem(...) method (which is of the same type as the type of the column) is one of these values (and becomes the selected item in the combo box). Your case is slightly different: it's the list of values that varies row to row (and I think the selected item is basically irrelevant).

  • 3. Re: List as a table cell
    JGagnon Newbie
    Currently Being Moderated

    You are entirely correct.  Yes, I was using the ComboBoxTableCell as my basis for comparison.

  • 4. Re: List as a table cell
    JGagnon Newbie
    Currently Being Moderated

    I think your simple idea (concatenated text with newlines or maybe the VBox) is just what I need.  As I had mentioned, none of the data needs to be editable in this view and no, it doesn't need to have the ListView "look".  I think this is what I've been searching for for the last couple weeks.  I will let you know how it works out.  Thanks.

  • 5. Re: List as a table cell
    JGagnon Newbie
    Currently Being Moderated

    This worked beautifully.  I adopted the VBox instead of just concatenated text.  So simple when all is said and done.

     

    Now, I've got to do the same for a list of checkboxes.  (Remember the problem you helped me out with yesterday?  The checkboxes that needed to be enabled/disabled based upon the "checked" state of another checkbox in the same table row).  Another one of the views on this dashboard lists roles and a collection of "groups" and associated CRUD permissions.  Each role row may have an arbitrary collection of groups and associated permissions.  In this case, all of the columns are read-only.  I'm guessing I can just expand on the solution you've provided and place checkboxes in a VBox and set the "checked" state of each box according to the particular permission setting.

  • 6. Re: List as a table cell
    James_D Guru
    Currently Being Moderated

    Yup, that should work.

  • 7. Re: List as a table cell
    JGagnon Newbie
    Currently Being Moderated

    OK this is all working very nicely (for both text and checkboxes).  Thank you very much.

     

    I have now a different problem though.  Everything on that dashboard is read-only, but the user has the ability to effect changes to the lists of information displayed on the dashboard via "editing" dialogs that allow the user to make changes once a given row has been selected on the dashboard table.  A given editor only operates on one row selected in the dashboard table.  For example, using the user/roles described earlier, the user selects a user on the dashboard and clicks "Edit" to open the editor.  The editor allows the user to add/remove roles for the selected user row and then click OK to save changes or Cancel to discard changes.

     

    The save logic will make the appropriate calls to save the pertinent information to a database and when that completes, the user row back on the dashboard needs to be refreshed to reflect any changes in roles that were made.  This is where I'm having the problem.  The editor screen correctly displays the collection of roles for a given user and I can make changes and click OK to "save" them.  However, once the editor dialog closes, the dashboard is not updated.  I suspect that it does not "know" about the changes that were made to the roles list for that user.  I know that the list change has been correctly updated to the underlying object, because if I open the editor again for that user, the changes that I had made are correctly represented.  The editors make a copy of the selected object (i.e. user) and that is what is modified in the editor.  Once the user clicks OK (committing their changes), I update the model object.  I do this so that I can ensure that nothing is changed outside of the editor until the user clicks OK.

     

    I'm guessing there's a technique that can be used to keep the dashboard items updated when edits are committed.  Keep in mind this is in the context of the concept of dashboard table rows that have some columns with multiple pieces of information.  And it happens to be that collection is what's being changed.  Any ideas?

  • 8. Re: List as a table cell
    JGagnon Newbie
    Currently Being Moderated

    In my refresh logic is removed the edited row and re-inserted it.  The dashboard now shows the changes.  For some reason it doesn't seem like the right way to do it.

  • 9. Re: List as a table cell
    JGagnon Newbie
    Currently Being Moderated

    Using the idea presented in earlier comments (which work nicely, except when the list content changes dynamically), is there a way to "bind" a collection of items used by the customized TableCell so that it will be aware of changes made to the list externally?  For example, I have taken your suggestions above and have attached an anonymous Callback to a table column that builds a VBox with labels (or checkboxes), one for each item in the "role" list for a given user.  This works great - except that if I make changes to the role list (via an editor dialog), the changes are not reflected back on the dashboard.  I think that I need some way to create a binding between the backing list (the collection of roles for a selected user) and the customized TableCell.  I just don't know how to do it.  I've been trying different things, but nothing works so far.  Does this idea make sense?  Any suggestions would be appreciated.

  • 10. Re: List as a table cell
    JGagnon Newbie
    Currently Being Moderated

    Update to the "delete and add" strategy: that works only sometimes (most often not).

  • 11. Re: List as a table cell
    James_D Guru
    Currently Being Moderated

    (Just a quick response here; don't really have time to test the code but hopefully you will get the idea.)

     

    You can't use a binding directly: you need to update the children of the vbox, which are not exposed as a property. So you have to implement this with a listener.

    So I think I'd implement the cell as an inner class, and do something like this:

     

    class RoleListTableCell extends TableCell<User, ObservableList<Role>> {
         private final ListChangeListener<Role> changeListener ;
         private final VBox vbox ;
         RoleListTableCell() {
              this.vbox = new VBox();
              // configure spacing, style etc if required...
              changeListener = new ListChangeListener<Role>() {
                   @Override
                   public void onChanged(Change<? extends Role> change) {
                        rebuildList();
                   }
              };
              itemProperty().addListener(new ChangeListener<ObservableList<Role>>() {
                   @Override
                   public void changed(Observable<? extends ObservableList<Role>> observable, ObservableList<Role> oldRoleList, ObservableList<Role> newRoleList) {
                        if (oldRoleList != null) {
                             oldRoleList.removeListener(changeListener);
                        }
                        if (newRoleList != null) {
                             newRoleList.addListener(changeListener);
                        }
                        rebuildList();
                   }
              });
              setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
              setGraphic(vbox);
         }
         private void rebuildList() {
              ObservableList<Role> roles = getItem();
              if (roles == null) {
                   vbox.getChildren().clear();
              } else {
                     List<Label> labels = new ArrayList<Label>();
                     for (Role role : getItem()) {
                          labels.add(new Label(role.toString()));
                     }
                     vbox.getChildren().setAll(labels);
              }
         }
    }

     

    Then your callback just returns a new instance of that class.

     

    As I said, that's just off the top of my head, and typed in here without testing, so there are likely typos that need fixing. The basic idea is that the list listener responds to changes in the list. The listener to the itemProperty makes sure the list listener is observing the correct list of roles for changes.

     

    I'm assuming here your Role class itself is immutable. It gets a whole lot more fun otherwise .

  • 12. Re: List as a table cell
    JGagnon Newbie
    Currently Being Moderated

    Any particular reason that you would make it an inner class?  Just because that's the only place it needs to be used?

    Not sure I understand what you mean as far as the Role class being immutable?  That it is final and therefore there is no chance for subclassing?

     

    Regarding your suggestion, so far so good.  Several of my columns use checkboxes instead of just a label, but as I mentioned sometime earlier, the dashboard screen is read-only so having to deal with inline editing is not a concern.  The biggest gripe is that I need to create a separate class for each instance where I need to do this (a total of 8 table columns spread amongst 3 tables) - and they're nearly all identical (for a given table data type), save for the "get" call to get the model object state of interest and rendering that state (as a text label or a checkbox setting) in the column.  I wish I could figure out a way to genericize that in some way.

  • 13. Re: List as a table cell
    James_D Guru
    Currently Being Moderated

    Any particular reason that you would make it an inner class?  Just because that's the only place it needs to be used?

    Yes. It could just as easily be a top-level class.

     

    Not sure I understand what you mean as far as the Role class being immutable?

    That the state of a Role object can't be changed once the Role object has been created. (Specifically, the state that's displayed in the table.)

    So

    public class Role {
         private final String name ;
         public Role(String name) {
              this.name = name ;
         }
         public String getName() {
              return name ;
         }
         @Override
         public String toString() {
              return name ;
         }
    }

    is fine, but

    public class Role {
         private String name ;
         public Role(String name) {
              this.name = name ;
         }
         public String getName() {
              return name ;
         }
         public void setName(String name) {
              this.name = name ;
         }
         @Override
         public String toString() {
              return name ;
         }
    }

    might cause problems (if the name of a role were changed via someRole.setName(...), the UI would not be notified).


     

    I wish I could figure out a way to genericize that in some way.

     

    Should be possible. Again, not tested, but something like


    public class ListTableCell<T extends Object> extends TableCell<User, ObservableList<T>> {
         private final Callback<T, String> formatter ;
         private final ListChangeListener<Object> changeListener ;
         private final VBox vbox ;
         public ListTableCell(Callback<T, String> formatter) {
              this.formatter = formatter ;
              // constructor as before but Role replaced by Object
         }
         private void rebuildList() {
              ObservableList<T> items = getItem();
              if (items==null) {
                   vbox.clear();
              } else {
                   List<Label> labels = new ArrayList<>();
                   for (T item : items) {
                        labels.add(new Label(formatter.call(item));
                   }
                   vbox.getChildren().setAll(labels);
              }
         }
    }

    You might have to play with the types a little to get that to work, but I think it's close to correct.

     

    Now you could do something like

    roleCol.setCellFactory(new Callback<TableColumn<User,ObservableList<Role>>, TableColumn<User, ObservableList<Role>>>() {
         @Override
         public TableCell<User, ObservableList<Role>> call(TableColumn<User, ObservableList<Role>> col) {
              return new ListTableCell<Role>(new Callback<Role, String>() {
                   @Override
                   public String call(Role role) {     
                        return role.toString();
                   }
              });
         });
    });

     

    All that will look so much nicer in Java8 with lambda expressions...

  • 14. Re: List as a table cell
    James_D Guru
    Currently Being Moderated

    You could make the formatter a Callback<T, Node> instead, if you wanted. That way you could be more general and provide a CheckBox, or other control. rebuildList() would create a List<Node> and set them in the VBox; for your role cell instantiation you'd return new Label(role.toString()) in the inner callback.

1 2 Previous Next

Legend

  • Correct Answers - 10 points
  • Helpful Answers - 5 points