Search code examples
javaeventsjavafxevent-handlingcustom-events

How to Properly Set Up custom Event


OK - I've fought and fought with this and read a million posts, tutorials, etc. None of them seem to address directly what I'm trying to do here. I've got this little code example to illustrate.

Basically, I want to be able to raise/fire a custom event from the button and have the label respond to that event.

(Please note that I AM NOT INTERESTED in finding other ways to do it than what I'm trying to do here with the actual Event objects. I know perfectly well how to do it with change listeners and such, but I want to learn how to do it this way.)

Here is the code, which you can see doesn't get me the result I'm looking for. Much of it comes from some examples (that didn't really work for me) and I admit there's parts here that are going "whoosh" over my head.

If someone can help me get it working, I can dissect it later to make sure I understand what is going on exactly. Here's the code:

import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

//-------------------------------------------------------------------
public class MyDemo extends Application {

    public static void main(String[] args) {
    launch(args);
    }

    @Override
    public void start(Stage primaryStage) {

    // label that should receive the event and react to it
    MyLabel lblReceiver = new MyLabel("And I Should Receive & React");

    // button to firs the event
    Button btnSender = new Button("Initate Event");
    btnSender.setPrefWidth(200);
    btnSender.setOnAction(e -> {
        MyEvent.fireEvent(lblReceiver, e); // really confused what the first parameter here is supposed to be.
    });

    // set up stage and show it
    Stage stage = new Stage();
    VBox root = new VBox(btnSender, lblReceiver);
    root.setSpacing(10);
    root.setPadding(new Insets(10, 10, 10, 10));
    Scene scene = new Scene(root);
    stage.setScene(scene);
    stage.show();
    }

}
//-------------------------------------------------------------------

// Interface for objects that want to listen to my event
//-------------------------------------------------------------------
interface MyEventListener {
    void onMyEvent();
}
//-------------------------------------------------------------------

// My event definition itself
//-------------------------------------------------------------------
class MyEvent extends Event {

    public static final EventType<MyEvent> MY_EVENT = new EventType<>(ANY, "MY_EVENT");

    public MyEvent(EventType<? extends MyEvent> eventType) {
    super(eventType);
    }
}
//-------------------------------------------------------------------

//base/parent class for my label - this is what should receive/respond to event and
// where I'm sure i have problems - just don't know what.
//-------------------------------------------------------------------
class MyLabel extends Label implements MyEventListener {

    public MyLabel(String name) {
    this.setAlignment(Pos.CENTER);
    this.setText(name);
    this.setPrefWidth(200);
    }

    @Override // this is what I'm expecting to happen when i click the button
    public void onMyEvent() {
    this.setText("YAY! i got the event!");
    }

    private final ObjectProperty<EventHandler<? super MyEvent>> onMyEventProp
        = new SimpleObjectProperty<EventHandler<? super MyEvent>>(this, "onMyEvent") {
    @Override
    protected void invalidated() {
        setEventHandler(MyEvent.MY_EVENT, get());
    }
    };

    public final void setOnMyEvent(EventHandler<? super MyEvent> handler) {
    onMyEventProp.set(handler);
    }

    public final EventHandler<? super MyEvent> getOnMyEvent() {
    return onMyEventProp.get();
    }

    public final ObjectProperty<EventHandler<? super MyEvent>> onMyEventProperty() {
    return onMyEventProp;
    }

}
//-------------------------------------------------------------------

Solution

  • Answer

    You seem have everything set up correctly except for two problems.

    1. You never add an EventHandler that listens for your event.

      Implementing an arbitrary interface will not make your object react to your custom event. The event processing system has no knowledge of your interface and doesn't even know you've implemented it. If you want your onMyEvent() method to be invoked when your event reaches the label, you would need to do something like:

      public MyLabel() {
          //...
          addEventHandler(MyEvent.MY_EVENT, event -> onMyEvent());
      }
      

      Note I used addEventHandler so that the class itself doesn't rely on the onMyEvent property. If you relied on the property then outside code could accidentally break your code by replacing the EventHandler.

      This makes me wonder if the MyEventListener interface is really necessary. Couldn't you just do what you needed inside the EventHandler?

    2. You never actually fire an instance of your MyEvent.

      You currently have:

      btnSender.setOnAction(e -> MyEvent.fireEvent(lblReceiver, e));
      

      This simply refires e (an ActionEvent) towards lblReceiver. The fireEvent method is a static method declared by javafx.event.Event. Prefixing the method call with MyEvent doesn't change what method is actually called here. Change that to the following:

      btnSender.setOnAction(e -> Event.fireEvent(lblReceiver, new MyEvent()));
      // or...
      btnSender.setOnAction(e -> lblReceiver.fireEvent(new MyEvent()));
      
      // The second option is a convenience method and simply forwards to Event.fireEvent
      

      In order to actually fire an instance of your own event class at your label.


    Fundamentals of Event Dispatching

    There are two phases to event processing in JavaFX:

    • Capturing phase

      • The first phase. Here the event travels from the start of its path down to the target. At each step along the way the appropriate event filters are invoked. Filters are added via methods such as Node.addEventFilter(EventType,EventHandler).
    • Bubbling phase

      • The second phase. Here the event travels from the target back up to the start of the path. At each step along the way the appropriate event handlers are invoked. Handlers are added via methods such as Node.addEventHandler(EventType,EventHandler) and via properties such as Node.onKeyPressed and ButtonBase.onAction.

    And event processing consists of the following components:

    • javafx.event.Event (and subclasses)

      • The actual object passed around. Carries information related to the event (e.g. the cursor's location for MouseEvents).

        An Event also carries the source. However, the source is dynamic; it will always be the object the EventHandler currently handling the event was added to. So an EventHandler added to a Button will have the Button as the event's source, but the same event will have the parent as it's source for an EventHandler added to the parent. This behavior doesn't change even if you used the same EventHandler instance both times.

    • javafx.event.EventType

      • Refines the meaning of a class of Event. For example, a MouseEvent with the MOUSE_PRESSED type means, unsurprisingly, the mouse was pressed. An EventType has a supertype; this forms a hierarchy across all types for every kind of event. A handler registered for a supertype will also be notified of subtypes.

        You cannot have two or more EventTypes with the same supertype and name. This is why instances are typically public static final fields of the corresponding event class.

    • javafx.event.EventHandler

      • Handles any events its registered for. This is a functional interface (can be the target of a lambda expression or method reference). It's implementations of this interface that do the application-level work (i.e. do something when a button is fired).
    • javafx.event.EventDispatcher

      • Responsible for dispatching the event to the appropriate handlers. Each object capable of being the target of an event typically has its own dispatcher. For example, each Window, Scene, and Node instance has its own EventDispatcher instance (held in a property).

        Note: Outside highly specialized situations, you will never have to deal with this interface directly.

    • javafx.event.EventDispatchChain

      • Represents the path the event will take when it's fired at a target. Instances of this interface function as a stack of EventDispatchers. When an event is fired, a new chain is created and configured based on the event's target. In the case of the scene graph and the target is a Node, the stack consists of the EventDispatchers belonging to the Window, the Scene, and then each Node down to (parent to child) and including the target. The event travels down this chain (the capturing phase) and then back up this chain (the bubbling phase).

        Note: You will likely never need to use this interface directly.

    • javafx.event.EventTarget

      • The actual target of the event. The EventTarget interface has one method that is used to build the EventDispatchChain. In other words, the target determines the path of the event. This is the first argument in Event.fireEvent(EventTarget,Event).

        Note: You only need to use this interface if you're creating an object that can (obviously) be the target of events (and your object doesn't extend from an implementation of EventTarget).