This discussion is archived
4 Replies Latest reply: Mar 14, 2013 11:21 AM by James_D RSS

Drag and Drop objects with JavaFX properties

James_D Guru
Currently Being Moderated
Has anyone tried to implement drag and drop functionality with objects containing JavaFX properties? The issue I'm running into is that the objects appear to need to be serializable, and the JavaFX properties are not serializable. I can implement Externalizable instead of Serializable, and basically serialize the values contained by the FX properties, but of course I lose any bindings by doing this (as the listeners underlying the bindings are not serialized).

The [url http://docs.oracle.com/javafx/2/api/javafx/scene/input/Clipboard.html]javadocs for Clipboard state "In addition to the common or built in types, you may put any arbitrary data onto the clipboard (assuming it is either a reference, or serializable. See more about references later)" but I don't see any further information about references (and at any rate, this doesn't really make sense since anything that's serializable would be a reference anyway). Is there a special DataFormat I can use to specify a reference in the local JVM so the reference gets passed without serialization?

I know this might not make sense without a code example; I just thought someone might have come across this closely enough to recognize the problem. I'll try to post a code example tonight...
  • 1. Re: Drag and Drop objects with JavaFX properties
    shakir.gusaroff Expert
    Currently Being Moderated
    Hi John. I think it doesn’t matter if the objects contain properties or not. All javafx nodes contain some properties.
    When you drag and drop you do not drag and drop the node itself. You need only the values of the dragging node.
    You put these values into the drag board. When the data is dropped somewhere you have to create a node from those data.
  • 2. Re: Drag and Drop objects with JavaFX properties
    James_D Guru
    Currently Being Moderated
    No, the point is I want to drag and drop a data object that uses FX properties (since I want to bind to those properties in other parts of the app).

    I'll throw together an example...
  • 3. Re: Drag and Drop objects with JavaFX properties
    James_D Guru
    Currently Being Moderated
    Here's pretty much the simplest example I can come up with. I have a real-world example of needing to do this, but the object model for the real example is quite a bit more complex.

    For the toy example, imagine we have a list of projects, some employees, and we want to assign each project to one or more employees. I have a Project class with a title and assignee. (In real life you can imagine other properties; due date, status, etc.) I'll keep a ListView with a bunch of unassigned projects (titles but assignees equal to null) and then a ListView for each of the employees.

    To assign a project to an employee, the user should be able to drag from the "master" list view to one of the employee list views. When the project is dropped, we'll create a new project, set the assignee, and bind the title of the new project to the title of the project which was dragged. (Remember we want to be able to assign a single project to multiple employees.)

    So here's the first shot:

    A couple of simple model classes

    Project.java
    import java.io.Externalizable;
    import java.io.IOException;
    import java.io.ObjectInput;
    import java.io.ObjectOutput;
    
    import javafx.beans.property.ObjectProperty;
    import javafx.beans.property.SimpleObjectProperty;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    
    
    public class Project {
         private final StringProperty title ;
         private final ObjectProperty<Employee> assignee ;
         
         public Project(String title) {
              this.title = new SimpleStringProperty(this, "title", title);
              this.assignee = new SimpleObjectProperty<Employee>(this, "assignee");
         }
         
         public Project() {
              this("");
         }
         
         public StringProperty titleProperty() {
              return title ;
         }
         
         public String getTitle() {
              return title.get() ;
         }
         
         public void setTitle(String title) {
              this.title.set(title);
         }
         
         public ObjectProperty<Employee> assigneeProperty() {
              return assignee ;
         }
         
         public Employee getAssignee() {
              return assignee.get();
         }
         
         public void setAssignee(Employee assignee) {
              this.assignee.set(assignee);
         }
         
         @Override
         public String toString() {
              String t = title.get();
              Employee emp = assignee.get();
              if (emp==null) {
                   return t;
              } else {
                   return String.format("%s (%s)", t, emp.getName()) ;
              }
         }
    }
    Employee.java
    import java.io.Externalizable;
    import java.io.IOException;
    import java.io.ObjectInput;
    import java.io.ObjectOutput;
    
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    
    
    public class Employee {
         private StringProperty name ;
         public Employee(String name) {
              this.name = new SimpleStringProperty(this,"name", name);
         }
         public StringProperty nameProperty() {
              return name ;
         }
         public String getName() {
              return name.get();
         }
         public void setName(String name) {
              this.name.set(name);
         }
    }
    and the application

    DnDExample.java
    import javafx.application.Application;
    import javafx.beans.value.ChangeListener;
    import javafx.beans.value.ObservableValue;
    import javafx.collections.ObservableList;
    import javafx.event.EventHandler;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.control.ListCell;
    import javafx.scene.control.ListView;
    import javafx.scene.control.TextField;
    import javafx.scene.input.ClipboardContent;
    import javafx.scene.input.DataFormat;
    import javafx.scene.input.DragEvent;
    import javafx.scene.input.Dragboard;
    import javafx.scene.input.MouseEvent;
    import javafx.scene.input.TransferMode;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    import javafx.util.Callback;
    
    public class DnDExample extends Application {
         
         private static final DataFormat PROJECT_DATA_FORMAT = new DataFormat("com.example.project"); // is there something special I can use here?
    
         @Override
         public void start(Stage primaryStage) {
              final HBox root = new HBox(5);
              final ListView<Project> allProjects = new ListView<Project>();
              final Employee fred = new Employee("Fred");
              final Employee ginger = new Employee("Ginger");
              final ListView<Project> fredsProjects = new ListView<Project>();
              final ListView<Project> gingersProjects = new ListView<Project>();
              
              final VBox employeeLists = new VBox(5);
              employeeLists.getChildren().addAll(new Label("Fred's Projects"), fredsProjects, new Label("Ginger's Projects"), gingersProjects);
              
              root.getChildren().addAll(allProjects, employeeLists);
              
              allProjects.setCellFactory(new Callback<ListView<Project>, ListCell<Project>>() {
                   @Override
                   public ListCell<Project> call(ListView<Project> listView) {
                        return new AllProjectListCell();
                   }
              });
              allProjects.setEditable(true);
              
              final EventHandler<DragEvent> dragOverHandler = new EventHandler<DragEvent>() {
                   @Override
                   public void handle(DragEvent dragEvent) {
                        if (dragEvent.getDragboard().hasContent(PROJECT_DATA_FORMAT)) {
                             dragEvent.acceptTransferModes(TransferMode.COPY);
                        }
                   }
              };
              
              fredsProjects.setOnDragOver(dragOverHandler);
              gingersProjects.setOnDragOver(dragOverHandler);
              
              fredsProjects.setOnDragDropped(new DragDropHandler(fred, fredsProjects.getItems()));
              gingersProjects.setOnDragDropped(new DragDropHandler(ginger, gingersProjects.getItems()));
              
              allProjects.getItems().addAll(new Project("Implement Drag and Drop"), new Project("Fix serialization problem"));
              
              Scene scene = new Scene(root, 600, 400);
              primaryStage.setScene(scene);
              primaryStage.show();
              
         }
    
         public static void main(String[] args) {
              launch(args);
         }
         
         private class DragDropHandler implements EventHandler<DragEvent> {
              private final Employee employee ;
              private final ObservableList<Project> itemList;
              DragDropHandler(Employee employee, ObservableList<Project> itemList) {
                   this.employee = employee ;
                   this.itemList = itemList ;
              }
              @Override
              public void handle(DragEvent event) {
                   Dragboard db = event.getDragboard();
                   if (db.hasContent(PROJECT_DATA_FORMAT)) {
                        Project project = (Project) db.getContent(PROJECT_DATA_FORMAT);
                        Project assignedProject = new Project();
                        assignedProject.titleProperty().bind(project.titleProperty());
                        assignedProject.setAssignee(employee);
                        itemList.add(assignedProject);
                   }
              }
         }
         
         private class AllProjectListCell extends ListCell<Project> {
                        
              private TextField textField ;
              
              private final EventHandler<MouseEvent> dragDetectedHandler ;
              
              AllProjectListCell() {               
                   this.dragDetectedHandler = new EventHandler<MouseEvent>() {
                        @Override
                        public void handle(MouseEvent event) {
                             Dragboard db = startDragAndDrop(TransferMode.COPY);
                             ClipboardContent cc = new ClipboardContent();
                             cc.put(PROJECT_DATA_FORMAT, getItem());
                             db.setContent(cc);
                             event.consume();
                        }
                   };
                   
                   this.setEditable(true);
              }
              @Override
              public void updateItem(final Project project, boolean empty) {
                   super.updateItem(project, empty);
                   if (empty) {
                        setText(null);
                        setGraphic(null);
                        setOnDragDetected(null);
                   } else if (isEditing()) {
                        if (textField != null) {
                             textField.setText(getItem().getTitle());
                        }
                        setText(null) ;
                        setOnDragDetected(null);
                        setGraphic(textField);                    
                   } else {
                        setText(project.getTitle());
                        setOnDragDetected(dragDetectedHandler);
                   }
              }
              @Override
              public void startEdit() {
                   super.startEdit();
                   if (!isEmpty()) {
                        textField = new TextField(getItem().getTitle());
                        textField.setMinWidth(this.getWidth()-this.getGraphicTextGap());
                        textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
                             @Override
                             public void changed(
                                       ObservableValue<? extends Boolean> observable,
                                       Boolean oldValue, Boolean newValue) {
                                  if (! newValue) {
                                       getItem().setTitle(textField.getText());
                                       commitEdit(getItem());
                                  }
                             }
                        });
                        setText(null);
                        setGraphic(textField);
                        setOnDragDetected(null);
                   }
                   
              }
              @Override
              public void cancelEdit() {
                   super.cancelEdit();
                   if (!isEmpty()) {
                        setText(getItem().getTitle());
                        setGraphic(null);
                        setOnDragDetected(dragDetectedHandler);
                   } else {
                        setText(null);
                        setGraphic(null);
                        setOnDragDetected(null);
                   }
              }
              @Override
              public void commitEdit(Project project) {
                   super.commitEdit(project);
                   if (!isEmpty()) {
                        setText(getItem().getTitle());
                        setGraphic(null);
                        setOnDragDetected(dragDetectedHandler);
                   }
              }
         }
    }
    If you try to drag from the list on the left to one of the lists on the right, it will fail pretty quickly and tell you that the data need to be Serializable.

    Simply adding "implements Serializable" to the Project and Employee classes doesn't work, as you find that SimpleStringProperty and SimpleObjectProperty are not Serializable. So instead I can use Externalizable:
    public class Project implements Externalizable {
    ...
    @Override
         public void writeExternal(ObjectOutput out) throws IOException {
              out.writeObject(title.get());
              out.writeObject(assignee.get());
         }
    
         @Override
         public void readExternal(ObjectInput in) throws IOException,
                   ClassNotFoundException {
              setTitle((String)in.readObject());
              setAssignee((Employee)in.readObject());
         }
    }
    and
    public class Employee implements Externalizable {
    ...
         @Override
         public void writeExternal(ObjectOutput out) throws IOException {
              out.writeObject(name.get());
         }
         @Override
         public void readExternal(ObjectInput in) throws IOException,
                   ClassNotFoundException {
              setName((String) in.readObject());
         }
    }
    This makes the drag and drop work, but if you drop a project on one of the employee lists, then edit the project title in the master list, the binding is not respected. This is because deserialization creates a new SimpleStringProperty for the title, which is not the property to which the title of the new Project object is bound.

    What I really want to do is to be able to drag and drop an object within the same JVM simply by passing the object by reference, rather than by serializing it. Is there a way to do this? Is there some DataFormat type I need?
  • 4. Re: Drag and Drop objects with JavaFX properties
    James_D Guru
    Currently Being Moderated
    So I can make this work by implementing a "local dragboard" myself. It feels like there should be some way to provide a DataFormat to the standard Dragboard implementation that causes the data to be passed by reference without serialization. The docs seem to hint at this, but either they're incomplete or I'm completely missing something. (I'm thinking of an equivalent of java.awt.datatransfer.DataFlavor.javaJVMLocalObjectMimeType.)

    For the record, my dragboard implementation looks like this:
         import java.util.HashMap;
         import java.util.Map;
         
         public class LocalDragboard {
              
              private final Map<Class<?>, Object> contents ;
              
              private final static LocalDragboard instance = new LocalDragboard();
              
              private LocalDragboard() {
                   this.contents = new HashMap<Class<?>, Object>();
              }
              
              public static LocalDragboard getInstance() {
                   return instance ;
              }
              
              public <T> void putValue(Class<T> type, T value) {
                   contents.put(type, type.cast(value));
              }
              
              public <T> T getValue(Class<T> type) {
                   return type.cast(contents.get(type));
              }
              
              public boolean hasType(Class<?> type) {
                   return contents.keySet().contains(type);
              }
              
              public void clear(Class<?> type) {
                   contents.remove(type);
              }
              
              public void clearAll() {
                   contents.clear();
              }
         }
    And the updated drag and drop code looks like this:
              AllProjectListCell() {               
                   this.dragDetectedHandler = new EventHandler<MouseEvent>() {
                        @Override
                        public void handle(MouseEvent event) {
                             // Set up dummy data on the dragboard else drag and drop won't be initiated
                             Dragboard db = startDragAndDrop(TransferMode.COPY);
                             ClipboardContent cc = new ClipboardContent();
                             cc.putString(getItem().getTitle());
                             db.setContent(cc);
                             LocalDragboard.getInstance().putValue(Project.class, getItem());
                             event.consume();
                        }
                   };
                   
                   this.setEditable(true);
              }
         private class DragDropHandler implements EventHandler<DragEvent> {
              private final Employee employee ;
              private final ObservableList<Project> itemList;
              DragDropHandler(Employee employee, ObservableList<Project> itemList) {
                   this.employee = employee ;
                   this.itemList = itemList ;
              }
              @Override
              public void handle(DragEvent event) {
                   LocalDragboard ldb = LocalDragboard.getInstance();
                   if (ldb.hasType(Project.class)) {
                        Project project = ldb.getValue(Project.class);
                        Project assignedProject = new Project();
                        assignedProject.titleProperty().bind(project.titleProperty());
                        assignedProject.setAssignee(employee);
                        itemList.add(assignedProject);
                   }
              }
         }
    I also had to update the previous code slightly to get the list cell to update when the project title changed, but that was something of a separate issue.

    Anyone know an easier way to do this?

Legend

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