Edit:
I first voted to close as a duplicate after finding this answer by James_D, which sets a TextFormatter on a TextField. But then firstly I found that (in a TableView context) the method TextFieldTableCell.forTableColumn() does not in fact draw a TextField when it starts editing, but instead a LabeledText, which does not subclass TextInputControl, and therefore does not have setTextFormatter().
Secondly, I wanted something which acted in a familiar sort of way. I may have produced the "canonical" solution in my answer: let others judge.
This is a TableColumn in a TableView (all Groovy):
TableColumn<Person, String> ageCol = new TableColumn("Age")
ageCol.cellValueFactory = { cdf -> cdf.value.ageProperty() }
int oldAgeValue
ageCol.onEditStart = new EventHandler(){
#Override
public void handle( Event event) {
oldAgeValue = event.oldValue
}
}
ageCol.cellFactory = TextFieldTableCell.forTableColumn(new IntegerStringConverter() {
#Override
public Integer fromString(String value) {
try {
return super.fromString(value)
}
catch ( NumberFormatException e) {
// inform user by some means...
println "string could not be parsed as integer..."
// ... and cancel the edit
return oldAgeValue
}
}
})
Excerpt from class Person:
public class Person {
private IntegerProperty age;
public void setAge(Integer value) { ageProperty().set(value) }
public Integer getAge() { return ageProperty().get() }
public IntegerProperty ageProperty() {
if (age == null) age = new SimpleIntegerProperty(this, "age")
return age
}
...
Without the start-edit Handler, when I enter a String which can't be parsed as an Integer NumberFormatException not surprisingly gets thrown. But I also find that the number in the cell then gets set to 0, which is likely not to be the desired outcome.
But the above strikes me as a pretty clunky solution.
I had a look at ageCol, and ageCol.cellFactory (as these are accessible from inside the catch block) but couldn't see anything better and obvious. I can also see that one can easily obtain the Callback (ageCol.cellFactory), but calling it would require the parameter cdf, i.e. the CellDataFeatures instance, which again you'd have to store somewhere.
I'm sure a validator mechanism of some kind was involved with Swing: i.e. before a value could be transferred from the editor component (via some delegate or something), it was possible to override some validating mechanism. But this IntegerStringConverter seems to function as a validator, although doesn't seem to provide any way to revert to the existing ("old") value if validation fails.
Is there a less clunky mechanism than the one I've shown above?
Edit
NB improved after kleopatra's valuable insights.
Edit2
Overhauled completely after realising that the best thing is to use the existing default editor and tweak it.
I thought I'd give an example with a LocalDate, slightly more fun than Integer. Given the following class:
class Person(){
...
private ObjectProperty<LocalDate> dueDate;
public void setDueDate(LocalDate value) {
dueDateProperty().set(value);
}
public LocalDate getDueDate() {
return (LocalDate) dueDateProperty().get();
}
public ObjectProperty dueDateProperty() {
if (dueDate == null) dueDate = new SimpleObjectProperty(this, "dueDate");
return dueDate;
}
Then you create a new editor cell class, which is exactly the same as TextFieldTreeTableCell (subclass of TreeTableCell), which is used by default to create an editor for a TreeTableView's table cell. However, you can't really subclass TextFieldTreeTableCell as, for example, its essential field textField is private.
So you copy the code in full from the source* (only about 30 lines), and you call it
class DueDateEditor extends TreeTableCell<Person, LocalDate> {
...
You then have to create a new StringConverter class, subclassing LocalDateStringConverter. The reason for subclassing is that if you don't do that it is impossible to catch the DateTimeParseException thrown by fromString() when an invalid date is received: if you use LocalDateStringConverter the JavaFX framework unfortunately catches it, without any frames in the stack trace involving your own code. So you do this:
class ValidatingLocalDateStringConverter extends LocalDateStringConverter {
boolean valid;
LocalDate fromString(String value) {
valid = true;
if (value.isBlank()) return null;
try {
return LocalDate.parse(value);
} catch (Exception e) {
valid = false;
}
return null;
}
}
Back in your DueDateEditor class you then rewrite the startEdit method as follows. NB, as with the TextFieldTreeTableCell class, textField is actually created lazily, when you first edit.
#Override
void startEdit() {
if (! isEditable()
|| ! getTreeTableView().isEditable()
|| ! getTableColumn().isEditable()) {
return;
}
super.startEdit();
if (isEditing()) {
if (textField == null) {
textField = CellUtils.createTextField(this, getConverter());
// this code added by me
ValidatingLocalDateStringConverter converter = getConverter();
Callable bindingFunc = new Callable(){
#Override
Object call() throws Exception {
// NB the return value from this is "captured" by the editor
converter.fromString( textField.getText() );
return converter.valid? '' : "-fx-background-color: red;";
}
}
def stringBinding = Bindings.createStringBinding( bindingFunc, textField.textProperty() );
textField.styleProperty().bind( stringBinding );
}
CellUtils.startEdit(this, getConverter(), null, null, textField);
}
}
NB don't bother trying to look up CellUtils: this is package-private, the package in question being javafx.scene.control.cell.
To set things up you do this:
Callback<TreeTableColumn, TreeTableCell> dueDateCellFactory =
new Callback<TreeTableColumn, TreeTableCell>() {
public TreeTableCell call(TreeTableColumn p) {
return new DueDateEditor( new ValidatingLocalDateStringConverter() );
}
}
dueDateColumn.setCellFactory(dueDateCellFactory);
... the result is a nice, reactive editor cell: when containing an invalid date (acceptable pattern yyyy-mm-dd; see other LocalDate.parse() variant for other formats) the background is red, otherwise normal. Entering with a valid date works seamlessly. You can also enter an empty String, which is returned as a null LocalDate.
With the above, pressing Enter with an invalid date sets the date to null. But overriding things to prevent this happening (i.e. forcing you to enter a valid date, or cancel the edit, e.g. by Escape) is trivial, using the ValidatingLocalDateStringConverter's valid field:
#Override
void commitEdit( LocalDate newDueDate ){
if( getConverter().valid )
super.commitEdit( newDueDate );
}
* I couldn't find this online. I extracted from the javafx source .jar file javafx-controls-11.0.2-sources.jar
Related
In my table I have one cell that does not update without interaction with the table.
I found the reason already here Java: setCellValuefactory; Lambda vs. PropertyValueFactory; advantages/disadvantages
My problem is, the default value of the cells item is LocalDate.MIN and I want my cell to contain "---" as long as the item has this default value. When I update the item, I want the cell to contain the current date string.
Item Class:
public class ItemEv {
private final ObjectProperty<LocalDate> openedAt;
#XmlJavaTypeAdapter(LocalDateAdapter.class)
public final LocalDate getOpenedAt() {
return openedAt.get();
}
public final ObjectProperty<LocalDate> openedAtProperty() {
return this.openedAt;
}
public final void setOpenedAt(LocalDate openedAt) {
this.openedAt.set(openedAt);
}
}
in another CellFactory I set the new value: i.setOpenedAt(LocalDate.now());
this is working but not wanted:
openedAtColumnEv.setCellValueFactory(cellData -> cellData.getValue().openedAtProperty().asString());
and this is what I tried so far:
openedAtColumnEv.setCellValueFactory(new Callback<CellDataFeatures<ItemEv, String>, ObservableValue<String>>() {
#Override
public ObservableValue<String> call(CellDataFeatures<ItemEv, String> i) {
if (i.getValue().getOpenedAt().equals(LocalDate.MIN)) {
return new SimpleStringProperty("---");
}
return i.getValue().openedAtProperty().asString();
}
});
and this:
openedAtColumnEv.setCellValueFactory(cellData -> {
if(cellData.getValue().openedAtProperty().getValue().equals(LocalDate.MIN)) {
return new SimpleStringProperty("---");
}
return cellData.getValue().openedAtProperty().asString();
});
Both of my tests return either SimpleStringProperty or StringBinding which should be fine.
In my tests I made a mistake where the first return in the IF statement does never return true, then the cell values show the standard string for LocalDate.MIN and get updated immediately when the item property changes.
Im a bit lost on this. Please forgive my bad english, Im not a native speaker.
If the property in the model class is an ObjectProperty<LocalDate>, then the column should be a TableColumn<ItemEv, LocalDate>, not a TableColumn<ItemEv, String>.
Implementing the cellValueFactory directly (typically with a lambda expression) is always preferable to using the legacy PropertyValueFactory class. You never "need to use" a PropertyValueFactory (and never should).
The cellValueFactory is only used to determine what data to display. It is not used to determine how to display the data. For the latter, you should use a cellFactory.
So:
private TableColumn<ItemEv, LocalDate> opendAtColumnEv ;
// ...
openedAtColumnEv.setCellValueFactory(cellData -> cellData.getValue().openedAtProperty());
openedAtColumnEv.setCellFactory(column -> new TableCell<ItemEv, LocalDate>() {
#Override
protected void updateItem(LocalDate openedAt, boolean empty) {
super.updateItem(openedAt, empty);
if (openedAt == null || empty) {
setText("");
} else {
if (openedAt.equals(LocalDate.MIN)) {
setText("---");
} else {
// Note you can use a different DateTimeFormatter as needed
setText(openedAt.format(DateTimeFormatter.ISO_LOCAL_DATE));
}
}
}
});
This is my first time working on JavaFx and I'm following this tutorial just as a template: http://code.makery.ch/library/javafx-8-tutorial/part3/.
For my application, I'm working with 2 columns on the left side, telephone number and the call start date/time. I'm wanting to change the formatting of the data in the table as it's currently coming through as yyyy-MM-ddThh:mm.
I can't seem to figure out where to place the formatting piece at. I have a date formatter function that you can find at the link above, but it's returning a string and giving me errors. Thanks for any help you can give. Here are some code snippets of what I'm working with.
Controller:
#FXML
private void initialize() {
// Initialize the person table with the two columns.
billingNumberColumn.setCellValueFactory(cellData -> cellData.getValue().billingNumberProperty());
callStartColumn.setCellValueFactory(cellData -> cellData.getValue().callStartProperty());
}
Model:
public LocalDateTime getCallStart() {
return callStart.get();
}
public void setCallStart(LocalDateTime callStart) {
this.callStart.set(callStart);
}
public ObjectProperty<LocalDateTime> callStartProperty() {
return callStart;
}
Date Format:
public static String format(ObjectProperty<LocalDateTime> callStart) {
if (callStart == null) {
return null;
}
return DATE_FORMATTER.format((TemporalAccessor) callStart);
}
Use a cellFactory. TextFieldTableCell provides a method to create a cell factory given a converter. As converter a LocalDateTimeStringConverter can be used:
callStartColumn.setCellValueFactory(cellData -> cellData.getValue().callStartProperty());
callStartColumn.setCellFactory(TextFieldTableCell.forTableColumn(new LocalDateTimeStringConverter(DATE_FORMATTER, DATE_FORMATTER)));
Specify column
TableColumn<Person, LocalDateTime> column = new TableColumn<>("Birth");
Code for this is quite complicated and not really good lookin.
Make sure yo utake care of empty case or / null handled when no data is in the cell
column.setCellFactory(
new Callback<TableColumn<Person, LocalDateTime>, TableCell<Person, LocalDateTime>>() {
#Override
public TableCell<Person, LocalDateTime> call(TableColumn<Person, LocalDateTime> param
) {
return new TableCell<Person, LocalDateTime>() {
#Override
protected void updateItem(LocalDateTime item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setStyle("");
} else {
//FORMAT HERE AND CALL setText() with formatted date string
}
}
};
}
}
);
I need to dinamically add columns to GXT grid. I can do that, but problem occurs, when I want to input data for rows. Thing is, that not all rows have specific column. So what I want to achieve is to check if given row has specific column and return proper value.
Problem is, that ValueProvider for my column doesn't allow to use arguments in it's methods. So I can't pass column name to ValueProvider, so it could check if given column exists in specific row and return proper data.
Here is my column:
ColumnConfig<SomeClass, String> column = new ColumnConfig<SomeClass, String> (props.attributeValue(name), 150, name);
Here is my ValueProvider
ValueProvider<LimitDTO, String> attributeValue(String name);
And here is my implementation (simplified):
public String getAttributeValue(String name) {
if(this.attributes.get(name) == null) {
return "";
} else {
return this.attributes.get(name);
}
}
But I get build error:
Method public abstract com.sencha.gxt.core.client.ValueProvider<com.example.SomeClass, java.lang.String> attributeValue(java.lang.String s) must not have parameters
SOLUTION
Thanks to your answers I was able to do it. This is my implementation of ValueProvider in case someone will look for solution. It wasn't so hard after all :)
public class CustomValueProvider implements ValueProvider<SomeClass, String> {
public String column;
public CustomValueProvider(String column) {
this.column = column;
}
#Override
public String getValue(SomeClass object) {
if(object.getAttributes().get(column) == null) {
return "";
} else {
return object.getAttributes().get(column);
}
}
#Override
public void setValue(SomeClass object, String value) {
}
#Override
public String getPath() {
return column.getName();
}
}
And here is how I used it
LimitsValueProvider lvp = new LimitsValueProvider(name);
ColumnConfig<SomeClass, String> newColumn = new ColumnConfig<>(lvp, 150, name);
Thanks a lot!
I would suggest, do not use
props.attributeValue(name)
Instead, you can follow the post Dynamic charts in GXT 3 and you can create your own dynamic value providers (See the section value providers), which will take columnId (path) as input and peform the same functionality.
Remember ValueProvider is just an interface and using GWT.create you provides its default implementation.
I am using the Oval validation framework to validate fields that HTML fields cannot hold malicious javascript code. For the malicious code detection, I am using an external framework that returns me a list of errors that I would like to use as error messages on the field. The problem I am running into is that I can only setMessage in the check implementation, while I would rather do something like setMessages(List). So while I am currently just joining the errors with a comma, I would rather pass them back up as a list.
Annotation
#Target({ ElementType.METHOD, ElementType.FIELD})
#Retention( RetentionPolicy.RUNTIME)
#Constraint(checkWith = HtmlFieldValidator.class)
public #interface HtmlField {
String message() default "HTML could not be validated";
}
Check
public class HtmlFieldValidator extends AbstractAnnotationCheck<HtmlDefaultValue> {
public boolean isSatisfied( Object o, Object o1, OValContext oValContext, Validator validator ) throws OValException {
if (o1 == null) {
return true;
} else {
CleanResults cleanResults = UIowaAntiSamy.cleanHtml((String) o1);
if (cleanResults.getErrorMessages().size() > 0) {
String errors = StringUtils.join(cleanResults.getErrorMessages(), ", ");
this.setMessage(errors);
return false;
} else {
return true;
}
}
}
}
Model class
class Foo {
#HtmlField
public String bar;
}
Controller code
Validator validator = new Validator(); // use the OVal validator
Foo foo = new Foo();
foo.bar = "<script>hack()</script>";
List<ConstraintViolation> violations = validator.validate(bo);
if (violations.size() > 0) {
// inform the user that I cannot accept the string because
// it contains invalid html, using error messages from OVal
}
If setMessage(String message) is a method created by a superclass, you can override it and once it receives the data, simply split the string into a list and call a second function in which you would actually place your code. On a side note, I would also recommend changing the separating string to something more unique as the error message itself could include a comma.
Your question doesn't really make much sense though. If you are "passing them back up" to a method implemented in a superclass, then this voids the entire point of your question as the superclass will be handling the data.
I am going to assume the setError methods is a simple setter that sets a String variable to store an error message that you plan to access after checking the data. Since you want to have the data in your preferred type, just create a new array of strings in your class and ignore the superclass. You can even use both if you so desire.
public class HtmlFieldValidator extends AbstractAnnotationCheck<HtmlDefaultValue> {
public String[] errorMessages = null;
public void setErrorMessages(String[] s) {
this.errorMessages = s;
}
public boolean isSatisfied( Object o, Object o1, OValContext oValContext, Validator validator ) throws OValException {
if (o1 == null) {
return true;
} else {
CleanResults cleanResults = UIowaAntiSamy.cleanHtml((String) o1);
if (cleanResults.getErrorMessages().size() > 0) {
//String errors = StringUtils.join(cleanResults.getErrorMessages(), ", ");
//this.setMessage(errors);
this.setErrorMessages(cleanResults.getErrorMessages());
return false;
} else {
return true;
}
}
}
}
Elsewhere:
HtmlFieldValidator<DefaultValue> hfv = new HtmlFieldValidator<DefaultValue>();
boolean satisfied = hfv.isSatisfied(params);
if (!satisfied) {
String[] errorMessages = hfv.errorMessages;
//instead of using their error message
satisfy(errorMessages);//or whatever you want to do
}
EDIT:
After you updated your code I see what you mean. While I think this is sort of overdoing it and it would be much easier to just convert the string into an array later, you might be able to do it by creating a new class that extends Validator its setMessage method. In the method, you would call super.setMethod as well as splitting and storing the string as an array in its class.
class ValidatorWithArray extends Validator {
public String[] errors;
public final static String SPLIT_REGEX = ";&spLit;";// Something unique so you wont accidentally have it in the error
public void setMessage(String error) {
super.setMessage(error);
this.errors = String.split(error, SPLIT_REGEX);
}
}
In HtmlFieldValidator:
public boolean isSatisfied( Object o, Object o1, OValContext oValContext, Validator validator ) throws OValException {
if (o1 == null) {
return true;
} else {
CleanResults cleanResults = UIowaAntiSamy.cleanHtml((String) o1);
if (cleanResults.getErrorMessages().size() > 0) {
String errors = StringUtils.join(cleanResults.getErrorMessages(), ValidatorWithArray.SPLIT_REGEX);
this.setMessage(errors);
return false;
} else {
return true;
}
}
}
And now just use ValidatorWithArray instead of Validator
The situation in which I want to achieve this was different from yours, however what I found was best in my case was to create an annotation for each error (rather than having one that would return multiple errors). I guess it depends on how many errors you are likely to be producing in my case it was only two or three.
This method makes also makes your code really easy to reuse as you can just add the annotations wherenever you need them and combine them at will.
At the moment I have this code (and I don't like it):
private RenderedImage getChartImage (GanttChartModel model, String title,
Integer width, Integer height,
String xAxisLabel, String yAxisLabel,
Boolean showLegend) {
if (title == null) {
title = "";
}
if (xAxisLabel == null) {
xAxisLabel = "";
}
if (yAxisLabel == null) {
yAxisLabel = "";
}
if (showLegend == null) {
showLegend = true;
}
if (width == null) {
width = DEFAULT_WIDTH;
}
if (height == null) {
height = DEFAULT_HEIGHT;
}
...
}
How can I improve it?
I have some thoughts about introducing an object which will contain all these parameters as fields and then, maybe, it'll be possible to apply builder pattern. But still don't have clear vision how to implement that and I'm not sure that it's worth to be done. Any other ideas?
So many parameters to a method is definitely a code smell. I would say a Chart object is waiting to be born. Here is a basic outline:
private RenderImage getChartImage(Chart chart) {
//etc.
}
private static class Chart {
private GanttChartModel model;
private String title = "";
//etc, initializing each field with its default value.
private static class Builder {
private Chart chart;
public Builder(GanttChartModel model) {
chart = new Chart();
chart.model = model;
}
public setTitle(String title) {
if (title != null) {
chart.title = title;
}
}
}
}
Other options include using primitives on the methods instead of objects to indicate that null isn't allowed, although that doesn't necessarily make it better. Another option is a bunch of overloaded methods, but given the types of parameters here, that doesn't really work because I get the idea that you want to make any of the parameters optional rather than having the first ones required and subsequent ones optional.
Your method's purpose is to construct a complex object. Therefore, the builder pattern seems appropriate to solve this problem. A builder can manage many options for the creation of an object.
Some properties of the image should not have a default value. For example, an image without a title is not very useful, but this depends on the needs of your application.
The use of a builder could look like:
RenderedImage image = RenderedImageBuilder.getNew(model)
.title("title").width(100).height(100)
.showLegend().build();
A further advantage of builders is that they make it easy to document any defaults for parameters and how they should be used.
The best I can think of off hand is to introduce a Parameter Object (which will also be a builder) called something like ChartOptions to contain all the options for this method.
The object could be built piecemeal:
ChartOptions options = new ChartOptions()
.setHeight(10)
.setWidth(100)
getChartImage(model, options);
etc.
If that doesn't work you can at least encapsulate the null check:
private <A> A checkNull(A object, A default)
{
return object == null ? default : object;
}
I would move that logic into the setter methods of the class you're returning an object of.
public class MyRenderedImage implements RenderedImage {
public MyRenderedImage(String title, ...) {
// constructor should call setters that do validation/coercion
}
public void setTitle(String title) {
if (title == null) {
this.title = "";
}
}
...
}
Another option to consider is to throw an InvalidArgumentException, but it sounds like you already know what you want to do.
Well, I'm thinking about that is there some framework support #NotNull annotation, if a method has this annotation, the framework will check all it's parameters.
#NotNull
public void doSomething(Parameter a, Parameter b) {
}
You can have a map values initially constructed. You can then do something like this,
private RenderedImage getChartImage(GanttChartModel model, String title,
Integer width, Integer height, String xAxisLabel,
String yAxisLabel, Boolean showLegend) {
title = removeNull(KEY_TITLE,title);
xAxisLabel = removeNull(KEY_X,xAxisLabel);
yAxisLabel = removeNull(KEY_Y,yAxisLabel);
showLegend = removeNull(KEY_LEG,showLegend);
width = removeNull(KEY_W,width);
height = removeNull(KEY_H,height);
}
//initialize the defaultMap with the key-value of default pairs
Map<Object,Object> defaultMap;
private Object removeNull(Object keyTitle, Object value) {
if(value==null){
return defaultMap.get(keyTitle);
}
return value;
}