PDA

View Full Version : Gxt3 - store.getModifiedRecords() not actually returning modified records



elirov
28 Apr 2012, 2:46 PM
It looks like the behavior of store.getModifiedRecords() changed between gxt2 and gxt3 in the following way:

in gxt2, getModifiedRecords() returned a list of actual modified Record wrappers with Models that had changes already applied

in gxt3, it returns a list of Record wrappers that had modifications but where the wrapped models do not show the actual modifications.

So, we can't see the modified objects until we actually commit. That means that we need to commitChanges() first, THEN try to send the modified objects to the server to save them. However, that means that if the save fails, then we have unsaved changes in the grid reflected as committed changes.

Here's the code. It doesn't actually save any changes on the first save() because store.getModifiedRecords() gives back the set of Models that were edited, but it gives them without the actual changes applied.

Maybe we can get a store.getModifiedModels<T>() function that will return the actual models with their modifications.

Collection<Store<RadarChartMeasure>.Record> modifiedRecords = store.getModifiedRecords();
for (Record r : modifiedRecords) {
RadarChartMeasure k = (RadarChartMeasure) r.getModel();
data.remove(k); // data is a hashset that stores the data that we want to save in the server
data.add(k);
}
final AutoProgressMessageBox waitBox = new AutoProgressMessageBox("Saving Data...");
waitBox.show();
Report.getService().saveProviderOpsStatData(request, new EGWTCallback<Void>() {
@Override
public void doOnFailure(Throwable e) {
waitBox.hide();
GWT.log("Could not save provider ops stats config", e);
new MessageBox("Error", "Could not save provider ops stats config").show();
}


@Override
public void onSuccess(Void result) {
store.commitChanges(); //would be nice to commit AFTER the save was successful
waitBox.hide();
}
});

jtaekema
31 Jul 2012, 4:04 PM
Where X is your data type:


Collection<X> modified = new ArrayList<X>();
for (Store<X>.Record record : store.getModifiedRecords()) {
X model = record.getModel();
for (Change<X, ?> change : record.getChanges()) {
change.modify(model);
}
modified.add(model);
}
// now you can submit your modified models, and store.commitChanges() on success

elirov
1 Aug 2012, 3:41 AM
Nice one. It would be great if there was a built-in method for this, though. Sencha?

Colin Alworth
1 Aug 2012, 6:57 AM
This is indeed different, but for good reason. In GXT 2, all model objects implemented the ModelData interface, which described exactly how they must store their data. In GXT 3, we allow for any POJO to be a model, and so remove many of the constraints that ModelData added, including:

* Model setters need not exist - as long as a ValueProvider is defined that can make the changes, GXT can handle it.
* If there is a setter, it can have side effects other than just setting a field - events could be fired, dirty flags can be set, whatever makes sense for the use case
* Properties can be read-only, or write-only, and the ability to revert a value after it has been set relies on having the ability to read the original value

Combining the first and third bullet point can be very helpful for RequestFactory - data can be just readonly getters, and a RequestContext can be used to apply the changes. But since RequestContexts can't have service calls removed once they are added, it is important not to add them until it is certain that these are the values to send to the server.

By lifting these constraints, GXT 3 is more flexible, and is certain to work with all kinds of persistence tools and model objects. The solution jtaekema offers is essentially the same as actually committing the change, except it doesnt get rid of the record instance.

Think of a Record not as a store the now-stale values, but as an intermediate step before actually modifying the models - changes are queued up on the Record, and only when you are ready to commit and clear the dirty flags do they actually pass through to the model. You can read these new values out without actually changing the model as in jtaekema's example, something like this:



for (Store<X>.Record record : store.getModifiedRecords()) {
X model = record.getModel();
for (Change<X, ?> change : record.getChanges()) {
Object newValue = change.getValue();
// do something with both model and newValue
}
}

elirov
17 Dec 2012, 10:17 AM
Would it make sense to add this method to the ListStore class?

public ArrayList<M> getEditedModels() {
Collection<com.sencha.gxt.data.shared.Store<M>.Record> modifiedRecs = getModifiedRecords();
ArrayList<M> modifiedModels = new ArrayList<M>();
for (Record r : modifiedRecs) {
r.commit(false);
modifiedModels.add(r.getModel());
}
return modifiedModels;
}

This would return the modified models without actually committing the changes. This way, the save method could:
1. getEditedModels() -> and then save them to the server
2. onSuccess() -> listStore.commit() to hide the red tags

Note, I'm using ArrayList on purpose instead of List in order to make it easier to pass the object properly through GWT's RPC mechanism without causing additional code bloat. I think the most common use-case for this method would be to pass data back to the server, so I think it makes sense in this case to use the concrete class.

Colin Alworth
19 Dec 2012, 8:33 AM
Actually, your code does commit the changes - r.commit(false) performs all changes, but just doesnt fire any events. This would not update the UI right away, but if the user were to filter or sort the items locally (among other things), the UI would need to completely repaint items, which would result in reading that there are no more changes locally.

The easiest way I see to provide such a feature would be to first clone the original objects, then apply all Change objects in the Record to the clone, not to the original (for each Change c : Record.getChanges, c.modify(clone) more or less). Then, send the clones to the server instead of the originals. Of course, without full reflection on the client, or Store simply not functioning without all model objects implementing Clonable, this won't work. That said, you can still build this feature into a store subclass, or into another part of your application.

elirov
19 Dec 2012, 8:57 AM
Oh. Duh.. There must be a way to get this done. I can't believe such a common use-case is so hard to do...

Colin Alworth
19 Dec 2012, 9:18 AM
Does my description with cloning not fit your case? This would be a minimal change in your model to allow reflection.

Another idea, this would reduce the data sent over the wire:
For each model, send its ID and all changes made. Each change consists of the property changed, and the value it was changed to (assumed to be serializable). The ID can be read from the model as you iterate through Records, the property name and value from the PropertyChange object (PropertyChange.getChangeTag returns a ValueProvider which has a getPath method to get the property - if you don't have a custom implementation of Change, this cast will always work). This would very nearly get you some of RequestFactory's sparse update features, usable from within RPC.

We have to be somewhat careful when building 'supported features' like this so that they work with RPC, RequestFactory, and any other JSON/XML or other way to talk to the server - not every application out there uses the same mechanism, and we aim to be flexible enough to support any or all.

Another idea: Actually perform the commit() operation, but before doing so, record the 'old' values by reading out the Change objects. This could let you revert to those old values if the server fails for some reason.

elirov
20 Dec 2012, 7:43 AM
It might work, but it seems like a bit of a hack. Maybe there's a way for the store to do the commit/save old values strategy?

Andreas Samjeske
2 Oct 2013, 7:56 AM
Where X is your data type:


Collection<X> modified = new ArrayList<X>();
for (Store<X>.Record record : store.getModifiedRecords()) {
X model = record.getModel();
for (Change<X, ?> change : record.getChanges()) {
change.modify(model);
}
modified.add(model);
}
// now you can submit your modified models, and store.commitChanges() on success


Did anyone ever tried to run this code? Over here I get the following error on store.commitChanges():

java.lang.AssertionError: Current value was somehow stored in a record's change set!
at com.sencha.gxt.data.shared.Store$Record.commit(Store.java:186)
at com.sencha.gxt.data.shared.Store.commitChanges(Store.java:579)
...

Seems like u shouldn't call
change.modify(model); on the actual model gotten of records.
Fix: Make a copy of model and modify that one.

jregen
16 Mar 2014, 5:54 PM
Oh. Duh.. There must be a way to get this done. I can't believe such a common use-case is so hard to do...

Welcome to Sencha.