0

I have an implementation where I want to reload data while scrolling a JList after scrolling every 100 records. I have a large database, and it takes around 7 seconds to load the JList. Therefore, a wait dialog will be displayed to the user while data is loaded in the background.

I am facing two issues with the following code:

  1. For every single mouse scroll, three records are scrolled instead of scrolling a single record.
  2. If I continue scrolling while the reload is in progress (that is if I continued moving the mouse wheel), the scroll action event is stored somewhere, and once the reload is completed, the stored action event will be executed. For example, if I keep scrolling six times while records are reloading, the scrollbar won't move at the moment because the wait dialog is present, but once the reload is complete, it will scroll six records.

My goal is to remove whatever mouse scroll/wheel movement events waiting for execution once the data is loaded or even a call is made to reload the data.

    JScrollPane calculateScrollPane = new JScrollPane();
    JList calculateList = new JList();
  
    MouseWheelListener mouseListener = new MouseWheelListener() {
      @Override
      public void mouseWheelMoved(MouseWheelEvent e) {
          int firstVisibleIndex = calculateList.getFirstVisibleIndex();
          WaitDialog wait = new WaitDialog();
          wait.start();
        if(firstVisibleIndex % 100 == 0)
            reloadData(e);
        wait.stop();
      
      }
    };
    calculateScrollPane.addMouseWheelListener(mouseListener);

I tried storing the firstIndexAfterReload = firstVisibleIndex; and resetting the calculateList first visible index using calculateList.ensureIndexIsVisible(firstIndexAfterReload); This resets the firstVisibleIndex only once. If I have rigorously scrolled multiple times then firstIndexAfterReload is also reset.

1
  • 2
    1. Disable the list before you start loading; 2. Use a SwingWorker to load the data; 3. Keep track of the last/next trigger row so you’re not reliant only internet scroll; 4. Aldo consider that a user can scroll the list using the keyboard Commented Oct 26, 2023 at 19:31

1 Answer 1

1

Let's start with some obvious issues

  1. Users can (and will) scroll through the list using the keyboard as well as the mouse, so, a MouseWheelListener (or any sort of mouse based listener) is a bad place to start
  2. Swing is single threaded and not thread safe, this means that you shouldn't be performing long running or blocking operations within the context of the Event Dispatching Thread and you shouldn't be updating the UI or anything the UI relies on from outside of the Event Dispatching Thread context. See Concurrency in Swing for more details

So, what's the answer?

Well, for me, I started with the veriticalScrollBar of the JScrollPane and added an AdjustmentListener to it. This will notify me when the vertical scroll bar position changes. This might not be the "best" option, but it was the obvious one to me. This means we're not relient on the monitoring the keyboard and mouse, instead, we're monitoring the state (or part of the state) of the scroll pane itself.

Next, I set up a "trigger" which was, in my test, 5 rows less then a full page of data. That is, in my example, a page was 20 rows, so my trigger was always 5 rows less (15/35/55/75/95), this means that we're attempting to load a new page slightly early.

When the AdjustmentListener was triggered, I checked the JList#getLastVisibleIndex property and if the values mathched, I then used a SwingWorker to off loading the loading of the data to a different thread, but which allows me an easy way to re-sync that data back to the EDT. See Worker Threads and SwingWorker for more details

This idea might look something like...

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingWorker;

public class Main {
    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {
        public TestPane() {
            DefaultListModel<String> model = new DefaultListModel<>();
            for (int index = 0; index < 20; index++) {
                model.addElement(Integer.toString(index));
            }
            JList<String> list = new JList<>(model);
            list.setVisibleRowCount(10);

            JScrollPane scrollPane = new JScrollPane(list);
            scrollPane.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() {

                private int triggerRow = 15;

                @Override
                public void adjustmentValueChanged(AdjustmentEvent e) {
                    int lastRow = list.getLastVisibleIndex();
                    if (lastRow == triggerRow) {
                        int startingValue = model.getSize();
                        triggerRow += 20;
                        SwingWorker<List<String>, List<String>> worker = new SwingWorker<>() {
                            @Override
                            protected List<String> doInBackground() throws Exception {
                                List<String> values = new ArrayList<>(20);
                                for (int index = 0; index < 20; index++) {
                                    values.add(Integer.toString(startingValue + index));
                                }
                                // Artificial 7 second delay...
                                Thread.sleep(7000);
                                publish(values);
                                return values;
                            }

                            @Override
                            protected void process(List<List<String>> chunks) {
                                for (List<String> values : chunks) {
                                    model.addAll(values);
                                }

                            }
                        };
                        worker.execute();
                    }
                }
            });

            setLayout(new BorderLayout());
            add(scrollPane);
        }
    }
}

Now, please, understand. This is simplified solution. I've deliberately set the setVisibleRowCount to a size less then my trigger row.

You might be able to change it so that if the last row is >= to the trigger row, you would start loading more pages, but, you'd probably need some way to determine if a loading is in progress first and possibly modify the logic which determines the next trigger point so that it's more efficient in a more dynamic environment.

I'd also consider rolling a lot of this up into some kind of "pagination" workflow, so maybe a dedicated class you could pass the scroll pane and list into and have it manage all the core workflow and maybe trigger an observer to load the data, but that's me.

Oh, and might also consider making use of JLayer to provide some kind of visual feedback, see How to Decorate Components with the JLayer Class for more details

Not the answer you're looking for? Browse other questions tagged or ask your own question.