the reason panel data sources can't be accessed outside the panel
Marky Roden recently called attention to a lesson he learned: if you associate a data source with a specific panel, you can only refer to it by name via events (or attributes) attached to components inside that panel. There are two very specific reasons for this:
- Hierarchical component processing
- The role the
requestScopeplays in variable resolution
While there are no doubt some subtle exceptions to this premise, you can consider all runtime processing of XPages to be hierarchical. To conceptualize this visually, imagine the view root (basically, the page itself) as the trunk of a tree. Think of each of the components that are placed directly on the page, rather than inside other components, as branches attached directly to the trunk of the tree. Each of those branches might, in turn, split off into several smaller branches.
Let's consider a simple real-world example:
Imagine an XPage that contains two 2x2 tables. In both tables, each row consists of two cells: one containing a label, and the second containing an editable field.
The server-side structure of an XPage at runtime is sometimes referred to as its "component tree": in this example, the XPage itself is the trunk of the tree. As we look up along the trunk, at some point the trunk splits into two branches: each branch is a table. This branch, in turn, splits into two separate branches, each of which is a table row. Each of these also splits into two branches, each of which is a table cell. The first of these two branches contains a single "leaf"; the leaf on the first branch is a label component. The leaf on the second branch is an input text.
So, if this were a real tree, and we wanted to reach any of those leaves (and, assuming these branches are too high for us to just reach up and touch a leaf), we would climb the trunk until we can reach the tables. We could then make our way along the branches - first one of the tables, then one of its rows, then one of its cells, until finally we're at a leaf. We've found our field. If we retreat back to the inner branches, and then repeat the process for all the rest, we would eventually touch each leaf.
To very briefly change the subject by introducing a less obvious analogy: imagine that some of these leaves (namely, those representing fields) are sentient, and your friend Steve can communicate telepathically them, so he constantly knows what they're thinking. But you can only communicate with them by touch. So… suppose that a leaf named Alan recently learned something you need to know. You decide to climb the tree and work your way along enough branches to eventually find Alan. This could take a while, because not only do most of these trees have a lot of branches, but you don't actually know what branch Alan is on. To find Alan, you have to follow each sub-hierarchy of branches, touching each leaf to ask it its name (technically, its ID).
Eventually, you'll ask a leaf, "are you Alan?", and it'll say yes. Now that you've found Alan, you can ask it everything it knows.
Meanwhile, Steve knew the whole time what Alan was thinking. He's standing safe on the ground, laughing at you scurrying from branch to branch and trying not to fall.
If any of my previous descriptions of why we should always call
dataSource.getValue("propertyName")-- instead of
getComponent("componentId").getValue()-- didn't make sense, perhaps this illustrates the premise a bit more clearly.
getComponent()crawls along all the branches until it eventually finds Alan.
dataSourceis Steve. Just stay on the ground and ask Steve.
Okay, back to the topic at hand.
Part of why it's not a problem for the core processing of an XPage to traverse this hierarchical structure is that each of the various JSF lifecycle phases has to touch all the components anyway (with the exception, during some phases, of components that aren't rendered, and the descendants thereof), and each only needs to be touched once. So each phase starts at the view root (the
<xp:view/>tag) and works its way inward until it's processed all of the components that it needs to a single time.
Some of the phases split the processing into "begin" and "end" operations for each component. For example, the bulk of the work of the
RENDER_RESPONSEphase (the very last phase, which is what actually sends the HTML markup to the browser during a typical XPage request) consists of executing the following three methods on every rendered component in the tree:
encodeBegin- For most components, the primary purpose of this method is to emit the opening HTML tag, along with all the currently applicable attributes.
encodeChildren- Once the HTML tag for the current component has been sent, this method basically loops through all the components directly within the current component and passes control to them to "encode" themselves.
encodeEnd- For many components, this just closes the HTML tag.
Because these methods execute in sequence, the result is hierarchical.
Let's consider an even simpler table than before: one row, one cell in that row, and one button in that cell:
- table runs
encodeBegin, emitting an opening table tag with all pertinent attributes (client-side ID, CSS class, et cetera)
encodeChildrenpasses control off to the table row
encodeBeginof the row opens a tr tag
encodeChildrenpasses control to the cell
encodeBeginopens a td
encodeChildrenpasses control to the button
encodeBeginopens a button tag
encodeChildrendoes nothing, because this button contains no child components
encodeEndof the button closes the button tag
encodeEndof the cell closes the td
encodeEndof the row closes the tr
encodeEndof the table closes the table
In reality, there's a bit more going on in there, like making sure there's a tbody surrounding any tr's, etc., but essentially the above is what occurs when this type of component hierarchy is rendered. When any components have multiple children, it gets more complex, except it's still really just these three methods making sure that a parent component gets to start sending its markup before any of its children are processed, that all children have been completely processed before it finishes sending its markup, and finally that it has an opportunity to finish processing itself before any sibling components are touched.
And that last premise is what causes the behavior Marky experienced. Panels are one of a few components that are considered "data containers". As such, when the above process starts for a panel, it doesn't just open a div tag... it also checks to see if any data sources (and/or data contexts) are associated with just that panel. If so, it goes through the motions of ensuring the data source exists, which consists of something like the following:
- Based on the scope (r/v/s/a) and name, does this already exist?
- If not, create one and store it in the correct scope
- Add a pointer in the
It's that last step that's crucial: adding anything to the
requestScopemakes it a variable.
Within any EL expression (whether standard Expression Language, SSJS, or some custom language), any symbols determined during expression parsing to be variables end up asking what's known as the "variable resolver" for the object to which that variable refers. There are several places the standard variable resolver looks, and one of them is the
requestScope. So any
requestScopekey is a valid variable for as long as that key exists within the
requestScope… which is usually the entire request.
Panels, however, don't just add variables, they clean up after themselves.* Specifically, at the end of a panel's processing, it again checks to see if it has its own data. If so, it removes the
requestScopepointer it created during its initial processing. If the data source has a higher scope (the default is
viewScope), the data source itself still exists within that scope. But there's no longer a pointer to it in the
requestScope, so there's no longer a variable by which it can be directly accessed. On any subsequent requests, when the same panel is processed again, it will find the existing data source instance in its correct scope, create a fresh
requestScopepointer, and then remove that pointer again when it finishes its processing.
That might seem a little pointless, since it's typically milli- (or even micro-) seconds between the creation and obliteration of the variable. But because all processing is hierarchical, this means that during all processing of components anywhere inside that panel, the
requestScopepointer does exist. Any components in that sub-structure can refer to that variable. But once the panel and everything within it have been processed, that variable is gone. The more complex an XPage, the more valuable this automatic cleanup, because it optimizes resolution of all variables. So it's a good practice to always be looking for ways to structure our user interfaces in ways that allow us to localize our data as much as possible. This not only maximizes app performance, but also makes the application source more logically coherent by compartmentalizing its visual structure according to the conceptual structure of its data.
* P.S. If you considered the above and immediately guessed that Custom Controls and repeats behave similarly, you get a gold star.