Search code examples
javaswingjdatechooser

propertychangelistener on a JDateChooser gets triggered 2 times additionally even when the listener is detached while setting the date programatically


I am using JDateChooser for a java swing project I am developing and in this, the date could be set in two ways: by the end user or programmatically.

So I have defined a propertychangelistener in the respective class(the variable trig is initialised to zero and maintains track on how many times a property change is listened).

public class WriteEntry{
private int trig=0;
private Date currentDate = new Date();
public JDateChooser dateChooser = new JDateChooser();
public CustomDate selectedDate = DateConverter.convertDate(currentDate);
private static String filename = StorageSpace.currentpath+CurrentUser.getInstance().getUserName()+"\\"+
        Integer.toString(selectedDate.getYear())+"\\"
          +Integer.toString(selectedDate.getMonth())+"\\"+Integer.toString(selectedDate.getDay())+".txt";
private JLabel dayinfo = new JLabel("");
private JTextArea contentfield = new JTextArea("");
private PropertyChangeListener lis = new PropertyChangeListener(){
      @Override
      public void propertyChange(PropertyChangeEvent e) {
          System.out.println("triggered "+trig++);
            if(dateBoundary())  {
                selectedDate = DateConverter.convertDate(dateChooser);
                filename = StorageSpace.currentpath+CurrentUser.getInstance().getUserName()+"\\"+
                        Integer.toString(selectedDate.getYear())+"\\"
                          +Integer.toString(selectedDate.getMonth())+"\\"+Integer.toString(selectedDate.getDay())+".txt";
            }
            else {
                updateDateChooser(selectedDate);
            }
            if(isAlreadyWritten())
            {
                try {
                    updateEditFields(selectedDate, "content");
                } catch (IOException e1) {
                    e1.printStackTrace();
                }   
            }
            else
            {       
                contentfield.setText("Start writing here");
                dayinfo.setText("You are making entry for: "+ new SimpleDateFormat("dd/MM/yyyy").format(dateChooser.getDate()));        
            }
      }      
    };
WriteEntry() //constructor
{
dateChooser.setDateFormatString("dd MM yyyy");
dateChooser.addPropertyChangeListener(lis);
updateEditFields(DateConverter.convertDate(currentDate), "Start");
}
}

And here is the code for dateBoundary():

public static boolean dateBoundary() {
    Object[] option = {"I get it","My Bad!"};
    if(dateChooser.getDate().compareTo(currentDate)>0) {
        JOptionPane.showOptionDialog(HomePage.getFrame(),"message1",
                "",JOptionPane.DEFAULT_OPTION,JOptionPane.ERROR_MESSAGE,null,option,option[0]);
        return false;
    }
    if(dateChooser.getDate().compareTo(DateConverter.convertfromCustom(CurrentUser.getInstance().getDob()))<0){
JOptionPane.showOptionDialog(HomePage.getFrame(),"message2",
                "",JOptionPane.DEFAULT_OPTION,JOptionPane.ERROR_MESSAGE,null,option,option[0]);
        return false;
    }
    return true;
}

Code for isAlreadyWritten():

public static boolean isAlreadyWritten() {
    File f = new File(filename);
    if(f.length()!=0)
    {
        Object[] option = {"Read","Edit"};
        JOptionPane.showOptionDialog(HomePage.getFrame(),"You already updated diary for this day. Do you want to edit?",
                "",JOptionPane.DEFAULT_OPTION,JOptionPane.INFORMATION_MESSAGE,null,option,option[0]);
        return true;
    }
    else
        return false;
}

Code for updateDateChooser():

public static void updateDateChooser(CustomDate date) {
dateChooser.removePropertyChangeListener(lis); //to stop it from getting triggered when date is set programatically
dateChooser.setDate(DateConverter.convertfromCustom(date));
dateChooser.addPropertyChangeListener(lis);
}

Code for updateEditFields():

public static void updateEditFields(CustomDate searchDate, String excontent) {
updateDateChooser(searchDate);
selectedDate = DateConverter.convertDate(dateChooser);
dayinfo.setText("You are editing entry for: "+ new SimpleDateFormat("dd/MM/yyyy").format(dateChooser.getDate()));
contentfield.setText(excontent);

}

Now my dateboundary function is working as expected. whenever a date greater than current date is chosen, the optiondialog gets displayed and its gone after a click, and the datechooser is set to the last selected date, although the propertychange method is called thrice:

  • once before the dialog is displayed
  • twice after the dialog gets closed.

But my isAlreadyWritten() is not working as expected and the optiondialog is getting displayed 4 times with propertychange() method being called four times: once before each time the dialog is displayed.

I want to understand why propertychange is being called 4 times even though the datechooser is detached from the listener when the date is set programatically?


Solution

  • So, I put together this quick snippet and ran it

    import com.toedter.calendar.JDateChooser;
    import java.awt.EventQueue;
    import java.beans.PropertyChangeEvent;
    import java.beans.PropertyChangeListener;
    import java.util.Date;
    import javax.swing.JFrame;
    import javax.swing.UIManager;
    import javax.swing.UnsupportedLookAndFeelException;
    
    public class Test {
    
        public static void main(String[] args) {
            new Test();
        }
    
        public Test() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                        ex.printStackTrace();
                    }
                    JDateChooser dateChooser = new JDateChooser();
                    dateChooser.addPropertyChangeListener(new PropertyChangeListener() {
                        @Override
                        public void propertyChange(PropertyChangeEvent evt) {
                            System.out.println(evt.getPropertyName());
                        }
                    });
                    dateChooser.setDate(new Date());
    
                    JFrame frame = new JFrame("Testing");
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.add(dateChooser);
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
    }
    

    I opened the date selector and selected a date. The program outputted...

    date
    ancestor
    date
    date
    
    1. Was me setting the date programmatically
    2. ancestor is it getting added to the container
    3. Was me selecting the date picker
    4. Was me selecting a date

    So, as you can see, not only are you getting spammed with a lot of "date" property changes, you're also getting all the "other" property changes as well 😓

    So, the first thing you want to do, is limit the the notifications to the "date" property only, something like...

    dateChooser.addPropertyChangeListener("date", new PropertyChangeListener() {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            System.out.println(evt.getPropertyName());
        }
    });
    

    This at least means you don't get bother by all the additional information you don't care about.

    While you can add and remove the listener, I tend to find it a pain, as I don't always have a reference to the listener(s), instead, I tend to use a state flag instead

    private boolean manualDate = false;
    //...
    dateChooser.addPropertyChangeListener("date", new PropertyChangeListener() {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if (manualDate) {
                return;
            }
            System.out.println(evt.getPropertyName());
        }
    });
    
    manualDate = true;
    dateChooser.setDate(new Date());
    manualDate = false;
    

    Not a big change, but this alone means that you're now down to two event notifications.

    Instead, you should compare the oldValue with the newValue of the PropertyChangeEvent

    JDateChooser dateChooser = new JDateChooser();
    dateChooser.addPropertyChangeListener("date", new PropertyChangeListener() {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if (manualDate) {
                return;
            }
            Date newDate = (Date) evt.getNewValue();
            Date oldDate = (Date) evt.getOldValue();
            if (newDate != null && oldDate != null) {
                LocalDate newLD = LocalDate.ofInstant(newDate.toInstant(), ZoneId.systemDefault());
                LocalDate oldLD = LocalDate.ofInstant(oldDate.toInstant(), ZoneId.systemDefault());
                if (newLD.equals(oldLD)) {
                    return;
                }
            }
            System.out.println(evt.getPropertyName());
        }
    });
    

    And now, we're down to one change event. The only draw back is it won't tell you when they reselect the current date.

    A slightly better work flow might be to ignore it all and simply have a JButton that the user can press to perform what ever associated actions you need carried out

    Runnable Example...

    import com.toedter.calendar.JDateChooser;
    import java.awt.EventQueue;
    import java.beans.PropertyChangeEvent;
    import java.beans.PropertyChangeListener;
    import java.time.LocalDate;
    import java.time.ZoneId;
    import java.util.Date;
    import javax.swing.JFrame;
    import javax.swing.UIManager;
    import javax.swing.UnsupportedLookAndFeelException;
    
    public class Test {
    
        public static void main(String[] args) {
            new Test();
        }
    
        private boolean manualDate;
    
        public Test() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                        ex.printStackTrace();
                    }
                    JDateChooser dateChooser = new JDateChooser();
                    dateChooser.addPropertyChangeListener("date", new PropertyChangeListener() {
                        @Override
                        public void propertyChange(PropertyChangeEvent evt) {
                            if (manualDate) {
                                return;
                            }
                            Date newDate = (Date) evt.getNewValue();
                            Date oldDate = (Date) evt.getOldValue();
                            if (newDate != null && oldDate != null) {
                                LocalDate newLD = LocalDate.ofInstant(newDate.toInstant(), ZoneId.systemDefault());
                                LocalDate oldLD = LocalDate.ofInstant(oldDate.toInstant(), ZoneId.systemDefault());
                                if (newLD.equals(oldLD)) {
                                    return;
                                }
                            }
                            System.out.println(evt.getPropertyName());
                        }
                    });
                    manualDate = true;
                    dateChooser.setDate(new Date());
                    manualDate = false;
    
                    JFrame frame = new JFrame("Testing");
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.add(dateChooser);
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
    }