I am trying to add an action on the click event on a menu in the menubar using javafx.
Thing is, I saw a lot of posts about it but no answers worked for me.
I manage to do it using the "On Showing" on the menu ,which is fine, but this event is only trigger (as the others) if the menu has at least one menu item.
This is not something I want but I have no choice for now.
Here is the fxml :
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.layout.BorderPane?>
<BorderPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="200.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.HangmanGameFXViews.view.MenuesActionsControlleur">
<top>
<MenuBar fx:id="menusBar" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" onMouseReleased="#switchToAbout" prefWidth="400.0" BorderPane.alignment="CENTER">
<menus>
<Menu mnemonicParsing="false" text="Fichiers">
<items>
<MenuItem mnemonicParsing="false" text="Nouveau" />
<MenuItem mnemonicParsing="false" onAction="#switchToScore" text="Scores" />
<MenuItem mnemonicParsing="false" onAction="#switchToRules" text="Règles" />
<MenuItem mnemonicParsing="false" onAction="#exit" text="Quitter" />
</items>
</Menu>
<Menu fx:id="about" mnemonicParsing="false" onShowing="#switchToAbout" text="À propos">
<items>
<!-- THIS THE DUMMY MENU I USE TO BE ABLE TO TRIGGER THE EVENT ON THE PARENT -->
<MenuItem fx:id="dummyMenuItem" mnemonicParsing="false" />
</items></Menu>
</menus>
</MenuBar>
</top>
</BorderPane>
The code of the view controller :
package org.HangmanGameFXViews.view;
import org.HangmanGameFXViews.Main;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.stage.Stage;
public class MenuesActionsControlleur {
private Stage stageDialogue;
private Main main;
#FXML
private Menu about;
#FXML
private MenuBar menusBar;
#FXML
private MenuItem dummyMenuItem;
#FXML
private void initialize() {
about.addEventHandler(Event.ANY, new EventHandler<Event>() {
#Override
public void handle(Event event) {
System.out.println("Showing Menu 1");
System.out.println(event.getTarget().toString());
System.out.println(event.getEventType().toString());
}
});
}
#FXML
public void switchToRules() {
main.switchToRules();
}
#FXML
public void switchToAbout() {
dummyMenuItem.setDisable(true);
main.switchToAbout();
dummyMenuItem.setDisable(false);
}
#FXML
public void clickableMenu(ActionEvent e){
System.out.println("Menu clicked");
}
#FXML
public void switchToNew() {
main.switchToNew();
}
#FXML
public void switchToScore() {
main.switchToScore();
}
public Stage getStageDialogue() {
return stageDialogue;
}
public void setStageDialogue(Stage stageDialogue) {
this.stageDialogue = stageDialogue;
}
#FXML
public void exit() {
stageDialogue.close();
}
public void setMainClass(Main m) {
main = m;
stageDialogue = main.getStagePrincipal();
}
}
Thank you for any help.
As already noted in the comments: a Menu is not meant to act like a Button (or MenuItem) - its "action" is to open a ContextMenu showing its items. Implementing it to fire in that context is possible (beware: it might confuse users and it requires a bit of dirt in using implementation details and internal api).
That said: the basic idea for installing custom handlers is to do so on the styleableNode that represents the Menu. By default, access to that node is implemented only if the Menu is an item in a ContextMenu and not when represented by a MenuButton in a MenuBar (which I would consider a bug, but that's another story).
So we have to do it ourselves
extend Menu
add api to set a containing MenuBar
implement getStyleableNode to access the MenuButton
add some wiring to redirect a mouseReleased (or whatever) into firing the Menu's action
A custom menu might be someting like (obviously not production quality :):
public class MyMenu extends Menu {
private MenuBar parentMenuBar;
private Parent menuBarContainer;
private MenuButton menuButton;
private EventHandler<MouseEvent> redirector = this::redirect;
public MyMenu(String string) {
super(string);
}
public void setParentMenuBar(MenuBar menuBar) {
this.parentMenuBar = menuBar;
// tbd: cleanup if menuBar and/or its skin disposed/changed
if (menuBar != null) {
menuBar.skinProperty().addListener((src, ov, nv) -> {
if (nv instanceof MenuBarSkin && menuBar.getChildrenUnmodifiable().size() == 1) {
menuBarContainer = (Parent) menuBar.getChildrenUnmodifiable().get(0);
menuBarContainer.getChildrenUnmodifiable().addListener((ListChangeListener)c -> {
updateEventRedirector();
});
updateEventRedirector();
}
});
}
}
protected void redirect(MouseEvent e) {
// fire only if there are no items
if (getItems().size() == 0) fire();
}
/**
* Rewire eventHandler when our styleable node is changed
*/
private void updateEventRedirector() {
if (menuButton != null) {
menuButton.removeEventHandler(MouseEvent.MOUSE_RELEASED, redirector);
}
menuButton = getParentMenuButton();
if (menuButton != null) {
menuButton.addEventHandler(MouseEvent.MOUSE_RELEASED, redirector);
}
}
#Override
public Node getStyleableNode() {
if (parentMenuBar != null && parentMenuBar.getChildrenUnmodifiable().size() == 1) {
return menuButton;
}
return super.getStyleableNode();
}
private MenuButton getParentMenuButton() {
if (parentMenuBar == null || parentMenuBar.getChildrenUnmodifiable().size() != 1) return null;
// beware: implementation detail of menuBarSkin!
Parent parent = (Parent) parentMenuBar.getChildrenUnmodifiable().get(0);
for (Node child : parent.getChildrenUnmodifiable()) {
// beware: internal api!
if (child instanceof MenuBarButton) {
MenuBarButton menuButton = (MenuBarButton) child;
if (menuButton.menu == this) {
return menuButton;
}
}
}
return null;
}
}
To use:
bar = new MenuBar();
first = new MyMenu("dummy");
first.setParentMenuBar(bar);
first.setOnAction(e -> System.out.println("menu handler"));
bar.getMenus().addAll(first, new Menu("other"));
Related
I have a TableView that contains entries from a database and a separate window for entering a new entry with a separate controller. The problem is that when I add a record to the database, the table scene controller doesn't know that there has been a change and the TableView is only updated after the application is restarted. How to create this connection between the controllers so that the TableView is updated immediately?
Main class.
public class ExampleApplication extends Application {
#Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("tableView.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 700, 400);
stage.setTitle("ExampleApp");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Scene controller with a table where entries are displayed.
public class TableController {
#FXML
private Button addEntryButton;
#FXML
private TableColumn<userEntry, String> entryColumn;
#FXML
private TableView<userEntry> table;
ObservableList<userEntry> userEntries = FXCollections.observableArrayList();
#FXML
private void initialize() {
table.getItems().clear();
getAllEntriesFromDbToList();
entryColumn.setCellValueFactory(new PropertyValueFactory<userEntry, String>("entryName"));
table.setItems(userEntries);
addEntryButton.setOnAction(event -> {
openNewScene("/com/example/addNewEntryDialogue.fxml");
});
}
private void getAllEntriesFromDbToList() {
ResultSet rs = new DatabaseHandler().getEntriesTableFromDb();
try {
while (rs.next()) {
userEntries.add(new userEntry(rs.getString("entryName")
));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private void openNewScene(String window) {
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource(window));
try {
loader.load();
} catch (IOException e) {
e.printStackTrace();
}
Parent root = loader.getRoot();
Stage stage = new Stage();
stage.setScene(new Scene(root));
stage.show();
}
}
Dialog window controller for adding a new entry.
public class DialogueController {
#FXML
private Button inputNewEntryCancelButton;
#FXML
private Button inputNewEntryOkButton;
#FXML
private TextField inputNewEntryTextField;
#FXML
void initialize() {
inputNewEntryOkButton.setOnAction(event -> {
addNewEntryToDbTable(inputNewEntryTextField.getText());
((Node) event.getSource()).getScene().getWindow().hide();
});
inputNewEntryCancelButton.setOnAction(event -> {
((Node) event.getSource()).getScene().getWindow().hide();
});
}
private void addNewEntryToDbTable(String entryName) {
DatabaseHandler dbHandler = new DatabaseHandler();
userEntry newEntry = new userEntry(entryName);
dbHandler.addEntryToDb(newEntry);
}
}
Model. Entry class.
public class userEntry {
private String entryName;
public userEntry(String entryName) {
this.entryName = entryName;
}
public String getEntryName() {
return entryName;
}
public void setEntryName(String entryName) {
this.entryName = entryName;
}
}
Model. Data base handler class.
public class DatabaseHandler extends Configs {
Connection dbConnection;
public Connection getDbConnection() throws ClassNotFoundException, SQLException {
String connectionString = "jdbc:mysql://" + dbHost + ":" + dbPort + "/" + dbName;
Class.forName("com.mysql.cj.jdbc.Driver");
dbConnection = DriverManager.getConnection(connectionString, dbUser, dbPass);
return dbConnection;
}
public void addEntryToDb(userEntry task) {
String insert = "INSERT INTO " + Const.ENTRIES_TABLE + "(" +
Const.ENTRIES_ENTRY_NAME + ")" +
"VALUES(?)";
try {
PreparedStatement prSt = getDbConnection().prepareStatement(insert);
prSt.setString(1, task.getEntryName());
prSt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public ResultSet getEntriesTableFromDb() {
ResultSet resSet = null;
String select = "SELECT * FROM " + Const.ENTRIES_TABLE;
try {
resSet = getDbConnection().createStatement().executeQuery(select);
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return resSet;
}
}
View. Table scene FXML. (tableView.fxml)
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.BorderPane?>
<BorderPane prefHeight="386.0" prefWidth="248.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/18" fx:controller="com.example.tableviewtesting.TableController">
<opaqueInsets>
<Insets />
</opaqueInsets>
<bottom>
<TableView fx:id="table" maxHeight="-Infinity" prefHeight="329.0" prefWidth="248.0" BorderPane.alignment="CENTER">
<columns>
<TableColumn fx:id="entryColumn" prefWidth="247.0" text="Entries" />
</columns>
</TableView>
</bottom>
<top>
<Button fx:id="addEntryButton" mnemonicParsing="false" text="Add entry">
<BorderPane.margin>
<Insets left="10.0" top="10.0" />
</BorderPane.margin>
</Button>
</top>
</BorderPane>
View. Dialog scene FXML. (addNewEntryDialogue.fxml)
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane prefHeight="100.0" prefWidth="300.0" xmlns="http://javafx.com/javafx/18" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.tableviewtesting.DialogueController">
<children>
<Label alignment="CENTER" layoutX="8.0" layoutY="18.0" prefHeight="36.0" prefWidth="70.0" text="Entry name:" />
<TextField fx:id="inputNewEntryTextField" layoutX="82.0" layoutY="16.0" prefHeight="40.0" prefWidth="208.0" />
<Button fx:id="inputNewEntryCancelButton" layoutX="156.0" layoutY="65.0" mnemonicParsing="false" prefHeight="25.0" prefWidth="134.0" text="Cancel" AnchorPane.bottomAnchor="5.0" AnchorPane.leftAnchor="156.0" AnchorPane.rightAnchor="10.0" />
<Button fx:id="inputNewEntryOkButton" layoutX="10.0" layoutY="70.0" mnemonicParsing="false" prefHeight="25.0" prefWidth="134.0" text="Create new entry" AnchorPane.bottomAnchor="5.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="156.0" />
</children>
</AnchorPane>
You did not produce an MCVE, so I am going to try to guess based on your limited code.
Here is a method that deletes a Person from an SQLite database.
The following code accepts the person that needs to be deleted in the DB and returns a boolean based on if the person was successfully deleted from the DB or not.
dBHandler.deletePerson(person)
The following code demonstrates using the boolean return value to determine what should happen based on if true or false is returned.
if (dBHandler.deletePerson(person)) {
//delete the `Person` from the `TableView`.
} else {
//Alert the user that the `Person` was not deleted from the DB.
//Do not delete the `Person` from the `TableView`.
}
Actual Code Snippet
#FXML
private void handleBtnDeletePerson(ActionEvent actionEvent) {
Person person = tvMain.getSelectionModel().getSelectedItem();
if (dBHandler.deletePerson(person)) {
//Update TableView
tvMain.getItems().remove(person); //Remove Person from the TableView
lblLastAction.setText(person.getFirstName() + " deleted!");
} else {
lblLastAction.setText(person.getFirstName() + " failed to delete!");
}
}
Full Code Link: https://github.com/sedj601/SQLitePersonTableViewExample
If you have a long-running task, see the following code: https://github.com/sedj601/SQLitePersonTableViewExampleUsingTask.
Warning: this is old code and may be lacking! Hopefully, the ideas are sound, though!
I have a button like this:
<Button Margin="0,20,0,0" Command="{Binding OnSkip}" BackgroundColor="{StaticResource Primary}" CornerRadius="2"
Text="Terms and Conditions of Use" VerticalOptions="End" TextColor="White">
<Button.Effects>
<effects1:ButtonClickEffect></effects1:ButtonClickEffect>
</Button.Effects>
</Button>
Upon adding the button effect inside the button, the 'OnSkip' command no longer fires and I'm not sure why.
The button click effect code is implemented as follows:
public class AndroidButtonClickEffect : PlatformEffect
{
protected override void OnAttached()
{
this.Control.Touch += this.Control_Touch;
}
private void Control_Touch(object sender, Android.Views.View.TouchEventArgs e)
{
if (e.Event.Action == MotionEventActions.Down)
{
this.SetColor(Android.Graphics.Color.Blue);
}
else if (e.Event.Action == MotionEventActions.Up)
{
this.SetColor(Android.Graphics.Color.LightBlue);
}
}
private void SetColor(Android.Graphics.Color color)
{
this.Control.SetBackgroundColor(color);
}
protected override void OnDetached()
{
this.Control.Touch -= this.Control_Touch;
}
}
Removing the button effect causes the command to fire again. Why does the button effect interfere with the command firing? Is there a way I can get the effect to invoke the desired command (generically so I can reuse the effect)?
Thanks.
In the main project, I added the following class, which binds the Command:
public class ButtonClickEffect : RoutingEffect
{
public ButtonClickEffect() : base("Framework.ButtonClickEffect") { }
public static readonly BindableProperty CommandProperty =
BindableProperty.Create("Command", typeof(ICommand), typeof(ButtonClickEffect));
public static ICommand GetCommand(BindableObject view)
{
return (ICommand)view.GetValue(CommandProperty);
}
public static void SetCommand(BindableObject view, ICommand value)
{
view.SetValue(CommandProperty, value);
}
public static readonly BindableProperty CommandParameterProperty =
BindableProperty.CreateAttached("CommandParameter", typeof(object),
typeof(ButtonClickEffect), (object)null);
public static object GetCommandParameter(BindableObject view)
{
return view.GetValue(CommandParameterProperty);
}
public static void SetCommandParameter(BindableObject view, object value)
{
view.SetValue(CommandParameterProperty, value);
}
}
The Android implementation was implemented as follows:
[assembly:ResolutionGroupName("Framework")]
[assembly:ExportEffect(typeof(AndroidButtonClickEffect), "ButtonClickEffect")]
namespace Framework.Droid.Effects
{
public class AndroidButtonClickEffect : PlatformEffect
{
protected override void OnAttached()
{
Control.Touch += Control_Touch;
}
private void Control_Touch(object sender, Android.Views.View.TouchEventArgs e)
{
if (e.Event.Action == MotionEventActions.Down)
{
SetColor(Color.LightBlue);
}
else if (e.Event.Action == MotionEventActions.Up)
{
SetColor(Color.Blue);
}
var command = ButtonClickEffect.GetCommand(Element);
command?.Execute(ButtonClickEffect.GetCommandParameter(Element));
}
private void SetColor(Color color)
{
Control.SetBackgroundColor(color);
}
protected override void OnDetached()
{
Control.Touch -= Control_Touch;
}
}
}
I then removed the 'Command' property from my button and replaced it as follows:
<Button Margin="0,20,0,0" BackgroundColor="{StaticResource Primary}" CornerRadius="2"
Text="Terms and Conditions of Use" VerticalOptions="End" TextColor="White"
effects1:ButtonClickEffect.Command="{Binding OnSkip}" effects1:ButtonClickEffect.CommandParameter="{Binding .}">
<Button.Effects>
<effects1:ButtonClickEffect></effects1:ButtonClickEffect>
</Button.Effects>
</Button>
In all honesty, the command binding is a lot more awkward now (all this code just to get a simple button effect), but the important thing is that it now works. Now I need to work out how implement for iOS.
Credit from here for the answer and whoever posted this URL as an answer (it got deleted).
I have created a custom Master-Detail pane for my project, where i use a split pane, in each i have two Anchor Panes. In one there is a TableView filled with Users (ObservableList). On each row (User) i have implemented a ChangeListener
table.getSelectionModel().selectedItemProperty().addListener(listElementChangeListener());
when the row is selected, i pass the UserObject for my DetailPane, and visualize User data in TextFields as detail. I have implemented controls, to understand if the User is under modification in Detail, and if so i would like to prevent a row change in my TableView. I tried to remove the ChangeListener from the TableView when i modify the User, but it dosent work well. I'm thinking of a solution like setting the focus and holding it on the row until i cancel or save the User modified.
Is there any nice solutions?
Thanks for your help.
I would probably approach this a little differently. I would bind the controls in the "detail view" bidirectionally to the properties in the User object. That way they will be updated in the object (and the table) as the user edits them. If you like, you can also provide a "cancel" button to revert to the previous values.
Here's a complete solution that uses this approach:
User.java:
package usermasterdetail;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class User {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
private final BooleanProperty admin = new SimpleBooleanProperty();
public User(String firstName, String lastName, boolean admin) {
setFirstName(firstName);
setLastName(lastName);
setAdmin(admin);
}
public final StringProperty firstNameProperty() {
return this.firstName;
}
public final String getFirstName() {
return this.firstNameProperty().get();
}
public final void setFirstName(final String firstName) {
this.firstNameProperty().set(firstName);
}
public final StringProperty lastNameProperty() {
return this.lastName;
}
public final String getLastName() {
return this.lastNameProperty().get();
}
public final void setLastName(final String lastName) {
this.lastNameProperty().set(lastName);
}
public final BooleanProperty adminProperty() {
return this.admin;
}
public final boolean isAdmin() {
return this.adminProperty().get();
}
public final void setAdmin(final boolean admin) {
this.adminProperty().set(admin);
}
}
DataModel.java:
package usermasterdetail;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class DataModel {
private final ObservableList<User> userList = FXCollections.observableArrayList(
new User("Jacob", "Smith", false),
new User("Isabella", "Johnson", true),
new User("Ethan", "Williams", false),
new User("Emma", "Jones", true),
new User("Michael", "Brown", true)
);
private final ObjectProperty<User> currentUser = new SimpleObjectProperty<>();
public final ObjectProperty<User> currentUserProperty() {
return this.currentUser;
}
public final User getCurrentUser() {
return this.currentUserProperty().get();
}
public final void setCurrentUser(final User currentUser) {
this.currentUserProperty().set(currentUser);
}
public ObservableList<User> getUserList() {
return userList;
}
}
TableController.java:
package usermasterdetail;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
public class TableController {
#FXML
private TableView<User> table ;
#FXML
private TableColumn<User, String> firstNameColumn ;
#FXML
private TableColumn<User, String> lastNameColumn ;
#FXML
private TableColumn<User, Boolean> adminColumn ;
private DataModel model ;
public void initialize() {
firstNameColumn.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
lastNameColumn.setCellValueFactory(cellData -> cellData.getValue().lastNameProperty());
adminColumn.setCellValueFactory(cellData -> cellData.getValue().adminProperty());
adminColumn.setCellFactory(CheckBoxTableCell.forTableColumn(adminColumn));
}
public void setDataModel(DataModel dataModel) {
if (model != null) {
model.currentUserProperty().unbind();
}
this.model = dataModel ;
dataModel.currentUserProperty().bind(table.getSelectionModel().selectedItemProperty());
table.setItems(model.getUserList());
}
}
UserEditorController.java:
package usermasterdetail;
import javafx.beans.value.ChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TextField;
public class UserEditorController {
#FXML
private TextField firstNameField ;
#FXML
private TextField lastNameField ;
#FXML
private CheckBox adminCheckBox ;
private String cachedFirstName ;
private String cachedLastName ;
private boolean cachedAdmin ;
private ChangeListener<User> userListener = (obs, oldUser, newUser) -> {
if (oldUser != null) {
firstNameField.textProperty().unbindBidirectional(oldUser.firstNameProperty());
lastNameField.textProperty().unbindBidirectional(oldUser.lastNameProperty());
adminCheckBox.selectedProperty().unbindBidirectional(oldUser.adminProperty());
}
if (newUser == null) {
firstNameField.clear();
lastNameField.clear();
adminCheckBox.setSelected(false);
} else {
firstNameField.textProperty().bindBidirectional(newUser.firstNameProperty());
lastNameField.textProperty().bindBidirectional(newUser.lastNameProperty());
adminCheckBox.selectedProperty().bindBidirectional(newUser.adminProperty());
cachedFirstName = newUser.getFirstName();
cachedLastName = newUser.getLastName();
cachedAdmin = newUser.isAdmin();
}
};
private DataModel model ;
public void setDataModel(DataModel dataModel) {
if (this.model != null) {
this.model.currentUserProperty().removeListener(userListener);
}
this.model = dataModel ;
this.model.currentUserProperty().addListener(userListener);
}
#FXML
private void cancel() {
firstNameField.setText(cachedFirstName);
lastNameField.setText(cachedLastName);
adminCheckBox.setSelected(cachedAdmin);
}
}
Table.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TableColumn?>
<StackPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="usermasterdetail.TableController">
<TableView fx:id="table">
<columns>
<TableColumn fx:id="firstNameColumn" text="First Name"/>
<TableColumn fx:id="lastNameColumn" text="Last Name"/>
<TableColumn fx:id="adminColumn" text="Administrator"/>
</columns>
</TableView>
</StackPane>
UserEditor.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Button?>
<?import javafx.geometry.Insets?>
<GridPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="usermasterdetail.UserEditorController"
hgap="5" vgap="5" alignment="CENTER">
<columnConstraints>
<ColumnConstraints halignment="RIGHT" hgrow="NEVER"/>
<ColumnConstraints halignment="LEFT" hgrow="SOMETIMES"/>
</columnConstraints>
<padding>
<Insets top="5" left="5" bottom="5" right="5"/>
</padding>
<Label text="First Name:" GridPane.columnIndex="0" GridPane.rowIndex="0"/>
<Label text="Last Name:" GridPane.columnIndex="0" GridPane.rowIndex="1"/>
<Label text="Admin Priviliges:" GridPane.columnIndex="0" GridPane.rowIndex="2"/>
<TextField fx:id="firstNameField" GridPane.columnIndex="1" GridPane.rowIndex="0"/>
<TextField fx:id="lastNameField" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
<CheckBox fx:id="adminCheckBox" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
<Button text="Cancel" onAction="#cancel" GridPane.columnIndex="0" GridPane.rowIndex="3" GridPane.columnSpan="2"
GridPane.halignment="CENTER"/>
</GridPane>
MainController.java:
package usermasterdetail;
import javafx.fxml.FXML;
public class MainController {
#FXML
private TableController tableController ;
#FXML
private UserEditorController editorController ;
private final DataModel model = new DataModel();
public void initialize() {
tableController.setDataModel(model);
editorController.setDataModel(model);
}
}
Main.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.SplitPane?>
<SplitPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="usermasterdetail.MainController">
<items>
<fx:include fx:id="table" source="Table.fxml"/>
<fx:include fx:id="editor" source="UserEditor.fxml"/>
</items>
</SplitPane>
And finally Main.java:
package usermasterdetail;
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws IOException {
primaryStage.setScene(new Scene(FXMLLoader.load(getClass().getResource("Main.fxml")), 800, 600));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
If you prefer the user experience you described, you can (as #SSchuette describes in the comments), just bind the table's disable property to the modifying property. This will prevent the user from changing the selection while the data is being edited (i.e. is not consistent with the data in the table). For this you just need the modifying property in the model:
package usermasterdetail;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class DataModel {
private final ObservableList<User> userList = FXCollections.observableArrayList(
new User("Jacob", "Smith", false),
new User("Isabella", "Johnson", true),
new User("Ethan", "Williams", false),
new User("Emma", "Jones", true),
new User("Michael", "Brown", true)
);
private final ObjectProperty<User> currentUser = new SimpleObjectProperty<>();
private final BooleanProperty modifying = new SimpleBooleanProperty();
public final ObjectProperty<User> currentUserProperty() {
return this.currentUser;
}
public final usermasterdetail.User getCurrentUser() {
return this.currentUserProperty().get();
}
public final void setCurrentUser(final usermasterdetail.User currentUser) {
this.currentUserProperty().set(currentUser);
}
public ObservableList<User> getUserList() {
return userList;
}
public final BooleanProperty modifyingProperty() {
return this.modifying;
}
public final boolean isModifying() {
return this.modifyingProperty().get();
}
public final void setModifying(final boolean modifying) {
this.modifyingProperty().set(modifying);
}
}
then in the table controller you can bind the disable property to it:
package usermasterdetail;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
public class TableController {
#FXML
private TableView<User> table ;
#FXML
private TableColumn<User, String> firstNameColumn ;
#FXML
private TableColumn<User, String> lastNameColumn ;
#FXML
private TableColumn<User, Boolean> adminColumn ;
private DataModel model ;
public void initialize() {
firstNameColumn.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
lastNameColumn.setCellValueFactory(cellData -> cellData.getValue().lastNameProperty());
adminColumn.setCellValueFactory(cellData -> cellData.getValue().adminProperty());
adminColumn.setCellFactory(CheckBoxTableCell.forTableColumn(adminColumn));
}
public void setDataModel(DataModel dataModel) {
if (model != null) {
model.currentUserProperty().unbind();
}
this.model = dataModel ;
dataModel.currentUserProperty().bind(table.getSelectionModel().selectedItemProperty());
table.setItems(model.getUserList());
table.disableProperty().bind(model.modifyingProperty());
}
}
The only place there is a bit of work to do is to make sure the modifying property is set to true any time the data are not in sync (though it sounds like you have already done this):
package usermasterdetail;
import javafx.beans.value.ChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TextField;
public class UserEditorController {
#FXML
private TextField firstNameField ;
#FXML
private TextField lastNameField ;
#FXML
private CheckBox adminCheckBox ;
private DataModel model ;
private ChangeListener<Object> modifyingListener = (obs, oldValue, newValue) -> {
if (model != null) {
if (model.getCurrentUser() == null) {
model.setModifying(false);
} else {
model.setModifying(! (model.getCurrentUser().getFirstName().equals(firstNameField.getText())
&& model.getCurrentUser().getLastName().equals(lastNameField.getText())
&& model.getCurrentUser().isAdmin() == adminCheckBox.isSelected()));
}
}
};
private ChangeListener<User> userListener = (obs, oldUser, newUser) -> {
if (oldUser != null) {
oldUser.firstNameProperty().removeListener(modifyingListener);
oldUser.lastNameProperty().removeListener(modifyingListener);
oldUser.adminProperty().removeListener(modifyingListener);
}
if (newUser == null) {
firstNameField.clear();
lastNameField.clear();
adminCheckBox.setSelected(false);
} else {
firstNameField.setText(newUser.getFirstName());
lastNameField.setText(newUser.getLastName());
adminCheckBox.setSelected(newUser.isAdmin());
newUser.firstNameProperty().addListener(modifyingListener);
newUser.lastNameProperty().addListener(modifyingListener);
newUser.adminProperty().addListener(modifyingListener);
}
};
public void setDataModel(DataModel dataModel) {
if (this.model != null) {
this.model.currentUserProperty().removeListener(userListener);
}
this.model = dataModel ;
this.model.currentUserProperty().addListener(userListener);
}
public void initialize() {
firstNameField.textProperty().addListener(modifyingListener);
lastNameField.textProperty().addListener(modifyingListener);
adminCheckBox.selectedProperty().addListener(modifyingListener);
}
#FXML
private void cancel() {
if (model != null) {
firstNameField.setText(model.getCurrentUser().getFirstName());
lastNameField.setText(model.getCurrentUser().getLastName());
adminCheckBox.setSelected(model.getCurrentUser().isAdmin());
}
}
#FXML
private void update() {
if (model != null && model.getCurrentUser() != null) {
model.getCurrentUser().setFirstName(firstNameField.getText());
model.getCurrentUser().setLastName(lastNameField.getText());
model.getCurrentUser().setAdmin(adminCheckBox.isSelected());
}
}
}
This solution requires an additional button to force the update in the data (and table):
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Button?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.HBox?>
<GridPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="usermasterdetail.UserEditorController"
hgap="5" vgap="5" alignment="CENTER">
<columnConstraints>
<ColumnConstraints halignment="RIGHT" hgrow="NEVER"/>
<ColumnConstraints halignment="LEFT" hgrow="SOMETIMES"/>
</columnConstraints>
<padding>
<Insets top="5" left="5" bottom="5" right="5"/>
</padding>
<Label text="First Name:" GridPane.columnIndex="0" GridPane.rowIndex="0"/>
<Label text="Last Name:" GridPane.columnIndex="0" GridPane.rowIndex="1"/>
<Label text="Admin Priviliges:" GridPane.columnIndex="0" GridPane.rowIndex="2"/>
<TextField fx:id="firstNameField" GridPane.columnIndex="1" GridPane.rowIndex="0"/>
<TextField fx:id="lastNameField" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
<CheckBox fx:id="adminCheckBox" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
<HBox spacing="5" alignment="CENTER" GridPane.columnIndex="0" GridPane.rowIndex="3" GridPane.columnSpan="2">
<Button text="Update" onAction="#update"/>
<Button text="Cancel" onAction="#cancel"/>
</HBox>
</GridPane>
I have used the following template in my Android application which has navigation drawer has a list of options such as Settings.
https://github.com/MvvmCross/MvvmCross-Samples/tree/master/XPlatformMenus
Source code could be downloaded from the following url
https://github.com/MvvmCross/MvvmCross-Samples
I wonder how could I able to make Settings page as a Dialog or CustomFragment which will look like similar to following image.
One approach that you could make use of is to create a custom implementation of Dialog. Following the XPlatformMenus sample you linked to, you could implement something as follows:
Generic Custom Dialog
This class inherits android Dialog control, and can be used with any XML/AXML layout you want. You could tightly couple it to a particular ViewModel/Layout or you can make it handle a generic ViewModel type. Here is an example of the generic type:
public class CustomDialog : Dialog, IMvxBindingContextOwner
{
public CustomDialog(Context context, int layout, IMvxViewModel viewModel)
: this(context, Resource.Style.CustomDialog)
{
this.BindingContext = new MvxAndroidBindingContext(context, (context as IMvxLayoutInflaterHolder));
ViewModel = viewModel;
Init(layout);
}
public CustomDialog(Context context, int themeResId)
: base(context, themeResId)
{
}
protected CustomDialog(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
protected CustomDialog(Context context, bool cancelable, IDialogInterfaceOnCancelListener cancelListener)
: base(context, cancelable, cancelListener)
{
}
protected CustomDialog(Context context, bool cancelable, EventHandler cancelHandler)
: base(context, cancelable, cancelHandler)
{
}
private void Init(int layout)
{
SetContentView(layout);
}
public override void SetContentView(int layoutResID)
{
var view = this.BindingInflate(layoutResID, null);
base.SetContentView(view);
}
public IMvxBindingContext BindingContext { get; set; }
public object DataContext
{
get { return this.BindingContext.DataContext; }
set { this.BindingContext.DataContext = value; }
}
public IMvxViewModel ViewModel
{
get { return this.DataContext as IMvxViewModel; }
set { this.DataContext = value; }
}
}
XML layout for modal:
<?xml version="1.0" encoding="utf-8" ?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/colorPrimary">
<Button
android:id="#+id/btn_option"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Show"
local:MvxBind="Click ShowSettingsCommand"/>
<Button
android:id="#+id/btn_close"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/btn_option"
android:text="CLOSE"
local:MvxBind="Click ShowCloseCommand"/>
</RelativeLayout>
CustomDialog style:
<resources>
<style name="CustomDialog">
<item name="android:windowIsFloating">true</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources>
Custom Presenter
Create a custom presenter to handle the navigation to show/hide the dialog:
public class CustomPresenter : MvxFragmentsPresenter
{
protected IMvxViewModelLoader MvxViewModelLoader => Mvx.Resolve<IMvxViewModelLoader>();
CustomDialog _modal;
public CustomPresenter(IEnumerable<Assembly> AndroidViewAssemblies) : base(AndroidViewAssemblies)
{
}
protected override void ShowActivity(MvxViewModelRequest request, MvxViewModelRequest fragmentRequest = null)
{
if (!Intercept(request))
base.ShowActivity(request, fragmentRequest);
}
protected override void ShowFragment(MvxViewModelRequest request)
{
if (!Intercept(request))
base.ShowFragment(request);
}
private bool Intercept(MvxViewModelRequest request)
{
if (request.ViewModelType == typeof(ThirdViewModel))
{
var activity = Mvx.Resolve<IMvxAndroidCurrentTopActivity>().Activity;
var viewModel = MvxViewModelLoader.LoadViewModel(request, null) as ThirdViewModel;
_modal = new CustomDialog(activity, Resource.Layout.modal_popup, viewModel);
_modal.Show();
return true;
}
if (_modal != null)
{
_modal.Dismiss();
_modal = null;
}
return false;
}
}
Register your custom presenter in the setup class:
protected override IMvxAndroidViewPresenter CreateViewPresenter()
{
var mvxFragmentsPresenter = new CustomPresenter(AndroidViewAssemblies);
Mvx.RegisterSingleton<IMvxAndroidViewPresenter>(mvxFragmentsPresenter);
return mvxFragmentsPresenter;
}
ViewModel
public class ThirdViewModel : BaseViewModel
{
private MvxCommand _showSettingsCommand;
public MvxCommand ShowSettingsCommand =>
_showSettingsCommand ?? (_showSettingsCommand = new MvxCommand(() => ShowViewModel<HomeViewModel>()));
private MvxCommand _showCloseCommand;
public MvxCommand ShowCloseCommand =>
_showCloseCommand ?? (_showCloseCommand = new MvxCommand(() => ShowViewModel<SettingsViewModel>()));
}
I have created a custom control (CustomCard) which is a subclass of the CardView control. I would like to use this control within my project in different places.
For example, I may place the CustomCard within an xml layout manually, or I may want the CustomCard to be an item in an MvxListView. The key is that I would like to re-use the code as much as possible and benefit from having control over the CustomCard class.
When the CustomCard is instantiated, I am inflating it's layout using the standard layout inflater, see code:
using System;
using Android.Animation;
using Android.Content;
using Android.Support.V7.Widget;
using Android.Util;
using Android.Views;
using Android.Widget;
public class Card : CardView
{
private readonly Context _context;
public Card(Context context)
: base(context)
{
_context = context;
Init();
}
public Card(Context context, IAttributeSet attrs)
: base(context, attrs)
{
_context = context;
Init();
}
private void Init()
{
var inflater = (LayoutInflater) _context.GetSystemService(Context.LayoutInflaterService);
CardView = inflater.Inflate(Resource.Layout.base_card, this);
}
}
Within the layout base_card.xml, I have some elements that I would like to bind using MVVMCross, for example,
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/white">
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:id="#+id/basecard_title"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Title Text-->
<TextView
android:id="#+id/tv_basecard_header_title"
style="#style/card.title"
android:text="title text"
local:MvxBind="Text Title"
/>
<!-- ImageView -->
<MvxImageView
android:id="#+id/ib_basecard_header_button_expand"
style="#style/card.image"
local:MvxBind="Bitmap ImageBytes,Converter=InMemoryImage"/>
</RelativeLayout>
</FrameLayout>
My actual base_card layout is much more complex.
If I try to use my CustomCard within another XML Layout, none of the binding takes place. I think this is because I am using the standard layout inflater to inflate my base_card within my CustomCard rather than BindingInflate() but I can't be sure.
I have searched on SO and through the forums but I can't find any references to anyone using a custom control that inflates it's own view when instantiated with MVVMCross binding.
Has anyone done it, or am I trying to do something that isn't possible?
I ran into similar issue with CardView control. Since CardView directly inherits from FrameLayout I decided to use implementation almost identical to MvxFrameControl (Thanks Stuart for pointing out MvxFrameControl sample):
public class MvxCardView : CardView, IMvxBindingContextOwner
{
private object _cachedDataContext;
private bool _isAttachedToWindow;
private readonly int _templateId;
private readonly IMvxAndroidBindingContext _bindingContext;
public MvxCardView(Context context, IAttributeSet attrs)
: this(MvxAttributeHelpers.ReadTemplateId(context, attrs), context, attrs)
{
}
public MvxCardView(int templateId, Context context, IAttributeSet attrs)
: base(context, attrs)
{
_templateId = templateId;
if (!(context is IMvxLayoutInflater))
{
throw Mvx.Exception("The owning Context for a MvxCardView must implement LayoutInflater");
}
_bindingContext = new MvxAndroidBindingContext(context, (IMvxLayoutInflater)context);
this.DelayBind(() =>
{
if (Content == null && _templateId != 0)
{
Mvx.Trace("DataContext is {0}", DataContext == null ? "Null" : DataContext.ToString());
Content = _bindingContext.BindingInflate(_templateId, this);
}
});
}
protected MvxCardView(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
protected IMvxAndroidBindingContext AndroidBindingContext
{
get { return _bindingContext; }
}
public IMvxBindingContext BindingContext
{
get { return _bindingContext; }
set { throw new NotImplementedException("BindingContext is readonly in the list item"); }
}
protected View Content { get; set; }
protected override void Dispose(bool disposing)
{
if (disposing)
{
this.ClearAllBindings();
_cachedDataContext = null;
}
base.Dispose(disposing);
}
protected override void OnAttachedToWindow()
{
base.OnAttachedToWindow();
_isAttachedToWindow = true;
if (_cachedDataContext != null
&& DataContext == null)
{
DataContext = _cachedDataContext;
}
}
protected override void OnDetachedFromWindow()
{
_cachedDataContext = DataContext;
DataContext = null;
base.OnDetachedFromWindow();
_isAttachedToWindow = false;
}
[MvxSetToNullAfterBinding]
public object DataContext
{
get { return _bindingContext.DataContext; }
set
{
if (_isAttachedToWindow)
{
_bindingContext.DataContext = value;
}
else
{
_cachedDataContext = value;
if (_bindingContext.DataContext != null)
{
_bindingContext.DataContext = null;
}
}
}
}
}
Usage:
<YourNamespace.MvxCardView
android:layout_width="match_parent"
android:layout_height="match_parent"
local:MvxTemplate="#layout/base_card"
local:MvxBind="DataContext ." />
Note: Using custom implementation also solved my problem with binding click command to CardView control using local:MvxBind="Click MyCommand", which wasn't working until subclassing CardView.