How to set validation check in ViewModel - java

I am newbie in building javafx MVVM app.
I've created a simple ViewModel:
public class PersonViewModel {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty age = new SimpleIntegerProperty();
public PersonViewModel() {}
// getters and setters
}
and simple View:
public class PersonView implements Initializable {
#FXML
TextField name;
#FXML
TextField age;
#FXML
Button ok;
#Override
public void initialize(URL location, ResourceBundle resources) {
PersonViewModel viewModel = new PersonViewModel();
name.textProperty().bindBidirectional(viewModel.name);
age.textProperty().bindBidirectional(viewModel.age);
}
}
Can you give me any idea how to make age validation? F.e. I wanna not to allow user to put characters into age (TextField) except [a-zA-Z]. And the main idea of my question to make this validation in ViewModel) Help me pls.
P.S. I wanna make it not using not standard javafx components.

You can use a TextFormatter both to filter input in a text input control, and to convert the text into a value of a specific type. If you want the view model to define the validation rules, then define a method in there representing the validation, and delegate to that method in the filter definition for the TextFormatter. For example:
public class PersonViewModel {
private final StringProperty name = new SimpleStringProperty();
public StringProperty nameProperty() {
return name ;
}
public final String getName() {
return nameProperty().get();
}
public final void setName(String name) {
nameProperty.set(name);
}
private final IntegerProperty age = new SimpleIntegerProperty();
public IntegerProperty ageProperty() {
return age ;
}
public final int getAge() {
return ageProperty().get();
}
public final void setAge(int age) {
ageProperty.set(age);
}
public boolean validAgeInput(String input) {
// must support partial entry while editing, including empty string
// accept any integer from 0 - 135 (arbitrary upper bound example)
String regex = "([0-9]{0,2})|(1[0-2][0-9])|(13[0-5])";
return input.matches(regex);
}
}
Now you can do:
public class PersonView implements Initializable {
#FXML
TextField name;
#FXML
TextField age;
#FXML
Button ok;
#Override
public void initialize(URL location, ResourceBundle resources) {
PersonViewModel viewModel = new PersonViewModel();
UnaryOperator<Change> filter = change -> {
if (viewModel.validAgeInput(change.getControlNewText()) {
// accept
return change ;
} else {
// reject
return null ;
}
};
TextFormatter<Integer> ageFormatter = new TextFormatter<>(new IntegerStringConverter(), 0, filter);
age.setTextFormatter(ageFormatter);
ageFormatter.valueProperty().bindBidirectional(viewModel.ageProperty().asObject());
name.textProperty().bindBidirectional(viewModel.nameProperty());
}
}
The filter defined here will only accept input in the control if it matches the rule defined by the method in the PersonViewModel. The valueProperty() of the TextFormatter represents the text in the TextField after passing it to the IntegerStringConverter: this is bound bidirectionally to the ageProperty() in the model. (The call to asObject() effectively just converts between an IntegerProperty and an ObjectProperty<Integer>.)

Related

Check if TextFieldTableCell equals something on edit commit?

I am using a custom TextFieldTableCell in JavaFX 8 to allow users to edit the text field. When the user hits Enter, however, I want to check to see if the text field equals a certain value. If it does equal this certain value, I do not want the entry to save and for it to revert to the text it had before the user started editing. Is there a method I can override to produce this result? I cannot find one that fits what I am looking for.
Thank you in advance!
Since the model is bound to the cell's data, you can do the validation and reset part in the modell class, in your case in the Person class.
Here is a simple example how you can do it:
public class Controller implements Initializable {
#FXML
private TableColumn<Model, String> name;
#FXML
private TableView<Model> tableView;
#Override
public void initialize(URL location, ResourceBundle resources) {
name.setCellValueFactory(data -> data.getValue().nameProperty());
name.setCellFactory(cell -> new TextFieldTableCell<>(new StringConverter<String>() {
#Override
public String toString(String object) {
return object;
}
#Override
public String fromString(String string) {
return string;
}
}));
tableView.setEditable(true);
tableView.setItems(FXCollections.observableArrayList(new Model("Test")));
}
private class Model {
private StringProperty name;
ChangeListener<String> nameChangeListener = (observable, oldValue, newValue) -> {
if (!newValue.matches("[A-Z][a-zA-Z]*")) { // a validation example insert your here.
this.name.set(oldValue);
}
};
public Model(String name) {
this.name = new SimpleStringProperty(name);
this.name.addListener(nameChangeListener);
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
}
}
From your description I assume that you are talking about an editable cell in a table view.
If that's the case, the following example should be working for you. You can use the .setOnEditCommit() method to add an event handler to the colum for which you would like to check the entered value.
//Create table
TableView<Person> table = new TableView<Person>();
table.setEditable(true);
//Create column
TableColumn<Person, String> column = new TableColumn<Person, String>("Full Name");
column.setCellValueFactory(new PropertyValueFactory<>("fullName"));
column.setCellFactory(TextFieldTableCell.<Person> forTableColumn());
column.setMinWidth(200);
column.setOnEditCommit(event -> {
//Get entered value
String newFullName = event.getNewValue();
//Get selected position
TablePosition<Person, String> pos = event.getTablePosition();
//Get row of position
int row = pos.getRow();
//Get data from selected row
Person person = event.getTableView().getItems().get(row);
//Check if text equals ...
if (newFullName.equals("Test")) {
person.setFullName(newFullName);
} else {
person.setFullName(event.getOldValue());
table.refresh();
}
});

How to restrict textfields in TornadoFX to numbers only

The problem here is that I wanna make sure that the user doesn't enter any strings or text especially that I need to enter his choice into a database later so I don't things to get messed up in the database's part, here is part of code which is the view I wish to use the textview with restricted Integers (specifically the amount am field).
PS: I'm still new to both JavaFX and TornadoFX so hope this doesn't sound like a rather silly question.
My Code:
package com.company.view
import javafx.beans.property.SimpleIntegerProperty
import javafx.scene.control.CheckBox
import tornadofx.*
import javafx.scene.control.TextField
import javafx.util.converter.NumberStringConverter
import java.sql.Connection
class Add: View() {
override val root = Form()
private val mainMenu: MainMenu by inject()
private var cname: TextField by singleAssign()
private var address: TextField by singleAssign()
private var sname: TextField by singleAssign()
private var ch: CheckBox by singleAssign()
private var am: TextField by singleAssign()
var conn: Connection?= mainMenu.conn
init {
with(root) {
vbox(30.0) {
fieldset("Enter Your Info below") {
field("Enter The Customer's Name") {
cname = textfield()
}
field("Enter the Customer's address") {
address = textfield()
}
field("Enter Bought Stock's Name") {
sname = textfield()
}
field("Do you wish to pay now?") {
ch = checkbox()
}
field("Enter the amount you wish to buy"){
am = textfield()
}
button("Submit")
{
setOnAction {
addPayment(cname.text, address.text, sname.text, ch.isSelected, am.text)
}
}
}
}
}
}
private fun addPayment(cusName: String, caddress: String, stname: String, che: Boolean,am: String){
//required code for inserting into the database here.
}
}
You can use the filterInput extension function we've added to TextField and check that the text after the addition is in int. If it's not, deny the last input change:
textfield {
filterInput { it.controlNewText.isInt() }
}
On another note, you really need to look into ItemViewModel. It's an anti-pattern to assign each input element to a variable and extract the values from the input values on submit. Your code will be a lot cleaner and easier to reason about and refactor later if you use view models.
PS: The filterInput function is available in the soon to be released TornadoFX 1.7.15, in the mean time you can add this extension function to your project:
fun TextInputControl.filterInput(discriminator: (TextFormatter.Change) -> Boolean) {
textFormatter = TextFormatter<Any>(CustomTextFilter(discriminator))
}
From your example it seems like that you'd want to use a PropertySheet which comes from ControlsFX. I use it in production and it works well with TornadoFX.
Here is an example from the samples project which you can peruse. This will let you edit and bind multiple types not just numbers:
public class PropertySheetExample extends VBox {
private static Map<String, Object> customDataMap = new LinkedHashMap<>();
static {
customDataMap.put("Group 1#My Text", "Same text"); // Creates a TextField in property sheet
customDataMap.put("Group 1#My Date", LocalDate.of(2000, Month.JANUARY, 1)); // Creates a DatePicker
customDataMap.put("Group 2#My Enum Choice", SomeEnumType.EnumValue); // Creates a ChoiceBox
customDataMap.put("Group 2#My Boolean", false); // Creates a CheckBox
customDataMap.put("Group 2#My Number", 500); // Creates a NumericField
}
class CustomPropertyItem implements PropertySheet.Item {
private String key;
private String category, name;
public CustomPropertyItem(String key) {
this.key = key;
String[] skey = key.split("#");
category = skey[0];
name = skey[1];
}
#Override
public Class<?> getType() {
return customDataMap.get(key).getClass();
}
#Override
public String getCategory() {
return category;
}
#Override
public String getName() {
return name;
}
#Override
public String getDescription() {
return null;
}
#Override
public Object getValue() {
return customDataMap.get(key);
}
#Override
public void setValue(Object value) {
customDataMap.put(key, value);
}
}
public PropertySheetExample {
ObservableList<PropertySheet.Item> list = FXCollections.observableArrayList();
for (String key : customDataMap.keySet())
list.add(new CustomPropertyItem(key));
PropertySheet propertySheet = new PropertySheet(list);
VBox.setVgrow(propertySheet, Priority.ALWAYS);
getChildren().add(propertySheet);
}
}
You can also take a look at this question for more info.

[JavaFx ]What is wrong with this binding?

So I have created Instrument class and Controller class. I have big problem with bindingBidirectional() method. It gives me an error when i'm trying to bind Combobox property with AmountProperty in Instrument class.
amount.valueProperty().bindBidirectional(instrument.amountProperty());
What am I doing wrong here?
Controller class
public class Controller implements Initializable{
#FXML
private ComboBox<Integer> amount = new ComboBox<>();
ObservableList<Integer> amountOptions = FXCollections.observableArrayList(0, 5, 10, 25, 50);
Instrument instrument = new Instrument();
#Override
public void initialize(URL location, ResourceBundle resources) {
amount.getItems().addAll(amountOptions);
//THIS ONE IS NOT WORKING
amount.valueProperty().bindBidirectional(instrument.amountProperty());
}}
And Instrument class:
public class Instrument {
private IntegerProperty amount = new SimpleIntegerProperty();
public int getAmount() {
return amount.get();
}
public IntegerProperty amountProperty() {
return amount;
}
public void setAmount(int amount) {
this.amount.set(amount);
}
}
IntegerProperty is an implementation of Property<Number>, not of Property<Integer>. The valueProperty in your combo box is a Property<Integer>. Consequently you cannot bind bidirectionally between the two directly, as the types don't match.
You can either change your combo box to be a ComboBox<Number>, or use IntegerProperty.asObject(), which creates an ObjectProperty<Integer> that is bidirectionally bound to the IntegerProperty:
amount.valueProperty().bindBidirectional(
instrument.amountProperty().asObject());

Javafx tableview reflection not working

I am trying to fill JavaFx TableView Columns with mock data, but I keep getting a reflection error, even though I think I'm following Bean conventions correctly:
// Data model
class SensorTableEntry {
SensorTableEntry(Integer id, String man, String type, String addr) {
this.id = new SimpleIntegerProperty(id);
this.manufacturer = new SimpleStringProperty(man);
this.type = new SimpleStringProperty(type);
this.btAddress = new SimpleStringProperty(addr);
}
private IntegerProperty id;
public Integer getId() { return idProperty().get(); }
public void setId(Integer value) { idProperty().set(value); }
public IntegerProperty idProperty() { return id; }
private StringProperty manufacturer;
public void setManufacturer(String value) { manufacturerProperty().set(value); }
public String getManufacturer() { return manufacturerProperty().get(); }
public StringProperty manufacturerProperty() { return manufacturer; }
private StringProperty type;
public void setType(String value) { typeProperty().set(value); }
public String getType() { return typeProperty().get(); }
public StringProperty typeProperty() { return type; }
private StringProperty btAddress;
public void setBtAddress(String value) { btAddressProperty().set(value); }
public String getBtAddress() { return btAddressProperty().get(); }
public StringProperty btAddressProperty() { return btAddress; }
}
// More code before this...
// Actual table inside the controller
ObservableList<SensorTableEntry> sensorEntries = FXCollections.observableArrayList(
new SensorTableEntry(1, "manufacturer", "type", "00:00:00:00:00:00")
);
TableView<SensorTableEntry> table = new TableView<SensorTableEntry>();
TableColumn<SensorTableEntry,Integer> idCol = new TableColumn<SensorTableEntry,Integer>("ID");
idCol.setCellValueFactory(new PropertyValueFactory<SensorTableEntry,Integer>("id"));
TableColumn<SensorTableEntry,String> manufacturerCol = new TableColumn<SensorTableEntry,String>("Manufacturer");
manufacturerCol.setCellValueFactory(new PropertyValueFactory<SensorTableEntry,String>("manufacturer"));
TableColumn<SensorTableEntry,String> typeCol = new TableColumn<SensorTableEntry,String>("Type");
typeCol.setCellValueFactory(new PropertyValueFactory<SensorTableEntry,String>("type"));
TableColumn<SensorTableEntry,String> btAddressCol = new TableColumn<SensorTableEntry,String>("Bluetooth Address");
btAddressCol.setCellValueFactory(new PropertyValueFactory<SensorTableEntry,String>("btAddress"));
table.setItems(sensorEntries);
table.getColumns().addAll(
idCol,
manufacturerCol,
typeCol,
btAddressCol
);
pane.getChildren().add(table);
I have checked other answers to similar questions like:
Javafx PropertyValueFactory not populating Tableview
JavaFx TableView not filling all required columns
Javafx tableview not showing data in all columns
But no matter how much I check I don't seem to find where my naming went wrong. Am I missing something?
The exception I get is:
Exception in thread "JavaFX Application Thread" java.lang.RuntimeException: java.lang.IllegalAccessException: Class sun.reflect.misc.Trampoline can not access a member of class SensorTableEntry with modifiers "public"
at com.sun.javafx.property.PropertyReference.getProperty(PropertyReference.java:200)
Your properties must be fully accessible so their getter and their owner class must both be public.
So simply replace this:
class SensorTableEntry {
With this:
public class SensorTableEntry {
Since you are using JavaFX properties in your model, you can use actual implementations of the callback (with lambda expressions for brevity) and avoid reflection completely. Note that IntegerProperty implements Property<Number>, not Property<Integer>, so you will need to fix the types (see JavaFX Properties in TableView):
TableColumn<SensorTableEntry,Number> idCol = new TableColumn<SensorTableEntry,Number>("ID");
idCol.setCellValueFactory(cellData -> cellData.getValue().idProperty());
TableColumn<SensorTableEntry,String> manufacturerCol = new TableColumn<SensorTableEntry,String>("Manufacturer");
manufacturerCol.setCellValueFactory(cellData -> cellData.getValue().manufacturerProperty());
TableColumn<SensorTableEntry,String> typeCol = new TableColumn<SensorTableEntry,String>("Type");
typeCol.setCellValueFactory(cellData -> cellData.getValue().typeProperty());
TableColumn<SensorTableEntry,String> btAddressCol = new TableColumn<SensorTableEntry,String>("Bluetooth Address");
btAddressCol.setCellValueFactory(cellData -> cellData.getValue().btAddressProperty());
This is generally a much better approach: the compiler will check that the properties exist and are of the correct type, and since you are not relying on reflection to evaluate the cell values, performance will be better (probably negligibly, but nevertheless...).
One other aside: in the JavaFX property pattern, the methods for the primitive wrapper properties should use primitive types, not object wrapper types, i.e.:
class SensorTableEntry {
SensorTableEntry(int id, String man, String type, String addr) {
this.id = new SimpleIntegerProperty(id);
this.manufacturer = new SimpleStringProperty(man);
this.type = new SimpleStringProperty(type);
this.btAddress = new SimpleStringProperty(addr);
}
private IntegerProperty id;
public int getId() { return idProperty().get(); }
public void setId(int value) { idProperty().set(value); }
public IntegerProperty idProperty() { return id; }
// existing code...
}

JavaFx - values from TableColumn do not change

I'm trying to get the value of the editable table using javaFX Scene builder, but I can not get the updated value.
I' have table with 2 columns: name column and value column.
whenever the user change on of the value of value column, I want to react.
But when I print the new change value, it always displays the default value.
public class MesssageField
{
private final StringProperty fieldName;
private final StringProperty fieldValue;
public MesssageField(String fieldName, String fieldValue) {
this.fieldName = new SimpleStringProperty(fieldName);
this.fieldValue = new SimpleStringProperty(fieldValue);
}
public StringProperty getFieldNameProperty() {
return fieldName;
}
public StringProperty getFieldValueProperty() {
return fieldValue;
}
public void setFieldValue(String fieldValue) {
this.fieldValue.set(fieldValue);
}
public String getFieldName() {
return fieldName.get();
}
public String getFieldValue() {
return fieldValue.get();
}
}
The controller class has:
private static ObservableList<MesssageField> obserListMsgsField;
#FXML
private TableView<MesssageField> msgTableView;
#FXML
private TableColumn<MesssageField, String> fieldNameColumn;
#FXML
private TableColumn<MesssageField, String> fieldValueColumn;
#Override
public void initialize(URL arg0, ResourceBundle arg1)
{
// create List Of fields
obserListMsgsField = FXCollections.observableArrayList();
fieldValueColumn.setCellFactory(TextFieldTableCell.forTableColumn());
// Initialize the person table with the two columns.
fieldNameColumn.setCellValueFactory(cellData -> cellData.getValue().getFieldNameProperty());
fieldValueColumn.setCellValueFactory(cellData -> cellData.getValue().getFieldValueProperty());
}
the onEditFieldValueCommit method attached to "on edit commit"
#FXML
public void onEditFieldValueCommit() {
MesssageField messageField = msgTableView.getSelectionModel().getSelectedItem();
// get field name
String fieldName = messageField.getFieldName();
// get field value
String valueString = messageField.getFieldValue();
// debug print
System.out.print("\n[DEBUG] Field Name = " + fieldName + " = " + valueString);
}
But the output is always the default value and not the changed value.
Thanks
In your onEditFieldValueCommit method you are not using the parameter required, and then you are not updating the list obserListMsgsField.
First, you need to add a parameter of the type TableColum.CellEditEvent, which is the event that is fired when a user performs an edit on the table cell.
Then you just get the new value or the row affected, updating the list:
#FXML
public void onEditFieldValueCommit(TableColumn.CellEditEvent<MesssageField, String> t) {
t.getRowValue().setFieldValue(t.getNewValue());
System.out.print("\n[DEBUG] Field Name = " + t.getRowValue().getFieldName() +
" = " + t.getRowValue().getFieldValue());
}

Categories

Resources