Search code examples
javafxfxml

Update fxml tag when state changes


I want to create some kind of 'security' fxml tag that disables / makes invisible its children depending on the state of some kind of SecurityManager class.

The difficulty I have is the following. When the state of the SecurityManager class changes I want all the securityTags to update their visible property. Granted, I can, everytime the tag constructor is called, add all SecurityTag nodes to a static list and loop over it when the SecurityManger class changes state. But then, what if a security tag node gets removed from a parent? How do I get rid of it in the list? Or maybe there is just an allround better way to deal with this?

public class SecurityTag extends Pane {

    public Security() {
        super();
        this.setVisible(false);
    }

}
public class SecurityManager {

    private boolean authorized;

    public SecurityManager() {
        this.authorized = false;
    }

    public void login() {
        this.authorized = true;
    }

    public void logout() {
        this.authorized = false;
    }

    public boolean isAuthorized() {
        return authorized;
    }

}


Solution

  • The most straightforward way to do this is to make the authorized property in your SecurityManager a JavaFX property:

    package org.jamesd.examples.security;
    
    import javafx.beans.property.ReadOnlyBooleanProperty;
    import javafx.beans.property.ReadOnlyBooleanWrapper;
    
    public class SecurityManager {
    
        private final ReadOnlyBooleanWrapper authorized ;
    
        public SecurityManager() {
            this.authorized = new ReadOnlyBooleanWrapper(false) ;
        }
    
    
    
        public void login() {
            this.authorized.set(true);
        }
    
        public void logout() {
            this.authorized.set(false);
        }
    
        public ReadOnlyBooleanProperty authorizedProperty() {
            return authorized.getReadOnlyProperty();
        }
    
        public boolean isAuthorized() {
            return authorizedProperty().get();
        }
    
    }
    

    Now you can simply bind the relevant properties to the authorized property of the SecurityManager. Depending on the property you're binding, you can do this directly in FXML or in the controller. You can make a SecurityManager instance available to the FXML file by placing it in the FXMLLoader's namespace, and make it available to the controller simply by passing it as a parameter to the controller constructor, and setting the controller manually (i.e. not using the fx:controller attribute) on the FXMLLoader.

    Here's an example FXML file. Note how the "Privileged Action" button binds its visibility to the security manager with

    visible = "${securityManager.authorized}" 
    

    You could also do

    disable = "${ !securityManager.authorized}" 
    

    if you preferred just to disable it.

    <?xml version="1.0" encoding="UTF-8"?>
    <?import javafx.geometry.Insets ?>
    <?import javafx.scene.layout.BorderPane ?>
    <?import javafx.scene.layout.VBox ?>
    <?import javafx.scene.control.Button ?>
    <?import javafx.scene.control.Label ?>
    
    <BorderPane xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1">
        <top>
            <Label fx:id = "securityStatus"></Label>
        </top>
        <center>
            <VBox spacing="5" fillWidth="true">
                <Button text="Regular Action" maxWidth="Infinity"></Button>
                <Button text="Privileged Action" visible = "${securityManager.authorized}" maxWidth="Infinity"></Button>
    
                <padding>
                    <Insets top="5" left="5" right="5" bottom="5"/>
                </padding>
            </VBox>
        </center>
        <left>
            <VBox spacing="5" fillWidth="true">
                <Button text="login" onAction="#login" maxWidth="Infinity"/>
                <Button text="logout" onAction="#logout" maxWidth="Infinity"/>
    
                <padding>
                    <Insets top="5" left="5" right="5" bottom="5"/>
                </padding>
            </VBox>
        </left>
    </BorderPane>
    

    Here's a controller. The text of the label is bound to the state of the security manager using a more complex binding than can be achieved in FXML:

    package org.jamesd.examples.security;
    
    import javafx.beans.binding.Bindings;
    import javafx.fxml.FXML;
    import javafx.scene.control.Label;
    
    public class SecurityController {
    
        private final SecurityManager securityManager ;
    
        @FXML
        private Label securityStatus ;
    
        public SecurityController(SecurityManager securityManager) {
            this.securityManager = securityManager ;
        }
    
        public void initialize() {
            securityStatus.textProperty().bind(Bindings
                .when(securityManager.authorizedProperty())
                .then("Logged In")
                .otherwise("Logged Out")
            );
        }
    
        @FXML
        private void login() {
            securityManager.login();
        }
    
        @FXML
        private void logout() {
            securityManager.logout();
        }
    }
    

    And finally, here's how it is all assembled:

    package org.jamesd.examples.security;
    
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    public class SecurityApp extends Application {
    
        @Override
        public void start(Stage primaryStage) throws Exception {
    
            SecurityManager securityManager = new SecurityManager();
    
            FXMLLoader loader = new FXMLLoader(getClass().getResource("SecurityExample.fxml"));
            loader.getNamespace().put("securityManager", securityManager);
            loader.setController(new SecurityController(securityManager));
    
            Scene scene = new Scene(loader.load());
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        public static void main(String[] args) {
            Application.launch(args);
        }
    }
    

    Note that this approach avoids any unnecessary subclassing of JavaFX nodes (e.g. Pane), which can lead to issues (for example, you may want to place security-dependent nodes in an existing layout pane, and this makes is more difficult to use standard layouts).


    If, as suggested in the comments, you want the SecurityManager class to be agnostic to JavaFX (and presumably desktop Java in general), you could simply create a delegate for the UI which uses JavaFX properties, and arrange for it to be updated when the "real" security manager is updated.

    E.g. here's a SecurityManager that implements a classical "listener" pattern:

    package org.jamesd.examples.security;
    
    @FunctionalInterface
    public interface AuthorizationListener {
        void authorizationChanged(boolean newStatus);
    }
    

    and

    package org.jamesd.examples.security;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class SecurityManager  {
    
        private boolean authorized ;
        private final List<AuthorizationListener> listeners ;
    
        public SecurityManager() {
            this.listeners = new ArrayList<>();
        }
    
        public void login() {
            setAuthorized(true);
        }
    
        public void logout() {
            setAuthorized(false);
        }
    
        public void addListener(AuthorizationListener listener) {
            listeners.add(listener);
        }
    
        public void removeListener(AuthorizationListener listener) {
            listeners.remove(listener);
        }
    
        public boolean isAuthorized() {
            return authorized;
        }
    
        private void setAuthorized(boolean authorized) {
            if (! this.authorized == authorized) {
                this.authorized = authorized ;
                listeners.forEach(l -> l.authorizationChanged(authorized));
            }
        }
    
    }
    

    Note these are completely agnostic with respect to any view technology.

    Now you can create a "UI security manager delegate":

    package org.jamesd.examples.security;
    
    import javafx.beans.property.ReadOnlyBooleanProperty;
    import javafx.beans.property.ReadOnlyBooleanWrapper;
    
    public class UISecurityDelegate  {
    
        private final ReadOnlyBooleanWrapper authorized ;
    
        private final SecurityManager manager ;
    
        public UISecurityDelegate(SecurityManager manager) {
            this.manager = manager ;
            this.authorized = new ReadOnlyBooleanWrapper(manager.isAuthorized()) ;
            manager.addListener(authorized::set);
        }
    
        public void login() {
            manager.login();
        }
        public void logout() {
            manager.logout();
        }
    
        public ReadOnlyBooleanProperty authorizedProperty() {
            return authorized.getReadOnlyProperty();
        }
    
        public boolean isAuthorized() {
            return authorizedProperty().get();
        }
    
    }
    

    And finally update the UI code with

    package org.jamesd.examples.security;
    
    import javafx.beans.binding.Bindings;
    import javafx.fxml.FXML;
    import javafx.scene.control.Label;
    
    public class SecurityController {
    
        private final UISecurityDelegate securityManager ;
    
        @FXML
        private Label securityStatus ;
    
        public SecurityController(UISecurityDelegate securityManager) {
            this.securityManager = securityManager ;
        }
    
        public void initialize() {
            securityStatus.textProperty().bind(Bindings
                .when(securityManager.authorizedProperty())
                .then("Logged In")
                .otherwise("Logged Out")
            );
        }
    
        @FXML
        private void login() {
            securityManager.login();
        }
    
        @FXML
        private void logout() {
            securityManager.logout();
        }
    }
    

    and

    package org.jamesd.examples.security;
    
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    public class SecurityApp extends Application {
    
        @Override
        public void start(Stage primaryStage) throws Exception {
    
            // probably created by data or service layer, etc:
            SecurityManager securityManager = new SecurityManager();
    
            UISecurityDelegate securityDelegate = new UISecurityDelegate(securityManager) ;
    
            FXMLLoader loader = new FXMLLoader(getClass().getResource("SecurityExample.fxml"));
            loader.getNamespace().put("securityManager", securityDelegate);
            loader.setController(new SecurityController(securityDelegate));
    
            Scene scene = new Scene(loader.load());
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        public static void main(String[] args) {
            Application.launch(args);
        }
    }