2009/5/25 Andy Schwartz <andy.schwartz(a)oracle.com>
Gang -
I spent the last couple of hours testing out various composite
component/f:ajax/id-related use cases, using the ajax-switchlist demo
(switchlistAjaxComponent.xhtml) as my base test case. The ajax-switchlist
component hierarchy looks like this (simplified down to significant parts):
<h:body>
<h:form id="form1">
<ez:switchlist id="switchlist"/>
<h:commandButton value="reload" type="submit"/>
</h:form>
</h:body>
And <ez:switchlist> composite component contains the following (again,
stripped down to interesting parts):
<cc:implementation>
<h:selectManyListbox id="list1"/>
<h:commandButton id="move1to2">
<f:ajax execute="@this #{cc.clientId}:list1"
render="#{cc.clientId}:list1 #{cc.clientId}:list2"/>
</h:commandButton>
<h:selectManyListbox id="list2"/>
</cc:implementation>
First thing to note... The execute/render ids are specified in an
overcomplicated manner. As I mentioned in email earlier today, the f:ajax
execute/render ids are spec'ed to adhere to findComponent() id resolution
semantics. Since the <f:ajax> behavior is attached to a component that is
in the same naming container as selectManyListbox components that we are
targeting, we can simplify from this:
<f:ajax execute="@this #{cc.clientId}:list1"
render="#{cc.clientId}:list1 #{cc.clientId}:list2"/>
To:
<f:ajax execute="@this list1" render="list1 list2"/>
Ah, that feels much better. Actually, this isn't just a simplification...
this is correct solution. Unfortunately the original code only happens to
work due to non-spec compliant implementation behavior. That is, when we
specify:
render="#{cc.clientId}:list1"
This is the same as specifying:
render="form1:switchlist:list1"
Which should have the same behavior as calling:
move1to2ButtonComponent.findComponent("form1:switchlist:list1");
This will result in the search expression "form1:switchlist:list1" being
resolved relative to the nearest naming container, which in this case is the
switchlist composite component itself. Since there is no
"form1:switchlist:list1" component within the composite component naming
container, findComponent() would return null, and the <f:ajax> tag should
log a warning.
However, instead of failing/logging a warning, we've got some code in
AjaxBehaviorRenderer.findComponent() that does the following:
UIComponent resolvedComponent = component.findComponent(expr);
if (resolvedComponent == null) {
// not found using a relative search, try an absolute search
resolvedComponent = component.findComponent(':' + expr);
}
The bonus absolute search is not part of the spec - ie. is not typical
findComponent() id resolution behavior. This code should be removed and
replaced by a warning. (We should be able to make the warning clearer by
identifying the naming container that provided the scope of the search.
In any case, when switching over to:
<f:ajax execute="@this list1" render="list1 list2"/>
Everything works as expected. We definitely should update this demo to use
this simpler, more spec-compliant solution.
In order to try to reproduce the problem that David was seeing, I decided
to see whether I could update the "reload" button on the main page from
within the composite component. So, first I gave the reload button an id:
<h:commandButton value="reload" type="submit"
id="reload"/>
And then I added a new render target inside of our <f:ajax> tag (inside of
the composite component):
<f:ajax execute="@this list1" render="list1 list2
:#{cc.parent.clientId}:reload"/>
Note that I used the leading ":" character to indicate that this is an
absolute id.
I was thinking that "#{cc.parent}" would resolve to the composite
component's immediate parent, which in this case happens to be the "form1"
component. As such, I figured that "#{cc.parent.clientId}" would give me
"form1", and ":#{cc.parent.clientId}:reload" would produce the
absolute id
":form1:reload", which should target the outer reload button (leaving aside
the question of whether this is a good thing to do or not).
However, when I ran the test case, I hit the following error:
<f:ajax> contains an unknown id '::reload'
Okay, that's weird. So this means that "#{cc.parent.clientId}" is not
resolving to the parent component's client id, but instead to null/empty
string. Huh.
After poking around some more, I found my way to the
com.sun.faces.el.CompositeComponentAttributesELResolver, which provides some
special handling for certain properties on the "cc" object, including the
"parent" property:
if (COMPOSITE_COMPONENT_PARENT_NAME.equals(propertyName)) {
UIComponent c = (UIComponent) base;
context.setPropertyResolved(true);
UIComponent ccParent =
UIComponent.getCompositeComponentParent(c);
return ccParent;
}
And, of course, UIComponent.getCompositeComponentParent() does not return
the composite component's parent, but instead returns the nearest ancestor
composite component. Which, in this particular demo, is... null. Yuck.
Okay, in retrospect, rather than re-defining the meaning of "parent" here,
we should have introduced a new derived property, eg. "compositeParent", or
something along those lines. (Unfortunately the current behavior is defined
by the spec - section 5.6.2.2, so too late to do anything about this for
2.0.)
I thought I might be able to work around this by avoiding the reference to
the "cc" implicit object - ie. thought I might be able to get this to work
by using the "component" implicit object instead of the "cc" object,
eg:
":#{component.parent.parent.clientId}:reload". But I ended up running into
the same problem.
So, David - it is possible that you are running into this as well.
I believe so.
It's kind of tough to tell with the auto-generated ids in your
content.
Any chance you could add explicit ids to the naming containers in your
sample just to make it easier to understand the client ids that are being
rendered?
Okay, sure.
On a positive note, I was able to manually pass in the reload button
id to
the composite component without any problems. To do this, I added the
following to the composite:interface:
<cc:attribute name="reloadId"/>
And the following to the composite:implementation:
<f:ajax execute="@this list1" render="list1 list2
#{cc.attrs.reloadId}"/>
And then updated the main page to specify the new attribute:
<ez:switchlist id="switchlist"
reloadId=":form1:reload"/>
This worked without any problems.
David - since I think we all agree that this is the preferred approach, and
since there doesn't seem to be any way to get at a composite component's
actual parent (unless I am missing something), this seems like the right way
to go for your J1 demo. If you continue to have problems with this
approach, maybe cc me when you send Ryan your war. Don't know how much time
I'll have to take a look, but I am definitely curious.
I agree that it's the preferred approach, however, interestingly enough, I
cannot get it to work. I think I'm doing essentially the same thing that
you are doing above. Hmmmm.
I already sent Ryan a war, but I've added explicit ids to all my containers,
so I will send him an updated war and cc you.
Thanks for all your help, I really appreciate it.
david
Andy
David Geary wrote On 5/25/2009 1:36 PM ET:
2009/5/24 Jim Driscoll <Jim.Driscoll(a)sun.com>
On 5/24/09 8:39 PM, David Geary wrote:
And that id is *exactly* what <f:ajax> says it cannot find. It's in the
page, but <f:ajax> cannot find it?!?
My guess is that <f:ajax> is evaluating the id before the page is
completely constructed, and therefore, it doesn't find it, but that's
just a WAG on my part.
So, if this is not correct:
<f:ajax render="#{cc.parent.clientId}:image"/>
Then how do I access the image in the parent (map) component?
It seems to me that I'm using <f:ajax> correctly, but I'd be happy to be
told otherwise.
It's late, but yes, that looks correct, and yes, your guess sounds
correct. Could you file this as an impl bug?
Okay. I'll send a WAR to Ryan, and see if we can get this fixed asap. I'd
like to get this working for my J1 demo.
Now, we do need to fix that as a bug, but I must argue that your use case
represents bad practice. Your component is using ajax inside a component to
modify the using page. That's difficult to maintain, and I suspect it will
lead to obfuscated code quite easily. I'd instead argue that if you are
going to have one component modify another, you should either group them
into one composite, or, have the ID of the component you act upon passed in
as an attribute (say "for="?)
Yup, I concur--too much coupling between parent and child components.
Unfortunately, I can't seem to pass the component ID to the child component
either. IOW, this doesn't work:
In parent component:
<places:mapZoomControl componentToRender="#{cc.clientId}:image"/>
And in mapZoomControl.xhtml:
<composite:attribute name="componentToRender"/>
...
<f:ajax render="#{cc.attrs.componentToRender}"/>
I suppose it's not surprising that the preceeding doesn't work--it looks
like a manifestation of the same bug. I'll give Ryan a buzz.
Thanks,
david
Of course, that second usage suggestion opens up another hole in the cc api
- the lack of ability to do name resolution. Though if/when we get method
parameters, then cc.findComponent will work, I guess. Though I've been
saying that for a pretty long time now, I should check on when we're getting
those method params...
Jim