25. Event-Driven Programming
In the previous lecture, we began our discussion of graphical applications. We saw how we typically separate the design of these applications into three aspects: their model, view, and controller. The model was very similar to the code we’ve been writing all semester, using classes and fields to model the state of the application and methods to access and/or modify this state. The view, however, was completely new, and we spent time understanding Java’s Swing framework and some of the classes that it provides that allow us to draw windows and widgets on the screen.
In today’s lecture, we’ll focus on the third aspect of graphical applications, their controllers. We recall that the controller contains the logic to update the application’s state (both underlying state in the model and visible state in the view) in response to different events. To do this, we’ll need to dive deeper into the Swing framework to understand how it handles events. We’ll also discuss the observer pattern for event handling and the inversion of control in graphical programs. To continue our running example from the previous lecture, we’ll complete the definition of our graphical Tic-Tac-Toe application and end up with a playable game program.
Event Handling
In an imperative program, the implementer specifies which instructions should be executed in which order. At runtime, these instructions are followed to produce the program's output or achieve its desired side-effects.
In the previous lecture, we saw that the Swing framework provided many useful abstractions that allowed our code to achieve a much more declarative style.
In a declarative program, the implementer specifies the desired outcome of their code. At runtime, the way to achieve these outcomes is determined and carried out behind the scenes.
For example, when we set up the component hierarchy for the view, we did this by constructing objects that modeled what we wanted to appear on the screen, and we used their methods to say how to modify their appearance. We didn’t need to worry about how these objects appeared; the Swing framework took care of this for us. Nevertheless, the view code that we wrote still could be executed sequentially and immediately, constructing and drawing the components to the screen one after another until the view was built out. At this point, the code that we wrote had all finished executing, but this did not cause the program to terminate (as it had in the past). We’ll soon discuss why.
Graphical user interfaces often expose different options for how the user will interact with the program. There may be multiple buttons that a user can click or different text boxes where the user can enter text, and we have no way of knowing in which order the user will interact with these elements. Further, programs may incorporate some element of time if they have some sort of animated component, which may require some parts of the program to continually execute while others await new inputs from the user. Such graphical programs are no longer imperative; they cannot pre-determine what code will need to be run at each point of their execution. Rather, these programs must be responsive to user inputs or other events.
An event is any occurrence that could necessitate a change in the program's state.
Responsive programs execute certain code instructions in response to events. For this reason, they are also known as event-driven programs.
Event-driven programs behave very differently from imperative programs. Since their instructions are executed after an event takes place, they spend a lot of their time waiting to detect these events. Moreover, since they don’t know how often or in which order these events will occur, they need to repeatedly check (or poll) for new events. We call this process of checking whether a new event has occurred and responding appropriately if it has an event loop.
An event loop is a piece of code that repeatedly checks whether a new event has occurred and dispatches any detected events to code that can handle them.
The event loop forms the basis for any responsive program. For example, JavaScript’s event loop enables interactivity on web pages. Next, we’ll discuss Java’s Swing event handling framework, which also relies on an event loop.
The Swing Event Loop
Last lecture, we saw that when we wrote the main() method for a Swing application, we used a somewhat complicated syntax:
TicTacToeGraphical.java
|
|
|
|
Our entire main() method consists of this single instruction. In fact, this instruction executes right away, and we almost immediately return from the main() method, which typically signals the end of our program. However, the invokeLater() method does something that we have not yet seen in the course: it passes computation to another thread. We’ll discuss threads in more detail in the final lectures of the course when we discuss concurrent programming (i.e., programs that split their computation across multiple threads). For today, you can think of a thread as an abstract entity that executes a sequence of code instructions.
The invokeLater() method starts up a special thread called Swing’s event dispatch thread. Behind the scenes, the Swing framework includes the code that will create and manage an event loop on this thread immediately after executing the code that was passed into invokeLater(); recall that the argument to invokeLater() is an object implementing the Runnable functional interface, whose run() method serves as a wrapper around a sequence of Java instructions.
The event dispatch thread handles the execution of almost all code in the Swing framework. In particular, it contains the main Swing event loop that enables event-handling in responsive graphical applications.
The following figure helps to visualize (at a high level) the execution of a Swing application.
Here, the red line visualizes the main application thread, which executes the main() method by calling the SwingUtilities.invokeLater() method. After this, the work of the main thread is done, so the thread stops. The call to SwingUtilities.invokeLater() starts up the event dispatch thread. At first, this thread executes the code in its Runnable parameter, which does all of the work to initialize the view in the main application JFrame and then makes this frame visible. At this point, the event dispatch thread enters its event loop and is ready to respond to user inputs. When the user closes the main JFrame, the event dispatch thread can be stopped, and the application terminates (since it no longer has any running threads).
Understanding the Event Loop
To better understand what actually takes place in the event loop, it can help to make an analogy with a real-life scenario. We’ll think of the event loop as the front desk of an office. The employees at the front desk sit prepared to handle incoming information. This information can come from many different sources. Sometimes, the phone rings, and they need to answer it and route the call to the correct employee. Other times, mail or package deliveries come in and need to be sent to the correct office. Different people with various requests may show up in person at the office lobby. Finally, there are some tasks, like weekly email reminders, that these employees may take care of on a routine schedule. Some points in the day are particularly busy, and multiple different tasks pile up; some calls may need to be placed on hold. At other points in the day, the office is less busy, and these employees can relax a bit or catch up on more long-term tasks.
The event loop works in a similar manner. Events in our graphical application can come from multiple sources:
- Some events are triggered by user inputs on peripheral devices. They might type something on their keyboard, or click or hover over a particular portion of the screen with their cursor.
- Other events are triggered by internal (to the code) objects, such as periodic notifications from a timer that can be used to manage applications that have state that evolves predictably over time.
- Still other events can be manually triggered within method definitions (we’ll see this with
PropertyChanges soon), such as requests to the layout manager topack()the components on screen or requests for portions of the screen to berepaint()ed.
Incoming events are queued up and then, one by one (in some order determined behind the scenes by the Swing framework), are dispatched out to be handled. In order for this dispatch to happen correctly, the handlers, pieces of code that understand how to respond to a particular event, must register with the event loop. In our analogy, an employee may inform the front desk that they are expecting a UPS delivery that day. Then, the front desk will know where to send the package when it arrives.
At first, this responsive programming paradigm can seem a bit unintuitive, since it runs counter to most programming you have likely done. When we write event-driven code, we specify up front the actions that will be taken when an event occurs, registering this response with the event loop; however, after this, we don’t know if/when these actions will ever be taken. We trust that the event loop will detect an incoming event and follow our instructions appropriately. We sometimes refer to this paradigm as an inversion of control, since we are handing over control of how our code gets executed to the Swing framework.
Inversion of control is used to describe the paradigm where we write custom subroutines but allow an external entity, such as a framework, to control when these subroutines are executed.
Another name for this is the observer design pattern.
In the observer pattern, different units of code (called the observers) register with (or subscribe to) a central module called the event source (i.e., the event loop), which notifies them of any relevant state changes by calling one of their methods so they can respond appropriately.
Rather than requiring each of the observer objects to independently take responsibility for monitoring the state (which would necessitate many threads and add significant complication to our code design), they delegate this responsibility to the event source.
Now that we’ve introduced many of the high-level ideas behind event handling, we are ready to see how to put these concepts into practice in our Java code.
Listeners
The Swing framework manages event handling using EventListeners, interfaces with methods that define what should happen when the event it is “listening for” is triggered. The source of an event (i.e., the entity that causes the event to be triggered in the first place) registers with the event loop using an add_Listener() method (where _ is replaced by the type of listener). By doing this, the event source says to Swing,
As a first example of this, let’s revisit our Tic-Tac-Toe game from the previous lecture.
At the bottom of the window, there is the “Reset” button. We can expect that the user might want to click this button when they are running our application. Thus, the button needs to tell the event loop what should happen when it is clicked. It does this by registering an ActionListener with its addActionListener() method.
If we open the ActionListener interface, we see that it contains a method actionPerformed() with an ActionEvent parameter. Clicking the button is “performing its action.” Therefore, when a button is clicked, the event loop will call the actionPerformed() method on all ActionListener objects registered to that button (i.e., added with the addActionListener() method). It constructs an ActionEvent object to pass to these listeners that packages up information that may be relevant to handle the event (the event’s source object, a description of what command the action performed, the time of the event, etc.).
For now, let’s create a new class that implements the ActionListener interface called a ResetListener that we will register with the reset button.
ResetListener.java
|
|
|
|
Resetting the game must primarily take place within the TicTacToeGrid, since this class has the game’s model as one of its fields and has access to the TicTacToeCells that constitute most of the game’s view. Let’s add a reset() method to the TicTacToeGrid class that contains the reset logic. This method will need to reset the model, which we can do by reassigning this field to a new TicTacToe object. Then, it must go through and clear the symbols out of all of the TicTacToeCells. Finally, it should request a repaint() to update the view for the user.
TicTacToeGrid.java
|
|
|
|
Then, the actionPerformed() method of our ResetListener simply needs to call the reset() method of the TicTacToeGrid. To do this, it will need to have an alias reference to the grid, which we can initialize in its constructor. Overall, this results in the following class definition.
|
|
|
|
We connect this listener to the reset button by calling the addActionListener() method from within the TicTacToeGraphical constructor:
TicTacToeGraphical.java
|
|
|
|
When we run the code from the end of the previous lecture, we see that the reset button now works as intended, clearing away the symbols that we “hard-coded” into the TicTacToeGrid constructor.
Simplified Listener Creation
While the reset button controller that we implemented in the previous section worked as intended, it was rather cumbersome to implement, requiring us to create a new listener class in a separate file. If our application contains more buttons, we don’t want to have to create separate files to make each button work. We’d like to find a simpler way. Fortunately, we can rely on ideas from earlier in the course (along with some new ones) to attain this simplification.
Step 1: Nest the Listener Class
There is no reason that we need our ResetListener class to live in its own file. This is an auxiliary class that will only ever be used from within the TicTacToeGraphical class, so we can nest it to promote better maintainability (fewer files to track) and encapsulation. To simplify the design of this class even more, we can make it a static nested class (it does not need access to any TicTacToeGraphical fields or instance methods) or even a record class.
TicTacToeGraphical.java
|
|
|
|
Step 2: Use an Anonymous Class
Since our ResetListener is only instantiated once (within the addActionListener()) argument, we do not actually need to create a new named type for this listener. Instead, we can use a Java feature called an anonymous class, writing:
TicTacToeGraphical.java
|
|
|
|
Notice that the new expression in the addActionListener() parameter includes the name of the interface type ActionListener, followed by the constructor parameters, followed by a curly-brace delimited scope containing the class’ body. Behind the scenes, Java will create a new class (with some automatically assigned name) with the given body implementing the ActionListener interface. Then, it will call the constructor of this new class with the provided parameters (in this case, no parameters).
The main benefit of this anonymous class is that it places the class definition within the TicTacToeGraphical constructor, providing access to the local grid variable. We say that the variable grid is captured by the anonymous class.
An anonymous class (or lambda expression) can capture a variable declared within a surrounding scope, making the value of that variable accessible within the anonymous class (or lambda expression).
Variable capture always works for fields and for static variables. These variables are allocated on the heap, so they will not be automatically deallocated when the current method returns. This ensures that they will remain accessible within the anonymous class. Local variables can only be captured when they are explicitly declared as final, or when they are "effectively final", meaning they are never modified after their initial assignment. This allows an anonymous class to safely copy the current value of this variable without needing to contend with the possibility of later modifications.
This variable capture allows us to avoid declaring an additional grid field in our anonymous class, which in turn allows us to forgo supplying a constructor.
Step 3: Use a Lambda Expression
Observe that (although it is not annotated in the documentation), ActionListener is a functional interface. It declares only one method, actionPerformed(). Therefore, we can instantiate an ActionListener using a lambda expression within the parameter of addActionListener().
TicTacToeGraphical.java
|
|
|
|
This is, by far, the most elegant solution. The lambda expression syntax clearly shows exactly what will happen when this event is triggered; it will execute grid.reset().
While we prefer using lambda expressions to model listeners, this will not always be possible. This is the reason why we presented the other approaches as well.
- When the
EventListenersub-interface we are realizing includes more than one method (for example, aMouseListeneror aKeyListener), it cannot be instantiated with a lambda expression, and we will need to write out a more expressive anonymous class. Typically, we prefer anonymous classes extending the adapter classes, such asMouseAdapterandKeyAdapter, since they can minimize the code we need to write. - When we need to instantiate the same listener class multiple times (for example, to register it to multiple event sources), we will need to give this class a name, likely defining it as a nested (record) class within whatever outer class we are writing.
- When the same listener class needs to be instantiated from multiple different files, it likely makes sense to define it in its own file
Adding More Listeners
Let’s complete the controller for our TicTacToeGraphical application by adding the other necessary listeners. First, we need to add a listener that detects when a grid cell is clicked. Since this is a more involved operation, we’ll extract it out into its own cellAction() factory method that returns a suitable ActionListener:
TicTacToeGrid.java
|
|
|
|
Take some time to think about what needs to be done within the cellAction() method. It may help to look back at the TicTacToe model class definition from Lecture 24 to see what methods are available.
cellAction() definition
If we run the game at this point, we’ll see that most of the functionality is working; we can click the cell buttons to register moves on the board, and we can click the reset button to clear away these moves. The main missing feature is updating the JLabel at the top of the application window to reflect which player has the next turn.
This is a new type of situation that we need to handle. In this case, an event in one class (clicking a button that triggers a listener defined within the TicTacToeGrid class) results in a side effect that affects a view component in another class (the JLabel referenced by turnLabel in the TicTacToeGraphical class). Swing enables these “class-to-class” effects through property changes. In particular, the event source can call firePropertyChange() to create a PropertyChangeEvent object on the event dispatch thread, which is intercepted by a PropertyChangeListener in another class.
Let’s see this in action. Within our cellAction() method, we’ll fire the property change with the propertyName “Turn”, signaling that the player’s turn has changed.
TicTacToeGrid.java
|
|
|
|
Then, we’ll add a PropertyChangeListener to our grid variable in the TicTacToeGraphical class (since the TicTacToeGrid object it references is the one that fires the property change) that listens for changes to the “Turn” property and updates the label text accordingly.
TicTacToeGraphical.java
|
|
|
|
There is one case that we’ve missed for updating the turnLabel text. Take a minute to try to identify it.
click to show
Final Thoughts
Dialog Boxes
To finish our Tic-Tac-Toe application, we want to add a pop-up dialog with a message displaying the game’s result (our outstanding “TODO” in the cellAction() method). We can do this using the JOptionPane class. In this case, a message dialog box is the most appropriate, and we can construct one using the showMessageDialog() method.
TicTacToeGrid.java
|
|
|
|
Note here that we used a switch expression to more simply select the appropriate message from among the two possibilities (the win and tie messages). In addition, we reset() the game after the user closes this dialog box so they can play again.
Handling Many Events
As we discussed earlier, the Swing framework relies on the event dispatch thread to manage its event loop. In practice, this means that all Swing widgets must be created and updated on the event dispatch thread to ensure that the event state is managed correctly. All of the event handling code (i.e., the bodies of all listeners) also executes on the event dispatch thread.
Our TicTacToeGraphical application is relatively simple. The user interacts with the game only by clicking on its buttons, and we can expect enough time between these button presses for the event dispatch thread to be able to process the events and complete the required updates of the model and the view. In more complicated applications, such as those with periodic timer events that render each frame of an animation, it can become more noticeable when event listeners try to carry out expensive computations. This can block the event dispatch thread from making progress on updating the view and introduce a lag into the program. For this reason, it is critical that event handling code is quick and simple. Any larger computations should be passed off to another thread (for example, by using a SwingWorker). We’ll talk more about spreading work across multiple threads in our final unit on concurrent programming.
Main Takeaways:
- All Swing framework components are constructed and managed by Swing's event dispatch thread, which also executes its main event loop. Long event handler code can block this thread and result in lag, so it should be avoided.
- Graphical applications leverage an inversion of control to be responsive to user inputs. The implementer packages behaviors into methods and delegates control over calling these methods to the framework's event loop. This is also referred to as the observer pattern.
- An event source registers one or more listeners, which contain code for responding to that event.
- Event listeners can often be instantiated as anonymous classes or lambda expressions.
Exercises
Consider the following snippet of Swing code for changing some text in a window when a button is clicked:
|
|
|
|
A student accidentally wrote the following as their main() method for TicTacToeGraphical.
|
|
|
|
checkbox. Note that JCheckBox has an instance method called addItemListener(), which is invoked anytime the checkbox is clicked. addItemListener() takes in an ItemListener as a parameter. You may also find ItemEvent helpful.
|
|
|
|
Define an external listener class.
|
|
|
|
AppWindow.
MouseListener interface that can accomplish this.
MouseListener to each button?
The developer instead decides to make TicTacToeCell implement MouseListener.
|
|
|
|
MouseListener.
mouseEntered() and mouseExited() to draw the border only when a user is hovering over this cell. You may use BorderFactory to quickly create a Border. You can leave the bodies of the other three MouseListener methods blank.
ActionListeners to the JComboBoxes to change the shape of the user. This action listener should fire a property change.
ActionListeners to the JRadioButtons to change the color scheme.
Reset button. You may need to refactor the existing component hierarchy to properly show both buttons.
TicTacToeGrid.
Define an undo() method that undoes a move.
TicTacToeGrid.java
|
|
|
|