0

I found this

In which the accepted answer is almost perfect, but I would like to instead add the elements to an ArrayList. Is this possible?

I'll try to be concise: I'm creating a JavaFX application and I have an ArrayList containing all my TextFields. I used this list to add an event handler for the Action event to each field, and this handler calls a method that moves focus to the next field in the list (this way the user can press return and will navigate to the next field automatically).

In the same loop I also add an event listener to each field, so that if a field loses focus another method is called to update the form. In this way the user can navigate however they choose (return, tab, mouse click, etc.), and the form will update automatically without the need for a button click when they are finished entering data.

Now, in the update method, I determine which field triggered the event and validate the text in the field and parse a double from it. I then need to do something with that data. Here is where I want to use my ArrayList of "worker functions." I can simply create a parallel ArrayList that matches the index order of the field traversal ArrayList, and call the correct update method.

I want to do it this way because each TextField is tied to a specific member of a specific object for example - the TextField qtyOrdered would be tied to the workOrder.qtyOrdered member, and the TextField materialWidth would be tied to the masterRoll.materialWidth member. These objects would of course have their own accessors and mutators, so I wanted to use the interface to create all these methods beforehand and call the correct one based on the index of the TextField.

I want something like this (pseudocode):

ArrayList<Worker> workerMethods = new ArrayList<>();
workerMethods.add(new Worker() {
    public void work(double input) {
        workOrder.setQtyOrdered(input);
    } 
});
//Add remaining methods...

method updateForm(TextField caller)
{
    get callerIndex;
    // validate user input
    if (valid)
        workerMethods.get(callerIndex).work(input);
}

In fact, using the method I linked to, this works with arrays like so:

workerMethods[callerIndex].update(callerValue);

However, when I use the same syntax for adding to an ArrayList instead it obviously does not work.

I would like to use an ArrayList instead of an array if possible. This is because some fields in the form are removed from the traversableFields ArrayList when they are deactivated so that field traversal will skip these fields when they are hidden from view, and resume in the correct order when they are visible. For example, if the user wants to estimate the length of material on the master roll, they can click a CheckBox to show those fields.

The Action event handler for that CheckBox would then call my updateControls() method which would enable the required fields, set them to visible, and add them to the list of traversable fields. If they uncheck it, the reverse happens.

If I am using an array to store my update methods, the indexes may not match the indexes of my traversableFields List at any given time.

Edit: Added my actual code below (this is a simplified version I am using in a small test environment).

Code for creating the list of traversable TextFields:

// ArrayList of traversable TextFields for form navigation.
private ArrayList<TextField> traversableFields = new ArrayList<>();

// Add all TextFields to the ArrayList.
traversableFields.addAll(Arrays.asList(field1, field2, field3));

// Loop through the ArrayList to add handlers/listeners.
for (int i = 0; i < traversableFields.size(); i++) {
    // Get the Control at index i in the ArrayList.
    TextField currentControl = traversableFields.get(i);

    // Add an event handler so that when the user presses return we can
    // move to the next field in the ArrayList.
    currentControl.addEventHandler(ActionEvent.ACTION, e ->
            moveToNextField(e));

    // Add a listener so that if a field loses focus we update the form.
    currentControl.focusedProperty().
            addListener((obs, wasFocused, isNowFocused) ->
            {
                if (!isNowFocused) {
                    updateForm(currentControl);
                }
            });
}

Code for creating the array of worker methods:

interface Update { public void update(double input); }

class Label1 { public void update(double input) { label1.setText(String.valueOf(input)); } }
class Label2 { public void update(double input) { label2.setText(String.valueOf(input)); } }
class Label3 { public void update(double input) { label3.setText(String.valueOf(input)); } }

Label1 l1 = new Label1();
Label2 l2 = new Label2();
Label3 l3 = new Label3();

Update[] updateFunctions = new Update[] {
        new Update() { public void update(double input) { l1.update(input); } },
        new Update() { public void update(double input) { l2.update(input); } },
        new Update() { public void update(double input) { l3.update(input); } }
        };

The snippet above is basically exactly what they did in the link I posted. As I mentioned earlier, this works. I can call updateFunctions[callerIndex].update(callerValue); and it does what I want. Here is the method called by the event handler:

// Move to the next field in the form. Called by the Action Event for each
// field in the traversableFields ArrayList.
private void moveToNextField(ActionEvent event)
{
    // Get the TextField that triggered the event.
    TextField caller = (TextField)event.getSource();
    
    // Get the index of this Control from the ArrayList.
    int callerIndex = traversableFields.indexOf(caller);

    // If we have reached the end of the list then we move to the
    // first field in the list.
    if (callerIndex == traversableFields.size() - 1)
        traversableFields.get(0).requestFocus();
    // Otherwise move to the next field.
    else
        traversableFields.get(++callerIndex).requestFocus();
}

and here is the method called by the focus listener:

// Update the form. This will contain the majority of functional code
// and will be called automatically when any TextField loses focus.
private void updateForm(TextField caller)
{
    // Get the index of the TextField
    int callerIndex = traversableFields.indexOf(caller);
    
    // Get the CSS id of the TextField for testing purposes.
    String callerID = caller.getId();
    
    // Get the TextField contents.
    String callerText = caller.getText();
    
    // Flag variable.
    boolean fieldEmpty = callerText.equals("");
    
    // Double for storing parsed input.
    double callerValue = 0;
    
    // If the field is not empty, ensure that it contains valid data before
    // proceeding.
    if (!fieldEmpty)
    {
        try
        {
            callerValue = Validation.getDouble(callerText);
            updateFunctions[callerIndex].update(callerValue);
            clearError(caller);
        }
        catch (Exception e)
        {
            // If the input was invalid, alert the user and move focus
            // back to the offending TextField.
            System.out.println("Invalid input.");
            markEntryInvalid(caller);
            caller.requestFocus();
        }
    }

    // Trace statements.
    System.out.println("updateForm() called by " + callerID);
    System.out.println("Parsed value was " + callerValue);
}

However, I want to use an ArrayList instead of an array. But if I do this:

ArrayList<Update> updateMethods = new ArrayList<>();

updateMethods.add(new Update() { public void update(double input) { l1.update(input); } });

I get <identifier> expected error.

MrClucky
  • 17
  • 2
  • `Worker` needs a common method (e.g. `#work`) that is overridden. You then call the interface's method, which delegates to the implementation's method body. You can also avoid the anonymous classes here and still have a `List`. Also, post your actual code you're having issues with, not pseudo-code that leaves us guessing the issue. – Rogue Feb 26 '21 at 16:05
  • Yo have called 2 different methods of `worker` in your samoles. In the `ArrayList` sample you used work method while in the array sample you used `update` method! – Tashkhisi Feb 26 '21 at 16:10
  • Where are you putting the code in the last code block in the question? It just sounds like you have executable code outside of a method. – James_D Feb 26 '21 at 18:27
  • Regarding method `moveToNextField`. I suggest using the `properties` map of class `Node`. The map key could be a string like "next" and the map value would be the `TextField` that needs to gain focus. Then no need for a `List` to determine which is the next focusable `TextField`. – Abra Feb 27 '21 at 06:39

3 Answers3

1

If I understand your question correctly, what you want to know is how to initialize an ArrayList with a given collection of values?

That would be done like this:

List<Workers> myWorkers = new ArrayList<>(
    Arrays.asList(
        new Worker() { ... }, 
        new Worker() { ... }
    )
);

Arrays.asList(element1, element2, ...) returns an immutable list of these elements. And the constructor of ArrayList can take a Collection to initialize the list.

Of course, you can also create the list and add single elements. The resulting list is the same:

List<Workers> myWorkers = new ArrayList<>();
myWorkers.add(new Worker() { ... });
myWorkers.add(new Worker() { ... });

In both cases, the resulting list can be modified using add and remove methods.

Two notes on the usecase, though:

First, a Map might be better suited for your usecase, because you don't need to take care about indexes:

// initialization:
Map<TextField, Worker> workers = new HashMap<TextField, Worker>();
workers.put(textfield1, worker1);
workers.put(textfield2, worker2);
// ...

void updateForm(TextField caller) {
    workers.get(caller).work();
}

Second, if your Worker interface only has one method, since Java 8 you can use it as a functional interface with a closure. So the initialization would look like this:

Map<TextField, Worker> workers = new HashMap<TextField, Worker>();
workers.put(textfield1, input -> { /* do something with input here */ });
workers.put(textfield2, input -> { /* do something with input here */ });

And maybe your Worker interface is even not needed and you can just use Consumer<InputType> instead.

Stefan Winkler
  • 3,871
  • 1
  • 18
  • 35
  • Your method of adding to the list during initialization works. Why does it not work the way I posted? Can I not declare the ArrayList and then add to it in my initialize method for the application? Edited to add: the map thing is amazing, thank you! I will have to look into the Consumer because I don't know anything about that. However, I will say that this is for a final project for school so I might stick with what we learned in class and improve upon it after I submit it for grading. – MrClucky Feb 26 '21 at 16:53
  • So If I do it your way, I can't add to the ArrayList or remove from it after initialization? That defeats the purpose. I suppose I am going about this all the wrong way... I need to be able to sync this list up with my list of traversable fields. – MrClucky Feb 26 '21 at 17:27
  • Updated my answer. Note the comment under your question. If you don‘t do the initialization in one expression, then the code calling the initial add methods must be in a method, not at class level. – Stefan Winkler Feb 27 '21 at 12:42
0

You can create List<Runnable> and execute the code in the current thread or in the parallel thread:

List<Runnable> workerMethods = List.of(
        () -> System.out.println("worker 1"),
        () -> System.out.println("worker 2"));

// execute code in the current thread
workerMethods.get(0).run(); // worker 1

// execute code in a new thread
new Thread(workerMethods.get(1)).start(); // worker 2

Similarly, you can create Map<String, Runnable>:

Map<String, Runnable> workerMethods = Map.of(
        "w1", () -> System.out.println("worker 1"),
        "w2", () -> System.out.println("worker 2"));

// execute code in the current thread
workerMethods.get("w1").run(); // worker 1

// execute code in a new thread
new Thread(workerMethods.get("w2")).start(); // worker 2
0

Might I suggest an entirely different approach?

Why not use the user data field of the TextField to point to your own object that can handle all of your needs.

e.g.

class FieldContext {
    TextField previous;
    TextField next;
    Update updater;
    // add whatever else you need, e.g. validation interface etc.
}

so when you get an event from the TextField you can just call:

FieldContext fieldCtx = (FieldContext)((TextField)event.getSource()).getUserData();

and then you are free to handle whatever specific event processing is needed having the context information for the TexTField.

I find a lot of people overlook the fact that you have a field for user data on the controls. I find it can simplify things quite a bit

swpalmer
  • 3,890
  • 2
  • 23
  • 31