-1

we want to populate a ListView as a list of instances of a single JavaFX view. This is something we attempted to get working earlier and ran out of time. The fall-back solution at the time was to have a static panel (i.e. not use a ListView) with four rows. In the static version, each row is an instance of the same JavaFX (FXML) view.

Each row is an individual (instance of) a controller matching a small FXML file. The way this works, is that when the initialize() method is called, the view gets an id, this mapped to the model data and from then on the controller / 'view' manages its own data row. In the static case we can use the FXML include-directive, which makes things very simple. I'll call the individual rows, a row-view.

To use a ListView in this way we need to do the following:

  1. The list view controller must call a "list loader" to load/initialise the list
  2. We want the row-views to be displayed as individual rows in the ListView
    • That seems OK to do if the ListView can take a (sub-)view / (sub-)component
    • The row-view is a pre-packaged sub-view;
  3. Need to be able to 'load' new row-view FXML definitions as required for row changes.
    • Is there a way to "clone" an existing view to make a copy?

While this question is similar to the POJO question:

The main difference is that our row can be any arbitrary FXML. I think the mechanical parts of this will work by adapting our existing fixed-size panel. The key concept is to establish the display "scaffold" and allow the view to be swapped-out, instead of having to recode a list box each time the design/requirements change.

update A ...

I've made some progress. I can load an FXML file with a class called "ViewLoader", the with the loadView(urlStr) method. (puts, just writes to a log or sysout).

    public  AnchorPane   loadView( final String urlStr ){

    AnchorPane  fxmlView    = null;
    Parent      root        = null;
    URL         fxmlResource;

    try
    {
        fxmlResource = getClass().getResource( urlStr );
        root = FXMLLoader.load( fxmlResource, Resources.getResourceBundle() );

        this.fxmlStr  = urlStr;
        this.rootNode = root;
        fxmlView = (AnchorPane) this.rootNode;
    }
    catch (Exception ex)
    {
        Util.puts( "  * Exception on FXMLLoader.load()");
        Util.puts( "  * "+ex.getMessage());
        Util.puts( "    ----------------------------------------\n");
    }

    return  fxmlView;
}

I'm beginning with a know layout so loadView() loads and returns an AnchorPane as defined in the file named by "urlStr". So far so good. In the debugger I can observe a loaded view and it loads its controller. Each cell would have its own controller and work independently this way.

In the first pass the naive approach was to try to set view via a CellFactory. Bzzt! This invalidates the view, so can't do it that way.

Apparently what's needed is a way to load the ListView with custom cell views. I'm stumped about how to make that happen though. At present we are using a local class called:

public class CustomListView
{
    :

    public class  CustomCellFactory implements  Callback<ListView<MyObject>, ListCell<MyObject>> 
    {

        @Override
        public ListCell<MyObject> call( ListView<MyObject> listView ) {

            ListCell<MyObject> cell = new ListCellType();

            return cell;
        }

    }//CustomCellCallback class

}//CustomListView

It seems to me that the logic of a factory pattern, then the new ListCellType() ought to return the loaded AnchorPane as the view. Where ListCellType is defined as a subclass of ListCell<> ...

public class ListCellType extends ListCell<MyObject>
{
    :

}//ListCellType

However, it looks to me as if we need the factory to return the loaded view, so instead of

  ListCell<MyObject> cell = new ListCellType();

This application needs to return a JavaFX Node or in this specific case an AnchorPane (and we can make it more general later). Something as-if:

  ListCell<AnchorPane> cell = new ListCellType();

While the theory seems OK, the fact remains that the ListView.setCellFactory(...), requires a ListView and not a node.

How to make this work, perhaps with a different factory/update scheme?

Community
  • 1
  • 1
will
  • 4,799
  • 8
  • 54
  • 90
  • hmm ... technically, you'll need list of data elements that know which fxml to loade and a cellFactory that loads the view from that fxml. Never tried, though. – kleopatra Dec 12 '14 at 09:40
  • Just want to reuse the same FXML for each row. What you are saying then, is a custom cellFactory. Good place to start, thanks. – will Dec 14 '14 at 11:16
  • This question looks suitable: http://stackoverflow.com/questions/19588029/customize-listview-in-javafx-with-fxml – will Dec 15 '14 at 02:37
  • what do you mean by "invalidates the view, can't do that"? It loads a new view every time it needs a new cell - exactly what a cellfactory is meant to do :-) Anyway and asking for the last time: please show a SSCCE that demonstrates what you are trying to achieve and how it fails your needs – kleopatra Dec 16 '14 at 10:15
  • @kleopatra ... Hi, I don't have a "[Short, Self Contained, Correct Example](http://sscce.org) ([SCCE](http://sscce.org)) yet; the purpose for this question is to determine what is possible to produce a list of "... (for example) views from an FXML definition file. If one could have a ListView of directives, that would demonstrate the target we're aiming at. – will Dec 16 '14 at 10:55
  • @kleopatra ... invalidating the view, I should have said, "the ViewCell". This happened when I assigned ... ViewCell.setChildren( ... ) an _inappropriate_ value. That is an erroneous operation hence a moot issue, imho. – will Dec 16 '14 at 11:04

1 Answers1

0

I believe we have found a raw answer that is suitable enough for proof of concept and usable as a starting point for a 'general purpose' pattern. The kernel comes from a couple of extensions to this Customized ListView example:

as mentioned earlier. These examples use simple objects as the list content. The first requirement was to have a ListView of sub-views, where the sub-view is another FXML file. Conceptually this would be similar to a ListVeiw using an FXML-<include> operator.

The basis for the raw solution is to pair the cell-data with the cell-view. So for a list of custom cell-views the ListView needs an ObservableList of (view, data) tuples. Or this could just be a "list of sub-views" when the sub-view's controller is smart enough to find the data in the model. For now our raw example keeps the data and an instance of the view together, viz.

public class ListCellView   extends CellView
{
    @FXML private Label             label1;
    @FXML private Label             label2;


    @Override
    public void setInfo( MyObject myObject ){

       label1.setText( myObject.getDay() );
       label2.setText( Integer.toString( myObject.getNumber() ) );

       label1.setTextFill( myObject.getColor() );
    }


        /**
         *  Initialize
         *    -- must use java -ea <program>, to check asserts
         **/
    @FXML
    @Override
    protected void initialize() {

        super.initialize();

        assert label1       != null : "fx:id=\"label1\" was not injected: check your FXML file 'CustomCell.fxml'.";
        assert label2       != null : "fx:id=\"label2\" was not injected: check your FXML file 'CustomCell.fxml'.";
    }

}//ListCellView

The essential element is a public method called setInfo(), that updates the data for display within the sub-view. Different arrangements are possible. It is most likely that list of sub-views will just be set-up with an Id that can be used to look-up model data appropriate the list position.

Where we've used CellView as an abstract class for sub-displays, such that the sub-views can be changed around while preserving a similar structure:

public abstract class CellView
{
    @FXML private ResourceBundle    resources;
    @FXML private URL               location;
    @FXML private AnchorPane        customCell;
    @FXML private HBox              hBox;

    public abstract void setInfo( MyObject myObject );

    public HBox getBox() {

        return hBox;
    }

    protected void initialize() {

        assert customCell   != null : "fx:id=\"customCell\" was not injected: check your FXML file 'CustomCell.fxml'.";
        assert hBox         != null : "fx:id=\"hBox\" was not injected: check your FXML file 'CustomCell.fxml'.";
    }

    public  CellView(){
    }

}//CellView

In the run-time code, the ListView class: CustomListView is loaded by the JavaFX application platform. For the purposes of testing the process, we've initialised the display with static data all using the same FXML view as follows in a list of 'MyObject' objects.

public class CustomListView
{
    @FXML private ResourceBundle        resources;
    @FXML private URL                   location;
    @FXML private ListView<MyObject>    listView;

    List<MyObject>                      myList              = prepareMyList();
    ObservableList<MyObject>            myObservableList    = FXCollections.observableList( this.myList );


    private void setListView(){

        this.listView.setItems( this.myObservableList );

        listView.setCellFactory(
            new Callback<ListView<MyObject>, javafx.scene.control.ListCell<MyObject>>() {
                @Override
                public ListCell<MyObject> call(ListView<MyObject> listView) {
                    return new CustomListCell();
                }
            }//callback
        );//setCellFactory

    }//setListView

        /**
         * Initializes the controller class.
         */
    @FXML
    void initialize() {

        assert listView != null : "fx:id=\"listView\" was not injected: check your FXML file 'CustomList.fxml'.";

        this.setListView();
    }

        /**
         *  Prepare My List
         *    - Create dummy list of Views and data = MyObject-s
         **/
    private List<MyObject> prepareMyList() {

        final String    FXML_VIEW = "/fxml/ListCell.fxml";
        List<MyObject>  newList   = new ArrayList<>();

        newList.add(new MyObject( FXML_VIEW, "Sunday",    50, Color.RED        ));
        newList.add(new MyObject( FXML_VIEW, "Monday",    60, Color.GREEN      ));
        newList.add(new MyObject( FXML_VIEW, "Tuesday",   20, Color.BLUE       ));
        newList.add(new MyObject( FXML_VIEW, "Wednesday", 90, Color.VIOLET     ));
        newList.add(new MyObject( FXML_VIEW, "Thursday",  30, Color.BLUEVIOLET ));
        newList.add(new MyObject( FXML_VIEW, "Friday",    62, Color.BROWN      ));
        newList.add(new MyObject( FXML_VIEW, "Saturday",  65, Color.GOLD       ));

        return newList;
    }

}//CustomListView

With this framework, the MyObject constructor does the 'work' of loading the FXML view as a CellView object. The view and controller only need to be loaded once. Each setInfo() call only needs to update the data/content for the view.

MyObject is an example or some data and a view:

public class MyObject
{
    private     ViewLoader      view    = null;
    private     String          day;
    private     int             number;
    private     Color           colour;

    public  String getDay() {

        return day;
    }

    public  int getNumber() {

        return number;
    }

    public  Color getColor(){

        return colour;
    }


    public  CellView    getView(){

        return this.view.getView();
    }


    public  MyObject( final String fxmlView, final String d, final int n, final Color c) {

        view    = new ViewLoader( fxmlView );

        day     = d;
        number  = n;
        colour  = c;
    }

}//MyObject class

MyObject retains the "ViewLoader" type to hold the FXML view and controller after loading.

public class ViewLoader
{
    private    String      fxmlStr         = "(none)";
    private    Parent      rootNode        = null;
    private    CellView    viewController  = null;


    public  void loadView( final String urlStr ){

        Parent      root        = null;
        URL         fxmlResource;
        FXMLLoader  fxmlLoader  = null;
        try
        {
            fxmlResource = getClass().getResource( urlStr );
            fxmlLoader = new FXMLLoader( fxmlResource, Resources.getResourceBundle() );

            root = fxmlLoader.load( );

            this.viewController = fxmlLoader.getController();
            this.fxmlStr        = urlStr;
            this.rootNode       = root;
        }
        catch (Exception ex)
        {
            puts( "  * Exception on FXMLLoader.load()");
            puts( "  * "+ex.getMessage());
            puts( "    ----------------------------------------\n");
        }
    }


    public  CellView    getView(){

        return this.viewController;
    }

    public  ViewLoader( final String urlStr ){

        loadView( urlStr );
    }

}//ViewLoader class

This example happily displays a list of subordinate View cells in a JavaFX ListView. The sub-view needs to understand the "MyObject" data. In the final version the data will be decoupled from the sub-view and retrieved from some model object.

Each ListView cell is a unique instance of the specific 'ListCellView' controller in use. Refinements to this pattern would be able to initialise the sub-view with a data source and maintain a clean decoupling of data from a view. Unlike , the sub-views must be loaded manually. You could specify an FXML file name as an invisible label in the main CustomListView.fxml if you want that ability.

Hope others find it useful, or if not at least you can see more of the workings for managing ListView-s.

Community
  • 1
  • 1
will
  • 4,799
  • 8
  • 54
  • 90