Assume following component tree:
A
B
C
A has a private field (called filter) and the reference to the filter is passed on to B and C.
In class B a property of the filter is modified through an AjaxLink (so no page refresh).
I see (through) logging that after each ajax call wicket will serialize A, B and C.
Now, everything works fine, clicking in the AjaxLinks in B will nicely update the filter and will refresh C and show the correct information in C (based on the filter).
However, assume if click an AjaxLink in B which will update a property of the filter to (for instance) value 2. Afterwards I press F5, it still works and the page (and all components in it) is deserialized (and again serialized afterwards). I then click on an AjaxLink the changes the value of the mentioned property to 3; this still works fine. If, however, I then do a page refresh (F5) the value of that property suddenly becomes 2 again.
It seems that on the page refresh (when wicket will load the page from disk) wicket is deserializing an older version of the filter.
Schematically:
Page initial load:
=> filter.value = 3 -> serialized
AjaxLink:
=> filter.value = 2 -> serialized
Page refresh:
=> filter.value = 2 -> deserialized/serialized
AjaxLink:
=> filter.value = 3 -> serialized
Page refresh:
=> filter.value = 2 -> deserialized/serialized
It seems that on action 5 the serialized version after action 4 is ignored and the serialized version after action 3 the loaded.
Hoping for a cause, explanation and if possible also the solution :-)
CODE
public class A extends Panel { //creates the filter and passes the reference on
Filter filter = new Filter(TypeEnum.ALL);
public A(final String id) {
super(id);
add(new B("filterPanel", filter));
add(new C("datatable", filter));
}
public class B extends Panel { //updates the filter
Filter filter;;
public B(final String id, Filter filter) {
super(id);
this.filter = filter;
add(new IndicatingAjaxLink<Void>("other") {
#Override
public void onClick(AjaxRequestTarget target) {
filter.setType(TypeEnum.OTHER);
...
}
};
}
}
public class C extends Panel { //uses the filter
Filter filter;;
public C(final String id, Filter filter) {
super(id);
this.filter = filter;
}
public void populateRepeatingView() {
final List<? extends WallEntry> result = service.find(filter);
...
}
}
Code has been simplified and I retained only the (I assume) pertinent stuff.
UPDATE
If I add following in my page class then it works:
#Override
public boolean isVersioned() {
return false;
}
Not sure however about the implications of this and why this makes it work. Isn't the page (de)serialized anymore?
UPDATE
I added following to my Page class:
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
System.err.println("Writing " + this + something to print out the type of the filter);
}
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
ois.defaultReadObject();
System.err.println("Reading " + this + something to print out the type of the filter);
}
When the Page is loaded first it prints (actually it prints this 5 times, not sure if it's normal):
Writing [Page class = com.bnpp.ecom.homepage.web.Homepage, id = 0, render count = 1]: type = ALL
When I click on AjaxLink 'ALL' (that will update the filter) it still prints:
Writing [Page class = com.bnpp.ecom.homepage.web.Homepage, id = 0, render count = 1]: type = ALL
When I click on AjaxLink 'DISCUSSIONS' (that will update the filter) it still prints:
Writing [Page class = com.bnpp.ecom.homepage.web.Homepage, id = 0, render count = 1]: type = DISCUSSIONS
When I refresh the page (F5) the pageid is updated:
Writing [Page class = com.bnpp.ecom.homepage.web.Homepage, id = 1, render count = 2]: type = DISCUSSIONS
When I click on AjaxLink 'ALL' (that will update the filter) it prints:
Writing [Page class = com.bnpp.ecom.homepage.web.Homepage, id = 1, render count = 1]: type = ALL
So far so good but when I refresh the page now (F5) this is printed out:
Reading [Page class = com.bnpp.ecom.homepage.web.Homepage, id = 0, render count = 1]: type = DISCUSSIONS
Writing [Page class = com.bnpp.ecom.homepage.web.Homepage, id = 2, render count = 2]: type = DISCUSSIONS
The url never changes, it stays http://.../?0
So it deserializes the page with id 0 although the last known page id was 1 and all changes that were done for version 1 are ignored (in this case switching the type from DISCUSSIONS to ALL).
Created a issue in the wicket jira for this: https://issues.apache.org/jira/browse/WICKET-4360
Watch the browser's url When you refresh the page. It probably includes the Wicket page version, so when you refresh a second time, a specific version of the page is deserialized.
Since you keep the filter in your page, it is serialized/deserialized along with its containing components.
A solution would be to put the filter into the session.
As mentioned in the jira issue: https://issues.apache.org/jira/browse/WICKET-4360 you need to setreuseitems on listviews to true or an ajax call will dirty your page when the listview is repopulated:
On F5 Wicket reads (once) the current page. But then it writes a the
page with a new page id because of the usage of ListView
(PropertyListView in DataPanel). By adding
"commentsListView.setReuseItems(true);" all is fine. See WICKET-4286
for a discussion about the ListView problem.
In your log statements, it seems like its only reading on the fifth request. That's probably why it works on the first page refresh.
Wicket serializes the page each time, but it keeps the last version of the page in memory and only de-serializes it if a previous version is requested.
If you check the hashCode of the Filters in the A, B, and C Components, you'll probably see they are different objects after deserialization. Instead of passing around a Filter, pass around a model which returns the Filter from Component A. If component A is a Page, it's really easy because every Component can invoke getPage().getDefaultModel() or getDefaultModelObject(). If Component A is a Panel, try navigating to it with getPage().get("A").getDefaultModel().
I think if the app is using the same Filter object, serialization wont matter.
Related
I am currently making a form for film database, using Vaadin. My problem is the whole situation with deep linking. I want to have a url with an id of movie in it, to make possible to get to the form of concrete movie.
localhost:port/filmView/idOfSelectedMovie
I am currently using pushState but there are couple of problems.
1) When I add string "Film/" before Id, the first selection works fine, however with following selections the url just keeps adding this.
http://localhost:8081/Film/Film/Film/Film/Film/4
2) The second option I tried was using just an id. The effect was that the url just keeps the host with port, and gets rid of ViewName
http://localhost:8801/4
I had already tried to use replaceState and Urifragment methods, the effect wasn't better at all.
the function handling selection of movie on the list
this.itemsList.addSelectionListener(selectionEvent -> {
if (selectionEvent.getFirstSelectedItem().isPresent()) {
Film selectedFilm = selectionEvent.getFirstSelectedItem().get();
this.setupForm(selectedFilm);
Page.getCurrent().pushState("Film/" + selectedFilm.getFilmId());
}
});
What you have to do while navigating between views this: let's say you have a View with a list of films, from you can select a film. When you select a film from that list, you move to the film View
getUI().getNavigator().navigateTo(FilmView + "/" + filmId); //let's say id 88
in this way, you will navigate to http://localhost:8081/Film/88
now, in your FilmView you can get and use this id, something like:
public class FilmView extends VerticalLayout implements View{
#Override
public void enter(ViewChangeEvent event) {
String yourPassedId = event.getParameters();
//do stuff with your id, for example loading from DB
}
}
In this way, you can reach every film you want with hard link, like http://localhost:8081/Film/88
It is hard to explain so I'll use an example:
#Override
public void start(Stage primaryStage) throws Exception
{
final VBox vbox = new VBox();
final Scene sc = new Scene(vbox);
primaryStage.setScene(sc);
final TableView<Person> table = new TableView<>();
final TableColumn<Person, String> columnName = new TableColumn<Person, String>("Name");
table.getColumns().add(columnName);
final ObservableList<Person> list = FXCollections.observableArrayList();
list.add(new Person("Hello"));
list.add(new Person("World"));
Bindings.bindContent(table.getItems(), list);
columnName.setCellValueFactory(new PropertyValueFactory<>("name"));
vbox.getChildren().add(table);
final Button button = new Button("test");
button.setOnAction(event ->
{
final Person removed = list.remove(0);
removed.setName("Bye");
list.add(0, removed);
});
vbox.getChildren().add(button);
primaryStage.show();
}
public static class Person
{
private String name = "";
public Person(String n)
{
name = n;
}
public String getName()
{
return name;
}
public void setName(String n)
{
name = n;
}
}
In this example, I show a TableView with a single column named "Name". Running this sample code, you will get two rows: first row with "Hello" in "Name" column; and second row with "World" in "Name" column.
Additionally, there is a button, this button removes the first Person object from the list, then makes some changes to the object, then adds it back in at the same index. Doing so would cause any ListChangeListener added to the ObservableList to be triggered, and I have tested this to be true.
I would expect the row with "Hello" to be replaced with "Bye", but it seems like the TableView continues to show "Hello". If I used a TimeLine to add delay before I add the removed Person object back to the list, it would change to "Bye".
final Timeline tl = new Timeline(new KeyFrame(Duration.millis(30), ae -> list.add(0, removed)));
tl.play();
Is there something weird with the API? Is there any way to do this without this problem?
This is essentially expected behavior.
Note that (and I'm guessing you are trying to work around this issue), if you simply called
list.get(0).setName("Bye");
which has the same effect in terms of the underlying data, the table would not update as it has no way of being notified that the String field name in the element of the list has changed.
The code
Person removed = list.remove(0);
removed.setName("Bye");
list.add(0, removed);
is really equivalent to list.get(0).setName("Bye");: you just temporarily remove the item from the list before changing it, and then add it back. As far as the list is concerned, the net result is the same. I guess you are doing this in the hope that removing and replacing the item from the list will persuade the table to notice the state of the item has changed. There's no guarantee this will be the case. Here is what's happening:
The binding between your two lists:
Bindings.bindContent(table.getItems(), list);
works like any other binding: it defines how to get the value of the binding (the elements of list), and marks the data as invalid if list is invalidated at any time. The latter happens when you add and remove elements from list.
The TableView will not perform layout every time the binding to the list changes; instead, when then binding is invalidated (add or remove an element), then the table view marks itself as potentially needing to be redrawn. Then, on the next rendering pulse, the table will check the data and see if it really needs to be redrawn, and re-render if needed. There are obvious performance-saving features of this implementation.
So what happens with your code is that an item is removed from the list, causing the binding to be marked as invalid. The item is then changed (by calling setName(...)), and the same item is then added back into the list at the same position. This also causes the binding to be marked as invalid, which has no effect (it is already invalid).
No rendering pulse can occur between the removal and re-addition of this element. Consequently, the first time the table actually looks at the changes that were made to the list has to be after the entire remove-change-add process. At that point, the table will see that the list still contains the exact same elements in the exact same order that it previously contained. (The internal state of one of the elements has changed, but since this is not an observable value - not a JavaFX property - the table is unaware of this.) Consequently, the table sees no changes (or sees that all the changes have cancelled each other out), and doesn't re-render.
In the case where you add the pause, then a rendering frame (or two) occurs between the removal of the item and its re-addition. Consequently, the table actually renders one or two frames without the item, and when it is added back in, it adds it back and renders the current value. (You might, possibly, be able to make the behavior unpredictable, by pausing for 16 or 17 milliseconds, which is right on the cusp of the time for one rendering frame.)
It's not clear what you really intend to do. If you are trying to persuade the table to update without using JavaFX properties, you can do
list.get(0).setName("Bye");
table.refresh();
though this is not a very satisfactory solution.
Note too that
list.remove(0);
list.add(0, new Person("Bye"));
will also work (since now the added element is not the same as the removed element).
The better approach is to implement your model class with JavaFX properties:
public static class Person
{
private final StringProperty name = new SimpleStringProperty("");
public Person(String n)
{
setName(n);
}
public StringProperty nameProperty() {
return name ;
}
public final String getName()
{
return nameProperty().get();
}
public final void setName(String n)
{
nameProperty().set(n);
}
}
and then simply calling
list.get(0).setName("Bye");
will update the table (because the cell will be observing the property).
This is my zul code:
<combobox id="digitalPublisherCombobox" value="#load(ivm.inventory.digitalPublisherName)"
onOK="#command('setDigitalPublisher', digitalPublisherBox = self)"
onSelect="#command('setDigitalPublisher', digitalPublisherBox = self)"
onChanging="#command('setupQuicksearchByEvent', searchlayout = event, prefix = 'PUB', tags = 'PublisherName, PublisherNameTranslit')"
mold="rounded" hflex="1" buttonVisible="false" autodrop="true">
<comboitem self="#{each=entry}" value="#{entry.key}" label="#{entry.value}"/>
</combobox>
And this is QuickSearch implementations:
#Command
public void setupQuicksearchByEvent(#BindingParam("searchlayout")Event event, #BindingParam("prefix") String prefix, #BindingParam("tags") String tags) throws WrongValueException, SearchException, IOException
{
if(event instanceof InputEvent)
{
InputEvent inputEvent = (InputEvent) event;
String inputText = inputEvent.getValue();
List<String> searchFields = Arrays.asList(tags.split(","));
ListModel model = new ListModelMap(ZKLogic.findDocsStartingWith(prefix, searchFields, "proxy", inputText), true);
ListModel subModel = ListModels.toListSubModel(model, Autocompleter.MAP_VALUE_CONTAINS_COMPARATOR, 10);
Combobox searchBox = (Combobox) event.getTarget();
searchBox.setModel(subModel);
searchBox.setItemRenderer(new ComboitemRenderer()
{
#Override
public void render( Comboitem item, Object data, int pos ) throws Exception
{
String publisherString = data.toString();
UID key = getUidFromPublisherString(publisherString);
int startIndex = publisherString.indexOf('=') + 1;
String publisher = publisherString.substring(startIndex);
item.setLabel(publisher);
item.setValue(key);
}
});
}
}
ZKLogic.findDocsStartingWith return map with UID-key and String-value.
With code above I achieved to get dropdown list when I switch to another window. I need to type something, then select another browser or notepad window - and comboitems will be displayed immediately.
So, my question still need answer, is there are any techniques to reproduce this windows switching in code? Or maybe I should do something with autocomplete, because I've got some ac working with preloaded lists, but this thing should return only 10 records from db, instead of all 70000 entries, every time when user type something in the field.
Edit 20/09/2013: Problem still exist. Rename question a bit, because thing that I need is to call render option by force in code. Is there is any way to do it? Code hasn't changed a lot, but print option in render method said, that method can miss two or more onChange events and suddenly render text for one variant.
Maybe you know another autocomplete options in zk framework where database participating? I'm ready to change implementation, if there is a guide with working implementation of it.
Ok I see two problems, you should solve first.
Setting the Renderer in every call of setupQuicksearchByEvent(...).
that is not logical, cos it is the same every time.
Add to the zul combobox tag something like
itemRenderer="#load(ivm.myRenderer)" ....
If you want just 10 items, do not let the db-request return more then 10.
If you use JPA klick here or for sql here or just google a bit.
After you fixed this two issues, we can exclude these as a reason of the unexpected behavior and fix it, if it is still present.
Edit
Ok, I have two possible ways to fix it.
Call Combobox#invalidate()
This schould force zk to rerender the Combobox, but could
lead to low performance and I would not prefer this.
Use Listbox with the select mold instead of Combobox.
To force the rerender, use Listbox#renderAll()
Try setting the selected item on your combobox or throw its related event
Solution is simple. Really. Nothing is better then brute-force, but I think I tried to avoid it and use it in despair.
#Command
public void setupQuicksearchByEvent(#BindingParam("searchlayout")Event event, #BindingParam("prefix") String prefix, #BindingParam("tags") String tags) throws WrongValueException, SearchException, IOException
{
if(event instanceof InputEvent)
{
InputEvent inputEvent = (InputEvent) event;
String inputText = inputEvent.getValue();
List<String> searchFields = Arrays.asList(tags.split(","));
Map<UID, String> publishers = ZKLogic.findDocsStartingWith(prefix, searchFields, "proxy", inputText);
Combobox searchBox = (Combobox) event.getTarget();
searchBox.getChildren().clear();
for (Map.Entry<UID, String > entry : publishers.entrySet())
{
Comboitem item = new Comboitem();
item.setLabel(entry.getValue());
item.setValue(entry.getKey());
searchBox.appendChild(item);
}
}
}
In my Vaadin UI, I have the following scenario:
In other words, there are two tables:
a "master" table that is editable,
a "slave" table that is non-editable, but must contain the same data as the master table, however with a potentially different sort order.
The final requirement precludes the tables having the same Container (in my understanding, which might be wrong).
What I've done currently is that I attach an ItemSetChangeListener and a ValueChangeListener, and use the events to update the data accordingly.
Here's the current crude implementation (in Scala actually, but I hope it's readable enough, if not, please comment) :
class DataTableSynchronizer(val master: Table, val slave: Table) extends ItemSetChangeListener with ValueChangeListener {
def init():Unit = {
master.addItemSetChangeListener(this)
containerMaster.addListener(this.asInstanceOf[ValueChangeListener])
}
private def containerOf(t: Table) = t.getContainerDataSource().asInstanceOf[IndexedContainer]
private def containerMaster = containerOf(master)
private def containerSlave = containerOf(slave)
override def containerItemSetChange(event: ItemSetChangeEvent) {
//handling
//remove all items that have been deleted
for(toDel <- containerSlave.getItemIds().filterNot(containerMaster.containsId(_))) {
containerSlave.removeItem(toDel)
}
//add new items to the start
for(toAdd <- containerMaster.getItemIds().filterNot(containerSlave.containsId(_))) {
containerSlave.addItem(toAdd)
}
slave.validate();
}
override def valueChange(event: ValueChangeEvent) = {
updateValuesInResults()
}
private def updateValuesInResults(): Unit = {
//update all values in the "slave" table from the "master" table
for((itemData,itemResults) <- containerMaster.getItemIds().map(id => (containerMaster.getItem(id),containerSlave.getItem(id)))) {
for(propId <- itemData.getItemPropertyIds()) {
itemResults.getItemProperty(propId).asInstanceOf[Property[Any]].setValue(itemData.getItemProperty(propId).getValue().asInstanceOf[Any])
}
}
}
}
However, my problem is that I need the data to be synchronized continuously, as the user types, which is not happening due to the relevant events being sent only after some operation completes (a row is added, etc.).
How do I solve this, i.e. how do I enforce events being emitted often enough to enable continuous synchronization? The only idea I had was to use an ActionListener mapping all the keys, but that screams "abuse".
Note: I realize doing this through the server-side is less efficient, but this is not a concern in my case. However, client-side-based answers are OK as well, of course.
You can set the TextChangeEventMode of the editor to EAGER and process single keystrokes in the event listener:
TextField textField = ...;
textField.setTextChangeEventMode(TextChangeEventMode.EAGER);
textField.addTextChangeListener(new TextChangeListener() {
#Override
public void textChange(TextChangeEvent event) {
String text = event.getText();
// sync with other component
}
});
I have several wicket tests that target a sortable DataTable, specifically ajax-clicking the sortable column headers and asserting the contents of the rendered body rows. Now the component hierarchy of the table component's descendants is auto generated by the wicket framework, and results in paths to the sorting links (ajax) similar to:
table:topToolbars:toolbars:0:headers:1:header:orderByLink
However, when the DataTable gets re-rendered across tests, the index of the toolbars component is incremented each time i.e similar to:
table:topToolbars:toolbars:1:headers:1:header:orderByLink
which then breaks the hard-coded paths of the succeeding tests as they will no longer match.
The code fragment for the datatable construction is as follows:
final PayeesProvider dataProvider = new PayeesProvider();
table = new DataTable<ResponsePayeeDetails>("payees", columns, dataProvider, rowsPerPage);
table.setOutputMarkupId(true);
table.addTopToolbar(new AjaxFallbackHeadersToolbar(table, dataProvider) {
private static final long serialVersionUID = -3509487788284410429L;
#Override
protected WebMarkupContainer newSortableHeader(final String borderId, final String property, final ISortStateLocator locator) {
return new AjaxFallbackOrderByBorder(borderId, property, locator, getAjaxCallDecorator()) {
#Override
protected void onRender() {
System.out.printf("Path: %s\n", this.getPageRelativePath());
super.onRender();
}
private static final long serialVersionUID = -6399737639959498915L;
#Override
protected void onAjaxClick(final AjaxRequestTarget target) {
target.add(getTable(), navigator, navigatorInfoContainer);
}
#Override
protected void onSortChanged() {
super.onSortChanged();
getTable().setCurrentPage(0);
}
};
}
});
table.addBottomToolbar(new NoRecordsToolbar(table));
add(table);
To be precise, when I run my tests, the above System.out.printf statement prints:
(1st test)
Path: payees:topToolbars:toolbars:0:headers:1:header
Path: payees:topToolbars:toolbars:0:headers:2:header
(2nd test)
Path: payees:topToolbars:toolbars:2:headers:1:header
Path: payees:topToolbars:toolbars:2:headers:2:header
(3rd test)
Path: payees:topToolbars:toolbars:4:headers:1:header
Path: payees:topToolbars:toolbars:4:headers:2:header
(4th test)
Path: payees:topToolbars:toolbars:6:headers:1:header
Path: payees:topToolbars:toolbars:6:headers:2:header
Path: payees:topToolbars:toolbars:6:headers:1:header
Path: payees:topToolbars:toolbars:6:headers:2:header
Path: payees:topToolbars:toolbars:6:headers:1:header
Path: payees:topToolbars:toolbars:6:headers:2:header
(5th test)
Path: payees:topToolbars:toolbars:8:headers:1:header
Path: payees:topToolbars:toolbars:8:headers:2:header
Does anyone know how I can force the index generation to be more deterministic / repeatable. Alternatively, is there a way of wild-carding or otherwise generalising the path, so as to make them immune to these increments?
Any help will be greatly appreciated chaps!
The id's are being incremented each time because of the default DataTable ItemReuseStrategy, which generates brand new items each time the table is refreshed. You may have some luck implementing a custom ItemReuseStrategy if keeping the id's the same is a top priority.
We ran into a similar issue and have settled on a strategy aligned to your alternative question. With this, you can ask the test to inspect e.g. the third row on the last rendered page. Note: Code is in Scala.
In short, to address this issue we implemented a findNthComponentOnPage method to augment the WicketTester.
/**
* #param id Wicket id of component to find
* #param n 1-based
* #tparam T class of component to find
* #return the Nth component of class T appearing on the lastRenderedPage
*/
def findNthComponentOnPage[T <: Component : ClassTag](id: String, n: Int): T = {
tester.getLastRenderedPage.visitChildren(classTag[T].runtimeClass, new IVisitor[T, T] {
var count = 0
override def component(checkbox: T, visit: IVisit[T]): Unit = {
if (checkbox.getId == id) {
count += 1
if (count == n) {
visit.stop(checkbox)
}
}
}
})
}
Determining the path can be quite hard but shouldn't be necessary, as this would consist an integration-test and not an unit-test.
Testing the sorting of a column shouldn't depend on the surrounding table, so you could create a dummy-table in your testcase adding the header and one column (the one to be sorted) for the test. If you have the toolbar-object, you can either use it right away or re-retrieve it by issuing getComponent(toolbar.getPageRelativePath()).
But ensuring that an AjaxFallbackOrderByBorder actually works shouldn't be your concern as a user of this class. This should have been done by the creator of the class. You'd only have to test your own code (like the sorting-code in your data provider)...