PDA

View Full Version : Complex Model with Internal ChangeEventListeners Serialized over RPC



pjaromin
4 Jun 2009, 2:12 PM
I've got an app with a fairly complex model, with deeply-nested bean properties. The "top level" bean listens for and publishes changes to it's children (adding itself as a listener on the children as each child is added). My UI registers itself to listen to this top level bean for ANY model changes and then handles appropriately.

This was working just fine when the "dummy" model was created for testing on the client-side. I've now moved the model "build" process to the server and am using RPC to fetch it. The model beans are specific to this app and I was therefore free to implement my beans as extensions of BaseModel, to take advantage of the built-in property change support.

I've just realized, however, that ChangeEventSupport in BaseModel is marked transient. I can guess why this would be, but now am trying to determine the best way to proceed...

I see a few options:

1) Write a method that "rewires" the parent-child listeners once they arrive on the client side.

2) "Fish out" the nested beans and add the listeners more directly in the relevant UI components. (though I have a clean interface setup that makes it currently somewhat automatic for the appropriate panels to recieve events...this would muddy that up a bit).

2) Break down the build process into multiple smaller RPC calls and "build" the model on the client side, which will wire in my nested change support. (I don't currently see an easy way to do this, but I'm sure I could work something out)

3) Write my own change support (as I've done with older GWT apps) that isn't transient and watch to ensure this doesn't massively bloat the data transfer.

Is there are better way to do this?

I'd love to hear how others have solved this problem.

Thanks!

- Patrick

Colin Alworth
5 Jun 2009, 12:07 PM
Maybe I am not fully understanding, but why are you wiring up the change event code on the server? The field is marked transient so that code written on the client doesn't get kicked up to the server.

If you want the server to 'listen' to data changes, then you need RPC calls to be made from the client (i.e. the client hooks up listeners, and when it hears an event go off, fires a RPC call).

Or are you suggesting that your object graph is so complex that you dont want to go in and wire up change events? In that case, can't your constructor do it for you (parameter-less constructors are run as part of the RPC process I think)?

pjaromin
5 Jun 2009, 1:50 PM
So here's what I've got. Let's say that I'm modeling a car, so I've got the following object graph:

[CAR] --> 1..4 [TIRE]

If a simple property of the car -- say "color" or even the list of tires changes, then any object registered as as listener to "car" would be notified. However, what if my UI component cares about properties of the tire, say the "pressure"? Changing the "pressure" property of Tire doesn't cause Car to notify it's listeners.

To solve this, the Car object might have the following methods:

public void add(Tire tire) {
List<Tire> tires = getTires();
tires.add(tire);
notifyPropertyChanged("tires", tire, tires);
tire.addChangeListener(this);
}

public void modelChanged(ChangeEvent event) {
// Pass it up the chain
event.setParent(this);
notify(event);
}

Now, when the UI component receives the model (setModel(Car car)), it registers itself as a change listener on the Car object.Now, anytime a property of Tire changes, it notifies Car which in turn "announces" the change to any interested components.

Otherwise, the UI component would need to "fish out" the children it cared about and register itself as a listener to ALL of them, as well as on the parent model so it could also be notified if the list of tires changes. This gets rather ugly rather fast...and difficult to maintain.

The other problem is that the model is somewhat large and deep and requires parsing a couple large XML documents. This is a task best left to a server process -- and since whole large chunks of the model are represented by a single XML document, it only makes sense to create it all at one time, serialize it as a whole and send it to the client. But in this process, the parent is no longer registered as a listener to its children.

I'm more accustomed to doing this sort of thing in Swing GUIs and therefore wasn't considering this serialization step when I wrote the "listener wiring" code...so now I'm considering how to best modify this code to work around the transient listener support.

Does this explanation make things a bit clearer? I'd love to be shown a better way...how would you handle this problem?

Thanks!

- Patrick

Colin Alworth
5 Jun 2009, 4:02 PM
Tell me if I have this right: You are not actually interested in serializing the full set of listeners, but instead you only want when an object is created/deserialized, that it get listeners to its children. That is, every object listens to every even the child makes.

(Side note: This also is requiring that your object graph always be without cycles, otherwise you will get events firing in circles.)

If you are using BaseModelData methods ONLY to read and write underlying data (i.e. no additional non-static, non-final, non-transient fields in the object), then it seems like you should be able to build a custom field serializer that would wire up events as it finds them.

Forgive me if this is delving too deeply into the realm of gwt-magic, but this would be my approach (Note please that I have never done this before).

Create a common superclass for all objects that will want to pass events from child to parent. (Make a warning in big letters for other developers to not use this in cases with cycles.) The name here isn't great... but note that it has no fields, and only serves to act as a common point of ancestry.

public abstract class ModelWithEvents extends BaseModel implements ChangeListener {
public void modelChanged(ChangeEvent event) {
event.setParent(this);
notify(event);
//consider restoring parent for later events to get called?
}
}Build a serializer for this abstract class. This should be called by the automatically generated serializers that TypeSerializerCreator makes for ProxyCreator for the purposes of RPC calls.

public ModelWithEvents_FieldSerializer {
public static void deserialize(SerializationStreamReader streamReader, ModelWithEvents instance) throws SerializationException {
//Build base object somehow, either referencing BaseModel_FieldSerializer (a generated class)
//or just do the work of BaseModelData_FieldSerializer here
for (Object obj : getProperties().values()) {
if (obj instanceof ModelWithEvents) {
((ModelWithEvents)obj).addChangeListener(instance);
} else if (obj instanceof List) {
//etc - probably refactor the above out to do all this work recursively
}//etc for map, set if so inclined
}
}
}I think this grants what you were originally looking for - instead of persisting and passing the full set of event notifications, it will only build the events that maintain the set of notification you seem to be interested in.

On the other hand, this sounds like overkill... you say "the UI component would need to "fish out" the children it cared about and register itself as a listener to ALL of them, as well as on the parent model so it could also be notified if the list of tires changes". This sounds like only one element is interested in all of the sub events that occur. If this is indeed the case, you might consider the same sort of recursion I started in the CustomFieldSerializer, but in the code that binds the model to the UI. Then, loop through and bind all subobjects to the UI object. If it is many many (great big tree) models to one UI object, then this would be faster, and not actually all that hairy - and you would wire up the same number of listener->model connections. In fact, it would be faster, as deeply nested object dont need to notify every ancestor, just the UI itself.

If you want a tree of UI objects to watch a tree of models, okay, then you need a full tree of events mirroring the tree of data. Which brings about another, simplier suggestion. (If you havent noticed, I like to overcomplicate.) Since we are talking about acyclical graphs here, you could just follow the BaseTreeModel and track your parents as well as your children. Then your base class looks something like

public abstract class ModelWithEvents extends BaseModel implements ChangeListener {
protected List<ModelWithEvents> parents;
public void setParent(ModelWithEvents... parents) {
this.parents = Arrays.asList(parents);
}
public void modelChanged(ChangeEvent event) {
event.setParent(this);
notify(event);
//consider restoring parent for later events to get called?
}
public void notify(ChangeEvent evt) {
super.notify(ChangeEvent evt);
for (ModelWithEvents parent : parents) {
parent.modelChanged(evt);
}
}
}I'm still trying to understand the bit about using the full object from XML, but at the very least you can be safe knowing that if it is XML, it is very likely to be a tree of objects, so cycles are not possible.

As said, I havent really addressed this kind of issue yet, and so I dont know what approach I would take, but theres 20 minutes of my mind for you - hope it helps! And please share any issues/successes you run into...

pjaromin
6 Jun 2009, 7:14 AM
Colin-

Thank you so much for the in-depth and well-considered reply. The initial part of your reply gets it right -- and you highlight what looks to me to be a rather clean way of accomplishing what I was considering doing in my #1 option from the OP. I've never worked with custom serializers (I must admit I didn't realize you could do this in GWT), so unless there's something here that would prevent this from working in my case I'm thinking this is the solution I'll use. It doesn't look like overkill to me...in fact, it looks rather elegant...much moreso that writing a method in the model called "rewireEvents" that would race through all the children of the model wiring things back up.

To answer your other questions...there are MANY UI components interested in this. Imagine this were a desktop publishing application (it _sorta_ is a very specialized one). What we're actually talking about here is the "open document" command from the UI grabing the "document", which contains many layers of children (text frames, images, nested frames, etc.) and then populating numerous UI Grids/Forms/Trees of properties for various components. EACH of these components is concerned with specific properties/children's properties of the document...a few of them cut across multiple children.

To have each of these UI components register individually with each child of the document that it cares about would be very messy and unmanageable.

The XML discussion was just to highlight that it would also be ugly to build the model on the client and "lazy load" the various children (wiring the listeners as added) with a number of RPC calls...since it would be TONS more work than simply digesting the couple base XML documents on the server in one step. In fact that part's been written already for a desktop version of the app and works nicely in the servlet environment.

Thanks so much for your thoughtful and helpful replies. I'll update here anything I learn about the details of implementing this strategy.

Thanks!

- Patrick

Colin Alworth
6 Jun 2009, 3:54 PM
A good way to learn more about the FieldSerializer classes is to take a look at the generated classes that GWT builds as part of the compilation process. Additionally there are other field serializers provided in GWT and GXT, following the patter *_CustomFieldSerializer. GXT provides one to hold the RpcMap object, and GWT has a bunch for collections and primitives.

And as far as treating the giant object as a document, that is not too far off from what we are doing, but we assume that the UI handles the events that it creates. After all, data comes from the server using RPC (so no events there), something has to generate the UI around those objects (usually following the same basic tree structure), and when any object changes, it is the UI that is doing the changing, so it should inform its parents about changes it has made (like size changes, which almost every data change will cause). Using model objects to handle data events makes sense on the surface, but having already finished solving a problem very similar to this, I would strongly suggest you look into having another object track changes, if in fact you really need that much power over each and every change event.

Overused, I know, but the pattern of Model (i.e. ModelData objects + RPC), View (UI Objects) and controller (Events and Event handlers) might make sense here to keep your server from needing to know anything about client events, and to keep your UI code from being too tightly bound to exactly what data events constitute dirty data.

pjaromin
8 Jun 2009, 5:30 AM
Thanks for the tip on the FieldSerializer classes...

As for the MVC comment...I'm thinking something's not clear in my description of my issue. The app is very much MVC. The ONLY part of the model that acts as an event listener is the ROOT object...and it's there as a convenience...listening for changes to it's OWN children's data SOLELY to pass those on up to any of its own registered listeners. The model never "handles" data changes. Just percolates the events upward.

In this way, the Controller can simply say...


void setModel(Model model) {
this.model = model;
model.addPropertyListener(this);
}instead of...


void setModel(Model model) {
model.addPropertyListener(this)
for (Child child : model.childrenType1()) {
child.addPropertyListener(this);
for (Child childL2 : child.children()) {
childL2.addPropertyListener(this):
}
...
}
for (Child child2 : model.childrenType2()) {
...
}
}

So the controller only has to listen for model changes in the root object. The root object really just acts as a facade for the events of its children to greatly simplify the coding.

Anyway, thanks for the helpful info.

- Patrick

pjaromin
8 Jun 2009, 10:59 AM
After playing around without much success with the custom serializer, I decided to simply "bite the bullet", yank the "parent-child" model listeners and instead have the controller register itself as a listener to the root and all necessary children of the model object directly.

Due to the added complexity of adding the listeners, I've also made the controller more prominent as a true broker of events between view and model, instead of allowing the view to listen to the model directly.

A bit more code, but ultimately this should prove more flexible in the long run.

Colin Alworth
8 Jun 2009, 11:03 AM
Assuming that your tree has non-trivial depth, this approach will result in less events going off, as they dont need to travel up the chain to get to the UI/Controller... Events are just function calls, but making that stack smaller probably will be to your benefit.

Glad you have a solution you seem to be happy with, though I would be curious as to some of the issues you had with the custom field serializers, if you have time to share.

pjaromin
8 Jun 2009, 11:16 AM
I think you're probably correct on that.

As for the custom serializer stuff, I was able to verify that the abstract base class deserialize method was being called as expected. But the first issue I ran into was that I did have some cases of cyclical relationships, with the child referring back to the parent. Removing those wasn't going to be pretty, and that pretty much negated my ability to use the clean recursive method outlined in your previous post. I figured I could more easily wire 'em up manually...which I then did.

But then noticed that the BaseModel_FieldSerializer that was generated was pretty much empty and included a native method call that was also nearly empty. So the "do the work of BaseModelData_FieldSerializer here" part meant that I'd really need to spend some quality time with the docs trying to determine if/how I needed to deal with these, including the native method.

I decided that it would be far easier, and result in more accessible code, to promote my controller to a full event broker and cut-n-paste the event code I'd just written there.

Now that I'm aware of the custom serialization stuff, I've got it on my "investigate" list for future review. Just in case it comes in handy some day!

Thanks, again!