JavaFX - XML格式存儲
第5部分的主題
- 持久化數據為XML
- 使用JavaFX的FileChooser
- 使用JavaFX的菜單
- 在用戶設置中保存最後打開的文件路徑。
現在我們的地址應用程序的數據隻保存在內存中。每次我們關閉應用程序,數據將丟失,因此是時候開始考慮持久化存儲數據了。
保存用戶設置
Java允許我們使用Preferences
類保存一些應用狀態。依賴於操作係統,Perferences
保存在不同的地方(例如:Windows中的注冊文件)。
我們不能使用Preferences
來保存全部地址簿。但是它允許我們保存一些簡單的應用狀態。一件這樣事情是最後打開文件的路徑。使用這個信息,我們能加載最後應用的狀態,不管用戶什麼時候重啟應用程序。
下麵兩個方法用於保存和檢索Preference。添加它們到你的MainApp
類的最後:
MainApp.java
/** * Returns the person file preference, i.e. the file that was last opened. * The preference is read from the OS specific registry. If no such * preference can be found, null is returned. * * @return */ public File getPersonFilePath() { Preferences prefs = Preferences.userNodeForPackage(MainApp.class); String filePath = prefs.get("filePath", null); if (filePath != null) { return new File(filePath); } else { return null; } } /** * Sets the file path of the currently loaded file. The path is persisted in * the OS specific registry. * * @param file the file or null to remove the path */ public void setPersonFilePath(File file) { Preferences prefs = Preferences.userNodeForPackage(MainApp.class); if (file != null) { prefs.put("filePath", file.getPath()); // Update the stage title. primaryStage.setTitle("AddressApp - " + file.getName()); } else { prefs.remove("filePath"); // Update the stage title. primaryStage.setTitle("AddressApp"); } }
持久性數據到XML
為什麼是XML?
持久性數據的一種最常用的方法是使用數據庫。數據庫通常包含一些類型的關係數據(例如:表),當我們需要保存的數據是對象時。這稱object-relational impedance mismatch。匹配對象到關係型數據庫表有很多工作要做。這裡有一些框架幫助我們匹配(例如:Hibernate,最流行的一個)。但是它仍然需要相當多的設置工作。
對於簡單的數據模型,非常容易使用XML。我們使用稱為JAXB(Java Architecture for XML Binding)的庫。隻需要幾行代碼,JAXB將允許我們生成XML輸出,如下所示:
示例XML輸出
<persons> <person> <birthday>1999-02-21</birthday> <city>some city</city> <firstName>Hans</firstName> <lastName>Muster</lastName> <postalCode>1234</postalCode> <street>some street</street> </person> <person> <birthday>1999-02-21</birthday> <city>some city</city> <firstName>Anna</firstName> <lastName>Best</lastName> <postalCode>1234</postalCode> <street>some street</street> </person> </persons>
使用JAXB
JAXB已經包含在JDK中。這意味著我們不需要包含任何其它的庫。
JAXB提供兩個主要特征:編列(marshal)Java對象到XML的能力,反編列(unmarshal)XML到Java對象。
為了讓JAXB能夠做轉換,我們需要準備我們的模型。
準備JAXB的模型類
我們希望保持的數據位於MainApp
類的personData
變量中。JAXB要求使用@XmlRootElement
注釋作為最頂層的類。personData
是ObservableList
類,我們不能把任何注釋放到ObservableList
上。因此,我們需要創建另外一個類,它隻用於保存Person
列表,用於存儲成XML文件。
創建的新類名為PersonListWrapper
,把它放入到ch.makery.address.model
包中。
PersonListWrapper.java
package ch.makery.address.model; import java.util.List; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; /** * Helper class to wrap a list of persons. This is used for saving the * list of persons to XML. * * @author Marco Jakob */ @XmlRootElement(name = "persons") public class PersonListWrapper { private List<Person> persons; @XmlElement(name = "person") public List<Person> getPersons() { return persons; } public void setPersons(List<Person> persons) { this.persons = persons; } }
注意兩個注釋:
-
@XmlRootElement
定義根元素的名稱。 -
@XmlElement
一個可選的名稱,用來指定元素。
使用JAXB讀寫數據
我們讓MainApp
類負責讀寫人員數據。添加下麵兩個方法到MainApp.java
的最後:
/** * Loads person data from the specified file. The current person data will * be replaced. * * @param file */ public void loadPersonDataFromFile(File file) { try { JAXBContext context = JAXBContext .newInstance(PersonListWrapper.class); Unmarshaller um = context.createUnmarshaller(); // Reading XML from the file and unmarshalling. PersonListWrapper wrapper = (PersonListWrapper) um.unmarshal(file); personData.clear(); personData.addAll(wrapper.getPersons()); // Save the file path to the registry. setPersonFilePath(file); } catch (Exception e) { // catches ANY exception Dialogs.create() .title("Error") .masthead("Could not load data from file:\n" + file.getPath()) .showException(e); } } /** * Saves the current person data to the specified file. * * @param file */ public void savePersonDataToFile(File file) { try { JAXBContext context = JAXBContext .newInstance(PersonListWrapper.class); Marshaller m = context.createMarshaller(); m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); // Wrapping our person data. PersonListWrapper wrapper = new PersonListWrapper(); wrapper.setPersons(personData); // Marshalling and saving XML to the file. m.marshal(wrapper, file); // Save the file path to the registry. setPersonFilePath(file); } catch (Exception e) { // catches ANY exception Dialogs.create().title("Error") .masthead("Could not save data to file:\n" + file.getPath()) .showException(e); } }
編組和解組已經準備好,讓我們創建保存和加載的菜單實際的使用它。
處理菜單響應
在我們RootLayout.fxml
中,這裡已經有一個菜單,但是我們冇有使用它。在我們添加響應到菜單中之前,我們首先創建所有的菜單項。
在Scene Builder中打開RootLayout.fxml
,從library組中拖曳必要的菜單到Hierarchy組的MemuBar
中。創建New,Open…,Save,Save As…和Exit菜單項。
提示:使用Properties組下的Accelerator設置,你能設置菜單項的快捷鍵。
RootLayoutController
為了處理菜單動作,我們需要創建一個新的控製器類。在控製器包ch.makery.address.view
中創建一個類RootLayoutController
。
添加下麵的內容到控製器中:
RootLayoutController.java
package ch.makery.address.view; import java.io.File; import javafx.fxml.FXML; import javafx.stage.FileChooser; import org.controlsfx.dialog.Dialogs; import ch.makery.address.MainApp; /** * The controller for the root layout. The root layout provides the basic * application layout containing a menu bar and space where other JavaFX * elements can be placed. * * @author Marco Jakob */ public class RootLayoutController { // Reference to the main application private MainApp mainApp; /** * Is called by the main application to give a reference back to itself. * * @param mainApp */ public void setMainApp(MainApp mainApp) { this.mainApp = mainApp; } /** * Creates an empty address book. */ @FXML private void handleNew() { mainApp.getPersonData().clear(); mainApp.setPersonFilePath(null); } /** * Opens a FileChooser to let the user select an address book to load. */ @FXML private void handleOpen() { FileChooser fileChooser = new FileChooser(); // Set extension filter FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter( "XML files (*.xml)", "*.xml"); fileChooser.getExtensionFilters().add(extFilter); // Show save file dialog File file = fileChooser.showOpenDialog(mainApp.getPrimaryStage()); if (file != null) { mainApp.loadPersonDataFromFile(file); } } /** * Saves the file to the person file that is currently open. If there is no * open file, the "save as" dialog is shown. */ @FXML private void handleSave() { File personFile = mainApp.getPersonFilePath(); if (personFile != null) { mainApp.savePersonDataToFile(personFile); } else { handleSaveAs(); } } /** * Opens a FileChooser to let the user select a file to save to. */ @FXML private void handleSaveAs() { FileChooser fileChooser = new FileChooser(); // Set extension filter FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter( "XML files (*.xml)", "*.xml"); fileChooser.getExtensionFilters().add(extFilter); // Show save file dialog File file = fileChooser.showSaveDialog(mainApp.getPrimaryStage()); if (file != null) { // Make sure it has the correct extension if (!file.getPath().endsWith(".xml")) { file = new File(file.getPath() + ".xml"); } mainApp.savePersonDataToFile(file); } } /** * Opens an about dialog. */ @FXML private void handleAbout() { Dialogs.create() .title("AddressApp") .masthead("About") .message("Author: Marco Jakob\nWebsite: http://code.makery.ch") .showInformation(); } /** * Closes the application. */ @FXML private void handleExit() { System.exit(0); } }
FileChooser
注意在上麵的RootLayoutController
中使用FileCooser
的方法。首先,創建新的FileChooser
類對象的,然後,添加擴展名過濾器,以至於隻顯示以.xml
結尾的文件。最後,文件選擇器顯示在主Stage的上麵。
如果用戶冇有選擇一個文件關閉對話框,返回null
。否則,我們獲得選擇的文件,我們能傳遞它到MainApp
的loadPersonDataFromFile(…)
或savePersonDataToFile()
方法中。
連接fxml視圖到控製器
-
在Scene Builder中打開
RootLayout.fxml
。在Controller組中選擇RootLayoutController
作為控製器類。 -
回到Hierarchy組中,選擇一個菜單項。在Code組中On Action下,應該看到所有可用控製器方法的選擇。為每個菜單項選擇響應的方法。
-
為每個菜單項重複第2步。
-
關閉Scene Builder,並且在項目的根目錄上按下刷新F5。這讓Eclipse知道在Scene Builder中所做的修改。
連接MainApp和RootLayoutController
在幾個地方,RootLayoutController
需要引用MainApp
類。我們也冇有傳遞一個MainApp
的引用到RootLayoutController
。
打開MainApp
類,使用下麵的替代initRootLayout()
方法:
/** * Initializes the root layout and tries to load the last opened * person file. */ public void initRootLayout() { try { // Load root layout from fxml file. FXMLLoader loader = new FXMLLoader(); loader.setLocation(MainApp.class .getResource("view/RootLayout.fxml")); rootLayout = (BorderPane) loader.load(); // Show the scene containing the root layout. Scene scene = new Scene(rootLayout); primaryStage.setScene(scene); // Give the controller access to the main app. RootLayoutController controller = loader.getController(); controller.setMainApp(this); primaryStage.show(); } catch (IOException e) { e.printStackTrace(); } // Try to load last opened person file. File file = getPersonFilePath(); if (file != null) { loadPersonDataFromFile(file); } }
注意兩個修改:一行給控製器訪問MainApp和最後三行加載最新打開的人員文件。
測試
做應用程序的測試驅動,你應該能夠使用菜單保存人員數據到文件中。
當你在編輯器中打開一個xml
文件,你將注意到生日冇有正確保存,這是一個空的<birthday/>
標簽。原因是JAXB不隻奧如何轉換LocalDate
到XML。我們必須提供一個自定義的LocalDateAdapter
定義這個轉換。
在ch.makery.address.util
中創建新的類,稱為LocalDateAdapter
,內容如下:
LocalDateAdapter.java
package ch.makery.address.util; import java.time.LocalDate; import javax.xml.bind.annotation.adapters.XmlAdapter; /** * Adapter (for JAXB) to convert between the LocalDate and the ISO 8601 * String representation of the date such as '2012-12-03'. * * @author Marco Jakob */ public class LocalDateAdapter extends XmlAdapter<String, LocalDate> { @Override public LocalDate unmarshal(String v) throws Exception { return LocalDate.parse(v); } @Override public String marshal(LocalDate v) throws Exception { return v.toString(); } }
然後打開Person.jar
,添加下麵的注釋到getBirthday()
方法上:
@XmlJavaTypeAdapter(LocalDateAdapter.class) public LocalDate getBirthday() { return birthday.get(); }
現在,再次測試。試著保存和加載XML文件。在重啟之後,它應該自動加載最後使用的文件。
它如何工作
讓我們看下它是如何一起工作的:
-
應用程序使用
MainApp
中的main(…)
方法啟動。 -
調用
public MainApp()
構造函數添加一些樣例數據。 -
調用
MainApp
的start(…)
方法,調用initRootLayout()
從RootLayout.fxml
中初始化根布局。fxml文件有關於使用控製器的信息,連接視圖到RootLayoutController
。 -
MainApp
從fxml加載器中獲取RootLayoutController
,傳遞自己的引用到控製器中。使用這些引用,控製器隨後可以訪問MainApp
的公開方法。 -
在
initRootLayout
方法結束,我們試著從Perferences
中獲取最後打開的人員文件。如果Perferences
知道有這樣一個XML文件,我們將從這個XML文件中加載數據。這顯然會覆蓋掉構造函數中的樣例數據。