> We've had a series of problems relating to persistence of component fields > when components are reused in the page and called as part of an Ajax > request.
Is this problem in jira? I did a quick search but didn't see it. On Thu, Jul 15, 2010 at 12:28 PM, Chuck Kring <cjkr...@pacbell.net> wrote: > Howard, > > I have another use case where this might be very useful. > > Our application consists of several dashboard pages that are used like a > network management console works: the pages automatically refresh as things > change, users can click on icons to drill down, etc. The page relies > heavily on Ajax calls to update icons and charts and we very often use the > same component multiple times in a page. This is a medical application and > we have icons to represent every patient state - that component is reused > once for every patient displayed. > > We tend to have two types of Ajax calls: zones and images. Images are > handled similar to zones but use our own Javascript that sets the img src > field once the image has been loaded into the browser cache. > We've had a series of problems relating to persistence of component fields > when components are reused in the page and called as part of an Ajax > request. We fixed this by having components register a context with the > calling page, then we set the component context prior to generating the JSON > response. I'm sure there is a better way to do this (perhaps a Tapestry > Service) but this is workable for now. Once again, the specific issue is > that when multiple copies of a component are used on a page and the > components are returned as a response to an XHR request, persisted fields > are not handled correctly across all similar components. > > I think another benefit of your proposal is it could make state management > in components more easily controlled by the application. For example, I > could forgo the above context mechanism if it were possible to retrieve a > handle to the FieldValueCondiut and to set that before re-rendering the > component. > > Regards, > > Chuck Kring > > Howard wrote: >> >> Tapestry applications are inherently stateful: during and between >> requests, information in Tapestry components, value stored in fields, >> stick around. This is a great thing: it lets you program a web >> application in a sensible way, using stateful objects full of mutable >> properties and methods to operate on those properties. >> It also has its downside: Tapestry has to maintain a pool of page >> instances. And in Tapestry, page instances are big: a tree of hundreds >> or perhaps thousands of interrelated objects: the tree of Tapestry >> structural objects that forms the basic page structure, the component >> and mixin objects hanging off that tree, the binding objects that >> connect parameters of components to properties of their containing >> component, the template objects that represents elements and content >> from component templates, and many, many more that most Tapestry >> developers are kept unawares of. >> This has proven to be a problem with biggest and busiest sites >> constructed using Tapestry. Keeping a pool of those objects, checking >> them in and out, and discarded them when no longer needed is draining >> needed resources, especially heap space. >> So that seems like an irreconcilable problem eh? Removing mutable state >> from pages and components would turn Tapestry into something else >> entirely. On the other hand, allowing mutable state means that >> applications, especially big complex applications with many pages, >> become memory hogs. >> I suppose one approach would be to simply create a page instance for >> the duration of a request, and discard it at the end. However, page >> construction in Tapestry is very complicated and although some effort >> was expended in Tapestry 5.1 to reduce the cost of page construction, >> it is still present. Additionally, Tapestry is full of small >> optimizations that improve performance ... assuming a page is reused >> over time. Throwing away pages is a non-starter. >> So we're back to square one ... we can't eliminate mutable state, but >> (for large applications) we can't live with it either. >> Tapestry has already been down this route: the way persistent fields >> are handled gives the illusion that the page is kept around between >> requests. You might think that Tapestry serializes the page and stores >> the whole thing in the session. In reality, Tapestry is shuffling just >> the individual persistent field values in to and out of the HttpSessio. >> To both the end user and the Tapestry developer, it feels like the >> entire page is live between requests, but it's a bit of a shell game, >> providing an equivalent page instance that has the same values in its >> fields. >> What's going on in trunk right now is extrapolating that concept from >> persistent fields to all mutable fields. Every access to every mutable >> field in a Tapestry page is converted, as part of the class >> transformation process, into an access against a per-thread Map of keys >> and values. The end result is that a single page instance can be used >> across threads without any synchronization issues and without any >> conflicts. Each thread has its own per-thread Map. >> This idea was suggested in years past, but the APIs to accomplish it >> (as well as the necessary meta-programming savvy) just wasn't >> available. However, as a side effect of rewriting and simplifying the >> class transformation APIs in 5.2, it became very reasonable to do this. >> Let's take an important example: handling typical, mutable fields. This >> is the responsibility of the UnclaimedFieldWorker class, part of >> Tapestry component class transformation pipeline. UnclaimedFieldWorker >> finds fields that have not be "claimed" by some other part of the >> pipeline and converts them to read and write their values to the >> per-thread Map. A claimed field may store an injected service, asset or >> component, or be a component parameter. >> public class UnclaimedFieldWorker implements >> ComponentClassTransformWorker { private final PerthreadManager >> perThreadManager; private final ComponentClassCache classCache; static >> class UnclaimedFieldConduit implements FieldValueConduit { private >> final InternalComponentResources resources; private final >> PerThreadValue<Object> fieldValue; // Set prior to the >> containingPageDidLoad lifecycle event private Object fieldDefaultValue; >> private UnclaimedFieldConduit(InternalComponentResources resources, >> PerThreadValue<Object> fieldValue, Object fieldDefaultValue) { >> this.resources = resources; this.fieldValue = fieldValue; >> this.fieldDefaultValue = fieldDefaultValue; } public Object get() { >> return fieldValue.exists() ? fieldValue.get() : fieldDefaultValue; } >> public void set(Object newValue) { fieldValue.set(newValue); // This >> catches the case where the instance initializer method sets a value for >> the field. // That value is captured and used when no specific value >> has been stored. if (!resources.isLoaded()) fieldDefaultValue = >> newValue; } } public UnclaimedFieldWorker(ComponentClassCache >> classCache, PerthreadManager perThreadManager) { this.classCache = >> classCache; this.perThreadManager = perThreadManager; } public void >> transform(ClassTransformation transformation, MutableComponentModel >> model) { for (TransformField field : >> transformation.matchUnclaimedFields()) { transformField(field); } } >> private void transformField(TransformField field) { int modifiers = >> field.getModifiers(); if (Modifier.isFinal(modifiers) || >> Modifier.isStatic(modifiers)) return; >> ComponentValueProvider<FieldValueConduit> provider = >> createFieldValueConduitProvider(field); >> field.replaceAccess(provider); } private >> ComponentValueProvider<FieldValueConduit> >> createFieldValueConduitProvider(TransformField field) { final String >> fieldName = field.getName(); final String fieldType = field.getType(); >> return new ComponentValueProvider<FieldValueConduit>() { public >> FieldValueConduit get(ComponentResources resources) { Object >> fieldDefaultValue = classCache.defaultValueForType(fieldType); String >> key = String.format("UnclaimedFieldWorker:%s/%s", >> resources.getCompleteId(), fieldName); return new >> UnclaimedFieldConduit((InternalComponentResources) resources, >> perThreadManager.createValue(key), fieldDefaultValue); } }; } } >> >> That seems like a lot, but lets break it down bit by bit. >> public void transform(ClassTransformation transformation, >> MutableComponentModel model) { for (TransformField field : >> transformation.matchUnclaimedFields()) { transformField(field); } } >> private void transformField(TransformField field) { int modifiers = >> field.getModifiers(); if (Modifier.isFinal(modifiers) || >> Modifier.isStatic(modifiers)) return; >> ComponentValueProvider<FieldValueConduit> provider = >> createFieldValueConduitProvider(field); field.replaceAccess(provider); } >> The transform() method is the lone method for this class, as defined by >> ComponentClassTransformWorker. It uses a method on the >> ClassTransformation to locate all the unclaimed fields. TransformField >> is the representation of a field of a component class during the >> transformation process. As we'll see it is very easy to intercept >> access to the field. >> Some of those fields are final or static and are just ignored. A >> ComponentValueProvider is a callback object: when the component >> (whatever it is) is first instantiated, the provider will be invoked >> and the return value stored into a new field. A FieldValueConduit is an >> object that takes over responsibility for access to a TransformField: >> internally, all read and write access to the field is passed through >> the conduit object. >> So, what we're saying is: when the component is first created, use the >> callback to create a conduit, and change any read or write access to >> the field to pass through the created conduit. If a component is >> instantiated multiple times (either in different pages, or within the >> same page) each instance of the component will end up with a specific >> FieldValueConduit. >> Fine so far; it comes down to what's inside the >> createFieldValueConduitProvider() method: >> private ComponentValueProvider<FieldValueConduit> >> createFieldValueConduitProvider(TransformField field) { final String >> fieldName = field.getName(); final String fieldType = field.getType(); >> return new ComponentValueProvider<FieldValueConduit>() { public >> FieldValueConduit get(ComponentResources resources) { Object >> fieldDefaultValue = classCache.defaultValueForType(fieldType); String >> key = String.format("UnclaimedFieldWorker:%s/%s", >> resources.getCompleteId(), fieldName); return new >> UnclaimedFieldConduit((InternalComponentResources) resources, >> perThreadManager.createValue(key), fieldDefaultValue); } }; } >> >> Here we capture the name of the field and its type (expressed as >> String). Inside the get() method we determine the initial default value >> for the field: typically just null, but may be 0 (for a primitive >> numeric field) or false (for a primitive boolean field). >> Next we build a unique key used to store and retrieve the field's value >> inside the per-thread Map. The key includes the complete id of the >> component and the name of the field: thus two different component >> instances, in the same page or across different pages, will have their >> own unique key. >> We use the PerthreadManager service to create a PerThreadValue for the >> field. >> Lastly, we create the conduit object. Let's look at the conduit in more >> detail: >> static class UnclaimedFieldConduit implements FieldValueConduit { >> private final InternalComponentResources resources; private final >> PerThreadValue<Object> fieldValue; // Set prior to the >> containingPageDidLoad lifecycle event private Object fieldDefaultValue; >> private UnclaimedFieldConduit(InternalComponentResources resources, >> PerThreadValue<Object> fieldValue, Object fieldDefaultValue) { >> this.resources = resources; this.fieldValue = fieldValue; >> this.fieldDefaultValue = fieldDefaultValue; } >> >> We use the special InternalComponentResources interface because we'll >> need to know if the page is loading, or in normal operation (that's >> coming up). We capture our initial guess at a default value for the >> field (remember: null, false or 0) but that may change. >> public Object get() { return fieldValue.exists() ? fieldValue.get() : >> fieldDefaultValue; } >> >> Whenever code inside the component reads the field, this method will be >> invoked. It checks to see if a value has been stored into the >> PerThreadValue object this request; if so the stored value is returned, >> otherwise the field default value is returned. >> Notice the distinction here between null and no value at all. Just >> because the field is set to null doesn't mean we should switch over the >> the default value (assuming the default is not null). >> The last hurdle is updates to the field: >> public void set(Object newValue) { fieldValue.set(newValue); // This >> catches the case where the instance initializer method sets a value for >> the field. // That value is captured and used when no specific value >> has been stored. if (!resources.isLoaded()) fieldDefaultValue = >> newValue; } >> >> The basic logic is just to stuff the new value into the PerThreadValue. >> However, there's one special case: a field initialization (whether it's >> in the component's constructor, or where the field is defined) turns >> into a call to set(). We can differentiate because that update occurs >> before the page is marked as fully loaded, rather than in normal use of >> the page. >> And that's it! Now, to be honest, this is more detail than a typical >> Tapestry developer ever needs to know. However, it's a good >> demonstration of how Tapestry's class transformation APIs make Java >> code fluid; capable of being changed dynamically (under carefully >> controlled circumstances). >> Back to pooling: how is this going to affect performance? That's an >> open question, and putting together a performance testing environment >> is another task at the top of my list. My suspicion is that the new >> overhead will not make a visible difference for small applications >> (dozens of pages, reasonable number of concurrent users) ... but for >> high end sites (hundreds of pages, large numbre of concurrent users) >> the avoidance of pooling and page construction will make a big >> difference! >> >> -- >> Posted By Howard to Tapestry Central at 7/14/2010 04:30:00 PM >> > > --------------------------------------------------------------------- > To unsubscribe, e-mail: users-unsubscr...@tapestry.apache.org > For additional commands, e-mail: users-h...@tapestry.apache.org > > -- -- http://www.bodylabgym.com - a private, by appointment only, one-on-one health and fitness facility. -- http://www.ectransition.com - Quality Electronic Cigarettes at a reasonable price! -- TheDailyTube.com. Sign up and get the best new videos on the internet delivered fresh to your inbox. --------------------------------------------------------------------- To unsubscribe, e-mail: users-unsubscr...@tapestry.apache.org For additional commands, e-mail: users-h...@tapestry.apache.org