Listview issue in javafx

68 views Asked by At

I have a javafx project. In the main view i have a listview with some tags item and a delete button. The whole project is connected to a postgresql db. The tags are stored as ( user_id, name, color). When i click the delete button, the tag is deleted from the db, but i get this error "No results were returned by the query." while i have items in db and the list is updated. Why do i keep getting this error?

public class TagsController extends SQLConnection implements Initializable {

    @FXML
    public Button forest, shop, timeline, tags, rewards, settings, friends, button;
    @FXML
    public VBox menu, vbox;
    @FXML
    public Label gold;

    @FXML
    public ListView<Tag> listView;


    public Stage stage;
    public Scene scene;

    ObservableList<Tag> list = FXCollections.observableArrayList();

    List<Tag> listOfTags;

    {
        try {
            listOfTags = new ArrayList<>(listOfTags());
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    User user = new User();



    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {

        //set the gold value
        gold.setText(String.valueOf(getGold(user.getId())));

        //set the image of the menu button
        InputStream in = getClass().getResourceAsStream("/images/menu.png");
        Image image = new Image(in);
        ImageView imageView = new ImageView(image);
        button.setGraphic(imageView);
        button.setMaxSize(40, 40);
        button.setMinSize(40, 40);
        button.setContentDisplay(ContentDisplay.TOP);
        imageView.fitWidthProperty().bind(button.widthProperty());
        imageView.setPreserveRatio(true);

        //hide the menu buttons
        forest.setVisible(false);
        shop.setVisible(false);
        timeline.setVisible(false);
        tags.setVisible(false);
        rewards.setVisible(false);
        settings.setVisible(false);
        friends.setVisible(false);
        menu.setVisible(false);

        listView.setItems(list);

        listView.setCellFactory(param -> new ListCell<>() {
            private final Button deleteButton = new Button();


            @Override
            protected void updateItem(Tag tag, boolean empty) {
                super.updateItem(tag, empty);

                if (empty || tag == null) {
                    setText(null);
                    setGraphic(null);
                } else {
                    setText(tag.getName());
                    setGraphic(deleteButton);
                    setStyle("-fx-background-color: #deaef4; -fx-padding: 20px; -fx-border-width: 1px; -fx-border-color: #cccccc; -fx-font-family: System Italic; -fx-font-size: 19; -fx-text-fill: #f5f599;");


                    InputStream in1 = getClass().getResourceAsStream("/images/bin.png");
                    Image image1 = new Image(in1);
                    ImageView imageView1 = new ImageView(image1);
                    imageView1.setFitHeight(40);
                    imageView1.setFitWidth(40);
                    deleteButton.setGraphic(imageView1);
                    deleteButton.setStyle("-fx-background-color: #deaef4;");

                    deleteButton.setOnAction(actionEvent -> {

                        removeTag(user.getId(), tag.getName());
                        list.remove(listView.getSelectionModel().getSelectedItem());
                        listView.getItems().remove(tag);
                        refreshTags();
                    });
                }
            }
        });

        refreshTags();

    }

    private void refreshTags(){

        listView.getItems().removeAll();
        list.removeAll();
        try {
            Connection connection = connection();
            Statement statement = connection.createStatement();
            String query = "SELECT * FROM tags WHERE user_id = " + user.getId();
            ResultSet resultSet = statement.executeQuery(query);
            while (resultSet.next()) {

                listView.getItems().add(new Tag(
                        user.getId(),
                        resultSet.getString("name"),
                        resultSet.getString("color"))
                );
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

}
1

There are 1 answers

0
Basil Bourque On

You seem to be working too hard. The point of using ObservableList is that when you add or delete to that collection, the subscribed listener objects will be informed of the change. So there is no need for you to replacing all elements. Just delete the selected Tag object from the observable list, and the ListView will update itself automatically. See example code below.

As discussed in the Comments, your code needs to be re-organized.

To make your code more comprehensible, learn to think in terms of separation of concerns. Each piece of code should have one single responsibility.

So… Code for interacting with the user should not know anything about SQL and databases. Code for managing the lifecycle of your app (launching, exiting, etc.) should not know the details of the forms appearing in your user-interface. And so on.

Let's write a revamped example. Here we define a simple Tag class as a record.

package work.basil.example.exfxlistview;

import java.util.UUID;

public record Tag( String title , UUID id ) {}

In your app, the key re-org is separating the database work from the user-interface (UI) work. In the Java world, we commonly seen this done via the Repository pattern. Define an interface with features needed by your app for interacting with a database.

In our case with this example, we need just two operations:

  • Fetch all Tag objects.
  • Delete a single Tag object.

So our interface looks like this:

package work.basil.example.exfxlistview;

import java.util.List;

public interface Repository
{
    List < Tag > fetchAllTags ( );

    boolean deleteTag ( final Tag tag );
}

One advantage of using an interface here is that we can write a simple hard-coded implementation using an in-memory collection of Tag objects. No need to get distracted by messy SQL code now. We can develop our UI without any database involvement.

package work.basil.example.exfxlistview;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class Repository_ListBacked implements Repository
{
    // Member fields.
    private final List < Tag > tags =
            new ArrayList <>(
                    List.of(
                            new Tag( "Alice" , UUID.fromString( "be5abd37-e737-440d-b6f6-43ba88612574" ) ) ,
                            new Tag( "Bob" , UUID.fromString( "cd8443ac-c0f8-4a7f-b864-6ee5bb6bd0a4" ) ) ,
                            new Tag( "Carol" , UUID.fromString( "798c300e-2733-46e7-a8b2-cee2cc48afd5" ) ) ,
                            new Tag( "Davis" , UUID.fromString( "f7711c12-0f7f-470e-9fbe-c0f1cfb8f848" ) ) ,
                            new Tag( "Edith" , UUID.fromString( "57f7352f-e705-49ac-b62e-07f1d8ae5341" ) ) ,
                            new Tag( "François" , UUID.fromString( "5b257c5e-b79f-46b8-b056-133f984cdc2f" ) )
                    )
            );

    // Constructors.
    public Repository_ListBacked ( ) { }

    // Implements Repository interface.
    @Override
    public List < Tag > fetchAllTags ( ) { return List.copyOf( this.tags ); }

    @Override
    public boolean deleteTag ( final Tag tag ) { return this.tags.remove( tag ); }
}

Now we can build our UI to use this hard-coded repository. Our entire UI for displaying and deleting tags is contained in its own class. This class extends one of the JavaFX layout managers.

Our class here fetches Tag objects from the repository. Those Tag objects are then transferred to an observable list. We use that observable list to back a ListView widget.

This UI class will need to interact with an instance of Repository. We pass an instance to the UI class constructor. This is one way of doing dependency injection, supplying a needed resource or service without the consuming class from having to know how to get it or where it came from.

Notice how the Delete button uses the Repository object to:

  • Populate its ListView
  • Delete the Tag object of the selected row.

As a nicety, we enable/disable our Delete button as the user selects/deselects a row. Notice how we add a selection listener to make that happen.

package work.basil.example.exfxlistview;

import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.VBox;
import javafx.util.Callback;

import java.time.Instant;
import java.util.Objects;

public class TagListEditorLayout extends VBox
{
    private final Repository repository;

    public TagListEditorLayout ( final Repository repository )
    {
        this.repository = repository;
        this.create( );
    }

    private void create ( )
    {
        // Widgets
        ObservableList < Tag > tags = FXCollections.observableArrayList( this.repository.fetchAllTags( ) );
        ListView < Tag > tagsListView = new ListView <>( tags );
        tagsListView.setCellFactory( new TagCellFactory( ) );

        Button deleteButton = new Button( "Delete" );
        deleteButton.setDisable( true );

        // Behavior

        // Enable button when a Tag element of list view is selected. Otherwise, disable Delete button.
        tagsListView.getSelectionModel( ).selectedItemProperty( ).addListener(
                ( ObservableValue < ? extends Tag > _ , Tag previouslySelectedTag , Tag newlySelectedTag ) ->
                        deleteButton.setDisable( Objects.isNull( newlySelectedTag ) )
        );

        deleteButton.setOnAction(
                ( ActionEvent event ) -> {
                    System.out.println( event );  // Debug.
                    Tag tag = tagsListView.getSelectionModel( ).selectedItemProperty( ).get( );
                    if ( Objects.nonNull( tag ) )
                    {
                        if ( repository.deleteTag( tag ) )
                        {
                            tags.remove( tag );
                            System.out.println( this.repository.fetchAllTags( ) );  // Debug.
                        } else
                        {
                            System.out.println( "ERROR Repository failed to delete tag, so we interrupted response to user clicking Delete button. " + Instant.now( ) );
                        }
                    } else
                    {
                        System.out.println( "ERROR Should not be reachable. Delete button should work only if a Tag element is selected in list view. " + Instant.now( ) );
                    }
                }
        );

        // Arrange
        this.getChildren( ).addAll( tagsListView , deleteButton );
        this.setSpacing( 10 );
        this.setPadding( new Insets( 10 , 10 , 10 , 10 ) );
    }

    static class TagCellFactory implements Callback < ListView < Tag >, ListCell < Tag > >
    {
        @Override
        public ListCell < Tag > call ( ListView < Tag > param )
        {
            return new ListCell <>( )
            {
                @Override
                public void updateItem ( Tag tag , boolean empty )
                {
                    super.updateItem( tag , empty );
                    if ( empty || tag == null )
                    {
                        setText( null );
                    } else
                    {
                        setText( tag.title( ) );
                    }
                }
            };
        }
    }
}

Look at how we respond to a button click. First we delete the matching object from our repository. The crucial part is how that deletion method returns a boolean to indicate success or failure. If successful, only then do we delete from our observable list. After deleting from the observable list, the ListView object automatically updates its display to omit the removed item.

And we need to harness all this code to make an app. We do this here by writing a class named App that extends from the JavaFX Application class. This App class manages the lifecycle of a JavaFX app, by overriding methods such as start and stop.

Notice how we instantiate a particular implementation of Repository. Then we instantiate our layout, a TagListEditorLayout object, and wrap into a Scene (viewport of window) placed in a Stage (window).

package work.basil.example.exfxlistview;

import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import javax.sql.DataSource;
import java.time.Instant;
import java.util.Objects;

public class App extends Application
{
    @Override
    public void init ( )
    {
        // For more info:  https://stackoverflow.com/a/76800602/642706
        String message = "INFO - The `App#init` method is running on separate thread " + Thread.currentThread( ).threadId( ) + " “" + Thread.currentThread( ).getName( ) + "”" + " at " + Instant.now( );
        System.out.println( message );
    }

    @Override
    public void start ( Stage stage )
    {
        String message = ( "INFO - The `App#start` method is running on JavaFX Application thread " + Thread.currentThread( ).threadId( ) + " “" + Thread.currentThread( ).getName( ) + "”" + " at " + Instant.now( ) );
        System.out.println( message );

        // -------------|  Resources  |---------------------------
        Repository repository = new Repository_ListBacked( );  

        // -------------|  GUI  |---------------------------
        Parent parent = new TagListEditorLayout( repository );
        Scene scene = new Scene( parent , 320 , 240 );
        stage.setTitle( "Tags Editor" );
        stage.setScene( scene );
        stage.show( );
    }

    @Override
    public void stop ( ) throws Exception
    {
        String message = ( "INFO - The `App#stop` method is running on JavaFX Application thread " + Thread.currentThread( ).threadId( ) + " “" + Thread.currentThread( ).getName( ) + "”" + " at " + Instant.now( ) );
        System.out.println( message );
    }

    public static void main ( String[] args )
    {
        javafx.application.Application.launch( );
    }
}

Now we can run our app. Select a row to enable the Delete button.

screenshot of demo app

Hit the Delete button to try our logic. You should see the selected row disappear as we delete the Tag object from the observable list.

Great! It all works. But what you really wanted was database access. So we need another implementation of Repository to work with a database. Here we use the H2 Database Engine, for convenience.

package work.basil.example.exfxlistview;

import javax.sql.DataSource;
import java.sql.*;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

public class Repository_H2 implements Repository
{
    // Member fields.
    private final DataSource dataSource;

    // Constructors.
    public Repository_H2 ( final DataSource dataSource ) { this.dataSource = dataSource; }

    // Implements `Repository`.
    @Override
    public List < Tag > fetchAllTags ( )
    {
        String sql = """
                SELECT * FROM tag_ 
                ;
                """;
        ArrayList < Tag > tags = new ArrayList <>( );
        try (
                Connection connection = this.dataSource.getConnection( ) ;
                Statement statement = connection.createStatement( ) ;
                ResultSet resultSet = statement.executeQuery( sql ) ;
        )
        {
            while ( resultSet.next( ) )
            {
                UUID id = resultSet.getObject( "id_" , UUID.class );
                String title = resultSet.getString( "title_" );
                Tag tag = new Tag( title , id );
                tags.add( tag );
            }
        } catch ( SQLException e )
        {
            e.printStackTrace( );
        }
        tags.trimToSize( );
        return List.copyOf( tags );
    }

    @Override
    public boolean deleteTag ( final Tag tag )
    {
        boolean deletionSuccessful = false;
        String sql = """
                DELETE FROM tag_ 
                WHERE id_ = ?
                ;
                """;

        try
                (
                        Connection connection = this.dataSource.getConnection( ) ;
                        PreparedStatement preparedStatement = connection.prepareStatement( sql ) ;
                )
        {
            Objects.requireNonNull( tag );
            UUID id = Objects.requireNonNull( tag.id( ) );
            preparedStatement.setObject( 1 , id );
            int rowsAffected = preparedStatement.executeUpdate( );
            deletionSuccessful = ( rowsAffected > 0 );
            if ( deletionSuccessful )
            {
                System.out.println( "INFO - Successfully deleted `tag_` row for ID: " + id + " at " + Instant.now( ) );
            } else
            {
                System.out.println( "ERROR - No `tag_` row` deleted for id: " + id + " at " + Instant.now( ) );
            }
        } catch ( SQLException e )
        {
            e.printStackTrace( );
        }
        return deletionSuccessful;
    }
}

And change our App class to use this alternative Repository implementation.

        // -------------|  Resources  |---------------------------
//        Repository repository = new Repository_ListBacked( );  // Can be swapped for `Repository_H2`.

        DataSource dataSource = Objects.requireNonNull( Environment.fetchDataSourceForH2( ) );
        new DatabasePrep_H2( dataSource ).prepare( );
        Repository repository = new Repository_H2( dataSource );

At this point, we can more easily debug any database problems. We know our app worked perfectly well without any database. So if we experience any problems, we know it comes from our database work. (Indeed, we could even write separate code to test our database access code without using our UI at all. But for now, let's try our UI code.)

Notice in code above that we use a DataSource object to store our database login credentials, such as user name, user password, the name of the database, the server address, and so on. Using DataSource gives you flexibility later, letting you externalize these details outside your source code. For example, a DataSource object can be retrieved from a naming/directory service such as an LDAP server. This way, you don't need to recompile your app just for a password change.

Here's class to mimic retrieving a DataSource object.

package work.basil.example.exfxlistview;

import javax.sql.DataSource;

// To interact with the host OS, and with network environs such as naming/directory services.
public class Environment
{
   static DataSource fetchDataSourceForH2 ( )
    {
        // In real work, a `DataSource` implementation would come from a naming/directory service such as LDAP server rather than hard-coding.
        org.h2.jdbcx.JdbcDataSource ds = new org.h2.jdbcx.JdbcDataSource( );
        ds.setURL( "jdbc:h2:mem:ex_fx_listview_db;DB_CLOSE_DELAY=-1" );
        ds.setUser( "scott" );
        ds.setPassword( "tiger" );
        return ds;
    }

}

Of course we need to also set up a database. So write a class for that. This class creates the database, creates our tag_ table, and populates that tables with some example data.

In real work, I would use a database migration tool like Flyway or Liquibase to run SQL scripts to create the tables and pre-populate rows.

package work.basil.example.exfxlistview;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.UUID;

public class DatabasePrep_H2
{
    private final DataSource dataSource;

    public DatabasePrep_H2 ( final DataSource dataSource )
    {
        this.dataSource = dataSource;
    }

    void prepare ( )
    {
        this.createTagTable( );
        this.populateTagTable( );
    }

    private void createTagTable ( )
    {
        String sql = """
                CREATE TABLE IF NOT EXISTS tag_ 
                ( 
                    id_ UUID PRIMARY KEY , 
                    title_ VARCHAR( 255 ) 
                ) 
                ;
                """;
        try (
                Connection connection = this.dataSource.getConnection( ) ;
                Statement statement = connection.createStatement( )
        )
        {
            statement.execute( sql );
            System.out.println( "Table created successfully." );
        } catch ( SQLException e )
        {
            System.out.println( "Error creating table: " + e.getMessage( ) );
        }
    }

    private void populateTagTable ( )
    {
        String sql = """
                INSERT INTO tag_ ( id_ , title_ ) 
                VALUES ( ? , ? )
                ;
                """;
        try (
                Connection connection = this.dataSource.getConnection( ) ;
                PreparedStatement preparedStatement = connection.prepareStatement( sql )
        )
        {
            // Inserting multiple rows using batch processing
            final List < Tag > tags =
                    List.of(
                            new Tag( "Alice" , UUID.fromString( "be5abd37-e737-440d-b6f6-43ba88612574" ) ) ,
                            new Tag( "Bob" , UUID.fromString( "cd8443ac-c0f8-4a7f-b864-6ee5bb6bd0a4" ) ) ,
                            new Tag( "Carol" , UUID.fromString( "798c300e-2733-46e7-a8b2-cee2cc48afd5" ) ) ,
                            new Tag( "Davis" , UUID.fromString( "f7711c12-0f7f-470e-9fbe-c0f1cfb8f848" ) ) ,
                            new Tag( "Edith" , UUID.fromString( "57f7352f-e705-49ac-b62e-07f1d8ae5341" ) ) ,
                            new Tag( "François" , UUID.fromString( "5b257c5e-b79f-46b8-b056-133f984cdc2f" ) )
                    );
            for ( Tag tag : tags )
            {
                preparedStatement.setObject( 1 , tag.id( ) );
                preparedStatement.setString( 2 , tag.title( ) );
                preparedStatement.addBatch( );
            }
            int[] affectedRows = preparedStatement.executeBatch( );
            System.out.println( "Rows inserted successfully. Affected rows: " + affectedRows.length );
        } catch ( SQLException e )
        {
            System.out.println( "Error inserting rows: " + e.getMessage( ) );
        }
    }
}

And, voilà, we have fully functioning database-backed list editor.

Later, in further development, if you started having problems, you could always switch back to the list-backed repository to eliminate the database from your debugging considerations.


For completeness, here is my POM & module-info.java.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>work.basil.example</groupId>
    <artifactId>ExFxListView</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>ExFxListView</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--        Remove JavaFX dependency if using a JVM that comes bundled with OpenJFX libraries. -->
        <!--        Otherwise, enable dependency.-->
        <!--        <dependency>-->
        <!--            <groupId>org.openjfx</groupId>-->
        <!--            <artifactId>javafx-controls</artifactId>-->
        <!--            <version>22</version>-->
        <!--        </dependency>-->

        <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.2</version>
            <scope>test</scope>
        </dependency>

        <!-- http://h2database.com/html/cheatSheet.html-->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>2.2.224</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.12.1</version>
                <configuration>
                    <source>22</source>
                    <target>22</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-maven-plugin</artifactId>
                <version>0.0.8</version>
                <executions>
                    <execution>
                        <!-- Default configuration for running with: mvn clean javafx:run -->
                        <id>default-cli</id>
                        <configuration>
                            <mainClass>work.basil.example.exfxlistview/work.basil.example.exfxlistview.App</mainClass>
                            <launcher>app</launcher>
                            <jlinkZipName>app</jlinkZipName>
                            <jlinkImageName>app</jlinkImageName>
                            <noManPages>true</noManPages>
                            <stripDebug>true</stripDebug>
                            <noHeaderFiles>true</noHeaderFiles>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
module work.basil.example.exfxlistview {
    requires javafx.controls;
    requires java.naming;
    requires java.sql;
    requires com.h2database;

    exports work.basil.example.exfxlistview;
}