Implementing a GUI as a State Machine

    Having come to a large project that uses Swing as a graphical interface, I realized that understanding the logic and connections between the JDialog or JFrame components is still not easy. And all the time that I was dealing with this code, I was trying to find some kind of universal solution that would avoid the complexity of the connections between the elements of the interface.



    The main problem I came across: each element has one or more PropertyChangeListener, which, depending on the situation (read from the state of other elements), in some way affect the other elements. Those elements, in turn, also have their own PropertyChangeListener, which also, depending on the context, affect others. Understanding how pressing a button will affect all other elements was sometimes very difficult, and even finding a piece of code where this happens, all the more so. It also complicated the change of dependencies (in order to change the logical rule, I had to look for all the connections in different places on different components, and most likely something was missed) and debugging.

    In such a situation, the Mediator pattern could help, but again, tracing all the connections and the logic of interaction will also be difficult. It is possible to break each JDialog into smaller JPanels that encapsulate certain logical places, but again, we need to maintain the interaction of these JPanels among themselves. If JDialog is already uncomplicated, then there will be nothing to break.

    The idea came to my mind that JDialog is always in strictly defined states at a certain point in time. For example, the dialogue was created, it is in the “I just created” state, then the user or the program logic itself puts the dialogue in the “I am processing” state, if the user clicked the cancel button during the “I am processing” state, the dialogue goes to the “ I was interrupted. "

    Why is reasoning like that good? At each individual point in time, we can focus on rendering one logical state. And, therefore, all the code responsible for the state of the elements will be in one, logical and physical place. It will not be necessary to search through all PropertyChangeListeners to understand how the JButton “Cancel” can affect the JLabel status when it was clicked after certain user interactions. The same goes for changes. For example, we have a dialogue that does something in the background thread and publishes its progress. The dialog displays the progress bar with the current progress. If the user clicks the Cancel button, we reset the JProgressBar. Now we also decided to disable the start button of the background process, if it was canceled. When our interface is written using states,

    So, you need to program the user interface as a set of states into which the dialog enters and exits. Less words more code!

    As an example, we consider a dialogue that allows you to find out the answer to the main question.

    this class represents the one who knows the answer:
    
    /**
     * class that can answer to the maing question
     * @author __nocach
     */
    public class MeaningOfLifeAnswerer {
        public int answer(){
            return 42;
        }
    }
    


    Since the answer is not so simple, we also need to prepare a class that can answer the main question. Preparation takes a long time, and it needs to be done through SwingWorker.
    
    /**
     * worker that prepares MeaningOfLifeAnswerer
     * @author __nocach
     */
    public class PrepareToAnswerMeaningOfLife extends SwingWorker{
        @Override
        protected MeaningOfLifeAnswerer doInBackground() throws Exception {
            Thread.sleep(1500);
            return new MeaningOfLifeAnswerer();
        }
    }
    


    Finding the answer itself is also a rather long operation, and should be done through SwingWorker:
    
    /**
     * worker that will retrieve answer to the main question using passed Answerer
     * @author __nocach
     */
    public class RetrieveMeaningOfLife extends SwingWorker{
        private final MeaningOfLifeAnswerer answerer;
        public RetrieveMeaningOfLife(MeaningOfLifeAnswerer answerer){
            if (answerer == null){
                throw new NullPointerException("prepareProvider can't be null");
            }
            this.answerer = answerer;
        }
        @Override
        protected Integer doInBackground() throws Exception {
            for(int i = 0; i < 100; i++){
                Thread.sleep(10);
                setProgress(i);
            }
            return answerer.answer();
        }
    }
    


    Requirements from the interface: After creating and displaying the dialog, we start the worker to initialize MeaningOfLifeAnswerer, disable the button for search, and write in the status that we are preparing MeaningOfLifeAnswerer. Once the MeaningOfLifeAnswerer is initialized, turn on the search button. By pressing the search button, we start the RetrieveMeaningOfLife worker, turn off the search button and write that we are in search. Once the answer is found, turn on the search button again and write on the button that we are ready to search again.

    A typical approach would look something like this:
    
    public class StandardWay extends javax.swing.JFrame {
        private Logger logger = Logger.getLogger(StandardWay.class.getName());
        private class FindAnswerAction extends AbstractAction{
            private final MeaningOfLifeAnswerer answerer;
            public FindAnswerAction(MeaningOfLifeAnswerer answerer){
                super("Find");
                this.answerer = answerer;
            }
            @Override
            public void actionPerformed(ActionEvent e) {
                RetrieveMeaningOfLife retrieveWorker = new RetrieveMeaningOfLife(answerer);
                retrieveWorker.addPropertyChangeListener(new PropertyChangeListener() {
                    @Override
                    public void propertyChange(PropertyChangeEvent evt) {
                        if ("progress".equals(evt.getPropertyName())){
                            progressBar.setValue((Integer)evt.getNewValue());
                        }
                        if ("state".equals(evt.getPropertyName())){
                        	if (StateValue.STARTED.equals(evt.getNewValue())){
                        	    //состояние начался поиск ответа
                        	    doButton.setText("In Search");
                        	    doButton.setEnabled(false);
                        	    labelStatus.setText("searching...");
                        	}
                            if (StateValue.DONE.equals(evt.getNewValue())){
                                RetrieveMeaningOfLife worker = (RetrieveMeaningOfLife)evt.getSource();
                                try{
                                    Integer answer = worker.get();
                                    //состояние ответ получен
                                    logger.info("got the answer");
                                    JOptionPane.showMessageDialog(rootPane, "THE ANSWER IS " + answer);
                                }
                                catch(Exception ex){
                                    //состояние ошибка при получении ответа
                                    logger.info("error while retrieving the answer");
                                    JOptionPane.showMessageDialog(rootPane, "Error while searching for meaning of life");
                                }
                                labelStatus.setText("answer was found");
                                doButton.setText("Find again");
                                doButton.setEnabled(true);
                            }
                        }
                    }
                });
                retrieveWorker.execute();
            }
        }
        /**
         * listener that updates gui state by progress of PrepareToAnswerMeaningOfLife worker
         * @author __nocach
         *
         */
        private class PrepareToAnswerMeaningOfLifeListener implements PropertyChangeListener{
        	 @Override
             public void propertyChange(PropertyChangeEvent evt) {
                 if ("state".equals(evt.getPropertyName())){
                     if (StateValue.STARTED.equals(evt.getNewValue())){
                    	 //здесь мы логически в состоянии инициализации MeaningOfLifeAnswerer
                         labelStatus.setText("Prepearing... ");
                         doButton.setEnabled(false);
                         logger.info("preparing...");
                     }
                     if (StateValue.DONE.equals(evt.getNewValue())){
                    	//здесь мы логически в состоянии готовности запустить поиск ответа на вопрос
                         labelStatus.setText("I am prepared to answer the meaning of life");
                         doButton.setEnabled(true);
                         PrepareToAnswerMeaningOfLife worker = (PrepareToAnswerMeaningOfLife)evt.getSource();
                         try{
                             doButton.setAction(new FindAnswerAction(worker.get()));
                             logger.info("prepared");
                         }
                         catch(Exception ex){
                        	//состояние ошибки при инициализации MeaningOfLifeAnswerer
                             JOptionPane.showMessageDialog(rootPane, "failed to find answerer to the question");
                             dispose();
                             logger.severe("failed to prepare");
                         }
                     }
                 }
             }
        }
        /** Creates new form StandardWay */
        public StandardWay() {
            initComponents();
            PrepareToAnswerMeaningOfLife prepareWorker = new PrepareToAnswerMeaningOfLife();
            prepareWorker.addPropertyChangeListener(new PrepareToAnswerMeaningOfLifeListener());
            prepareWorker.execute();
        }
       //...
       //код инициализации компонентов и запуска JFrame опущен
       //...
        private javax.swing.JButton doButton;
        private javax.swing.JLabel labelStatus;
        private javax.swing.JProgressBar progressBar;
    }
    


    So, all the code for changing the state is hardcoded in two PropertyChangeListener's (PrepareToAnswerMeaningOfLife worker and RetrieveMeaningOfLife worker). The logic of the dialogue with PrepareToAnswerMeaningOfLifeListener starts, which monitors the progress of the launched PrepareToAnswerMeaningOfLife, then, after successful initialization, the search button receives FindAnswerAction, which, when pressed, launches the RetrieveMeaningOfLife worker. There we add an anonymous PropertyChangeListener to synchronize the state of our interface while searching for the answer to the main question. In fact, the above listing can be brought into an acceptable form if each, logically whole change of state is put into a separate method of the form setViewToPreparing (), where the code will be
    
    private void setViewToPreparing(){
      labelStatus.setText("Prepearing... ");
      doButton.setEnabled(false);
      logger.info("preparing...");
    }
    

    But if in your dialogue each such logical piece will be more than 20 lines, then the rendered methods will need to be split into even smaller methods, so the JFrame class will be packed with a huge number of not very related private methods.

    What the approach would look like using Finite Automata:
    
    public class StateMachineWay extends javax.swing.JFrame {
    	private Logger logger = Logger.getLogger(StandardWay.class.getName());
    	/**
    	 * controlls switching between gui states
    	 */
    	private GuiStateManager stateManager = new GuiStateManager();
    	private class PreparingAnswererState extends BaseGuiState{
    		@Override
    		public void enterState() {
                                    labelStatus.setText("Prepearing... ");
                                    doButton.setEnabled(false);
    		}
    	}
    	private class ReadyToFindTheAnswer extends BaseGuiState{
    		private final MeaningOfLifeAnswerer answerer;
    		public ReadyToFindTheAnswer(MeaningOfLifeAnswerer answerer){
    			this.answerer = answerer;
    		}
    		@Override
    		public void enterState() {
                                    labelStatus.setText("I am prepared to answer the meaning of life");
                                    doButton.setEnabled(true);
                                    doButton.setAction(new FindAnswerAction(answerer));
    		}
    	}
    	private class FoundAnswerState extends BaseGuiState{
    		private final Integer answer;
    		public FoundAnswerState(Integer answer){
    			this.answer = answer;
    		}
    		@Override
    		public void enterState() {
    		    labelStatus.setText("answer was found");
    		    doButton.setText("Find again");
    		    doButton.setEnabled(true);
    		    JOptionPane.showMessageDialog(rootPane, "THE ANSWER IS " + answer);
    		}
    	}
    	private class FailedToPrepareAnswerer extends BaseGuiState{
    		@Override
    		public void enterState() {
    		    JOptionPane.showMessageDialog(rootPane, "failed to find answerer to the question");
                                    dispose();
    		}
    	}
    	private class FailedToFoundAnswer extends BaseGuiState{
    		@Override
    		public void enterState() {
    		    labelStatus.setText("failed to find answer");
    		    doButton.setText("Try again");
    		    doButton.setEnabled(true);
    		    JOptionPane.showMessageDialog(rootPane, "Error while searching for meaning of life");
    		}
    	}
    	private class SearchingForAnswer extends BaseGuiState{
    		@Override
    		public void enterState() {
    		    labelStatus.setText("searching...");
    		    doButton.setText("In Search");
    		    doButton.setEnabled(false);
    		}
    	}
    	/**
    	 * actions that starts worker that will find the answer to the main question
    	 * @author __nocach
    	 *
    	 */
        private class FindAnswerAction extends AbstractAction{
            private final MeaningOfLifeAnswerer answerer;
            public FindAnswerAction(MeaningOfLifeAnswerer answerer){
                super("Find");
                this.answerer = answerer;
            }
            @Override
            public void actionPerformed(ActionEvent e) {
                RetrieveMeaningOfLife retrieveWorker = new RetrieveMeaningOfLife(answerer);
                retrieveWorker.addPropertyChangeListener(new PropertyChangeListener() {
                    @Override
                    public void propertyChange(PropertyChangeEvent evt) {
                        if ("progress".equals(evt.getPropertyName())){
                            progressBar.setValue((Integer)evt.getNewValue());
                        }
                        if ("state".equals(evt.getPropertyName())){
                            if (StateValue.DONE.equals(evt.getNewValue())){
                                RetrieveMeaningOfLife worker = (RetrieveMeaningOfLife)evt.getSource();
                                try{
                                    Integer answer = worker.get();
                                    stateManager.switchTo(new FoundAnswerState(answer));
                                    logger.info("got the answer");
                                }
                                catch(Exception ex){
                                    logger.info("error while retrieving the answer");
                                    stateManager.switchTo(new FailedToFoundAnswer());
                                }
                            }
                            if (StateValue.STARTED.equals(evt.getNewValue())){
                            	stateManager.switchTo(new SearchingForAnswer());
                            }
                        }
                    }
                });
                retrieveWorker.execute();
            }
        }
        /**
         * listener that updates gui state by progress of PrepareToAnswerMeaningOfLife worker
         * @author __nocach
         *
         */
        private class PrepareToAnswerMeaningOfLifeListener implements PropertyChangeListener{
        	 @Override
             public void propertyChange(PropertyChangeEvent evt) {
                 if ("state".equals(evt.getPropertyName())){
                     if (StateValue.STARTED.equals(evt.getNewValue())){
                    	 logger.info("preparing...");
                                 stateManager.switchTo(new PreparingAnswererState());
                     }
                     if (StateValue.DONE.equals(evt.getNewValue())){
                         PrepareToAnswerMeaningOfLife worker = (PrepareToAnswerMeaningOfLife)evt.getSource();
                         try{
                        	 MeaningOfLifeAnswerer meaningOfLifeAnswerer = worker.get();
                        	 stateManager.switchTo(new ReadyToFindTheAnswer(meaningOfLifeAnswerer));
                                 logger.info("prepared");
                         }
                         catch(Exception ex){
                             logger.severe("failed to prepare");
                             stateManager.switchTo(new FailedToPrepareAnswerer());
                         }
                     }
                 }
             }
        }
        /** Creates new form StandardWay */
        public StateMachineWay() {
            initComponents();
            PrepareToAnswerMeaningOfLife prepareWorker = new PrepareToAnswerMeaningOfLife();
            prepareWorker.addPropertyChangeListener(new PrepareToAnswerMeaningOfLifeListener());
            prepareWorker.execute();
        }
       //...
       //код инициализации компонентов и запуска JFrame опущен
       //...
        private javax.swing.JButton doButton;
        private javax.swing.JLabel labelStatus;
        private javax.swing.JProgressBar progressBar;
    }
    


    The main classes are GuiStateManager.
    
    /**
     * State machine of swing gui
     * @author __nocach
     */
    public class GuiStateManager {
        private GuiState currentState = new EmptyState();
        /**
         * makes passed state current
         * @param newState not null new state
         */
        public synchronized void switchTo(GuiState newState){
            if (newState == null){
                throw new NullPointerException();
            }
            currentState.leaveState();
            currentState = newState;
            currentState.enterState();
        }
        public GuiState current(){
            return currentState;
        }
    }
    

    A state machine class that is responsible for switching states and calling the appropriate methods for entering and exiting states.

    and class state GuiState
    
    public interface GuiState {
       /**
        * called when entering to this state
        */
        public void enterState();
        /**
         * called when leaving this state
         */
        public void leaveState();
    }
    


    When approaching through the state machine, we eventually came up with the following inner classes:
    PreparingAnswererState,
    ReadyToFindTheAnswer,
    FoundAnswerState,
    FailedToPrepareAnswerer,
    FailedToFoundAnswer

    In the above example, instead of hardcoded logic, there is now a transition to the state and nothing more, for example:
    
    stateManager.switchTo(new ReadyToFindTheAnswer(meaningOfLifeAnswerer));
    

    All the logic of changing the elements is concentrated in the state to which we switched (in this case, ReadyToFindTheAnswer, which makes the search button active and changes JLabel's labels and buttons). It is worth noting that now we can easily move the place of state switching, or use the same switching in different places.

    Someone may say that we just created a bunch of classes, but in this case we use 5 classes to increase the readability of the code, because each class name is a state name.

    Now, when we want to change the behavior of the interface at the moment we prepared MeaningOfLifeAnswerer (that is, we made the button for launching the answer search active) we just need to find the ReadyToFindTheAnswer state and add, for example, a pop-up dialog with a message that we are ready to respond to question.

    Each state class can be freely refactored _locally_ without clogging the external JFrame with unnecessary variables or methods. State classes can be moved to separate files for later testing.

    Also, this approach _forces_ writing dialogue code more clearly, creating for each state a separate small class with a logical name. Although, of course, here you can ruin everything with a huge number of anonymous classes.

    The above state machine code is just an example of an idea. I am not the first one who decided to think of the user interface as a finite state machine, and therefore there is a ready-made SwingStates framework that thought this idea quite deeply.

    Well, obviously, this approach can be used in any desktop application in any language.

    Sample code is available here.

    Also popular now: