Tuesday, December 31, 2013

javaFX redirect page and passing object to controller

i am building a java desktop application using javaFX 2.2 with FXML to create the user interface, following this (http://docs.oracle.com/javafx/2/get_started/fxml_tutorial.htm) tutorial.

i use jdk7u45, the jfxrt.jar is located in $JAVA_HOME/jre/lib/jfxrt.jar.
note: if u're facing an exception
 Exception in thread "main" java.lang.NoClassDefFoundError: javafx/application/Application   
when running the app, the jar is not loaded in the classpath, an alternative solution is to copy the jfxrt.jar to $JAVA_HOME/jre/lib/ext/ folder.

back to topic, the tutorial above give us an example to create a page using FXML and bind it's components and action listener method to a java controller class. and the follow-through question is: how to redirect page and passing object between controller class in javaFX?

this is not a tutorial, just want to share how i do page redirect in javaFX and passing object between controller classes.

first of all, it's the structure. it's kind of like this:


stage(window) - scene - page.
the stage is instantiate by javafx's Application, act as the application window. i figure scene as a wrapper of a page. and a page is the user interface we create using fxml and the controller.

from that structure, to enable redirection, we should save the instance of the stage somewhere that can be retrieved by each page, and there should be a controller manager which manage the page rendering.

so, architecturally, i separate the page controller manager (the one that controls the content of the main window, loads and renders the page of a scene) from the pages, including the main page.

here's the controller manager, i named it CommonController:
 public abstract class CommonController implements Initializable {  
   public final Logger LOGGER = Logger.getLogger(this.getClass().getName());  
   private static final String UI_BASE_LOCATION = "/pages/";  
   private static final String FXML_PREFIX = ".fxml";  
   private RestClient restClient = new RestClient();  
   public static void setSceneContentStartup(Stage stage) throws IOException{  
     Context.getInstance().setCurrentStage(stage);  
     setSceneContent("login");  
   }  
   public static Parent setSceneContent(String pageName) throws IOException {  
     Stage currentStage = Context.getInstance().getCurrentStage();  
     Scene scene = currentStage.getScene();  
     Parent page = (Parent) FXMLLoader.load(CommonController.class.getResource(UI_BASE_LOCATION + pageName + FXML_PREFIX));  
     if (scene == null) {  
       scene = new Scene(page);  
       currentStage.setScene(scene);  
       currentStage.setTitle("Sample JavaFX Application");  
       currentStage.setWidth(800);  
       currentStage.setHeight(600);  
     } else {  
       currentStage.getScene().setRoot(page);  
     }  
     currentStage.centerOnScreen();  
     currentStage.sizeToScene();  
     currentStage.show();  
     return page;  
   }  
   public RestClient getRestClient(){  
     return restClient;  
   }  
 }  

this is an abstract class, so it couldnt be instantiated and it implements Initializable so every controller class that extends this class will have to implement the initialize method.

this class holds the .fxml file folder location, and the setSceneContent receive a string as input argument which is the filename of the page's .fxml file and load the file, put it in a scene, and assign the scene to the stage showing. this method loads and renders the page.

we see that the scene that contained to page we want is attached to the stage. so we need to retrieve the current showing stage. for that case, i make a static holder for the stage, and this holder can be use also for holding some objects that want to be passed between controller, i name it as Context:
 public class Context {  
   private static Context instance;  
   public static Context getInstance(){  
     return instance == null ? new Context() : instance;  
   }  
   private static Map<String, Object> contextObjects = new HashMap<String, Object>();  
   private static Stage currentStage;  
   public Map<String, Object> getContextObjects(){  
     return contextObjects;  
   }  
   public Object getContextObject(String key){  
     return contextObjects.get(key);  
   }  
   public Object removeContextObject(String key){  
     return contextObjects.remove(key);  
   }  
   public void addContextObject(String key, Object value){  
     contextObjects.put(key, value);  
   }  
   public void clearContextObjects(){  
     contextObjects.clear();  
   }  
   public Stage getCurrentStage() {  
     return currentStage;  
   }  
   public void setCurrentStage(Stage stage) {  
     currentStage = stage;  
   }  
 }  

here's the main page:
 public class MainPage extends Application {  
   public static void main(String[] args) {  
     Application.launch(args);  
   }  
   @Override  
   public void start(Stage stage){  
     try {  
       CommonController.setSceneContentStartup(stage);  
     } catch (IOException ex) {  
       Logger.getLogger(MainPage.class.getName()).log(Level.SEVERE, null, ex);  
     }  
   }  
 }  

the MainPage called the CommonController.setSceneContentStartup and passed the stage as the argument. and that method stored the stage to Context
 Context.getInstance().setCurrentStage(stage);  
and call the redirect/rendering method
 setSceneContent("login");  
which asks to load and render page login.fxml.

and so, the login.fxml is rendered and showed in the stage. what about the redirection?

let's see the login page:
 <GridPane fx:controller="org.mazb.samplejavafx.controller.LoginController"  
      xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">  
   ...  
   <HBox spacing="10" alignment="bottom_right"  
      GridPane.columnIndex="1" GridPane.rowIndex="4">  
     <Button text="Sign In"    
         onAction="#handleSubmitButtonAction"/>  
   </HBox>  
   ...  
 </GridPane>  

and login controller:
 public class LoginController extends CommonController {  
   @FXML  
   private Text actiontarget;  
   @FXML  
   private TextField usernameField;  
   @FXML  
   private PasswordField passwordField;  
   @Override  
   public void initialize(URL url, ResourceBundle rb) {  
     LOGGER.log(Level.INFO, "initialize not implemented yet.");  
   }  
   @FXML  
   protected void handleSubmitButtonAction(ActionEvent event) {  
     String username = usernameField.getText();  
     String pass = passwordField.getText();  
     User user = new User(username, pass);  
     user = (User) getRestClient().postFormData(user, CommonConstant.RestOperationPath.VALIDATE_USER);  
     if(user==null){  
       actiontarget.setText("forbidden");  
     }else{  
       try {  
         Context.getInstance().addContextObject("loggedInUser", user);  
         setSceneContent("home");  
       } catch (IOException ex) {  
         LOGGER.log(Level.SEVERE, null, ex);  
       }  
     }  
   }  
 }  

when button is clicked, it will trigger the invocation of method handleSubmitButtonAction.

in my case, the process then will invoke a site via rest, and check the result. if the result is not null, then it will ask to redirect to page home.fxml and passes the object result.

the object to be passed is placed in static HashMap ContextObjects and can be retrieved anytime. the CommonController will then get the current stage from the Context, and load home.fxml and then render it.

and then the home.fxml is rendered:
 <GridPane fx:controller="org.mazb.samplejavafx.controller.HomeController"  
      xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">  
   ...  
   <Text fx:id="welcomeText" id="welcome-text"  
      GridPane.columnIndex="0" GridPane.rowIndex="0"  
      GridPane.columnSpan="2"/>  
 </GridPane>  

and the method initialize in HomeController will be invoked:
 public class HomeController extends CommonController {  
   private User user;  
   @FXML  
   private Text welcomeText;  
   @Override  
   public void initialize(URL url, ResourceBundle rb) {  
     user = (User) Context.getInstance().getContextObject("loggedInUser");  
     welcomeText.setText("Welcome, "+user.getRealName());  
   }  
 }  

in the method, we retrieved the passed ContextObject (instance of User) and assign its property realName to the welcomeText.

the page is succesfully redirected, the object is successfully passed. i think, until this time, this is the most suitable architecture for page redirection and object passing in javaFX.

sample source code is available at https://github.com/mazbergaz/sample_javafx

No comments:

Post a Comment