his is a followup to a post about dynamic dropdown. Please read it to have the appropriate context.
After dynamically setting dropdown values, another requirement that comes up frequently is ability to do cascading dropdowns, with one dropdown being dependent on another. For example, ability to select a country, and then have another dropdown populated with cities.
This feature requires ability to pass the value of one dropdown to the renderer of another, which, as we’ll see, introduces a few issues we need to resolve. The way i implemented this is through adding ability to insert references to other property values into the query that returns dynamic results. So if in my Lucene query, I’d like to replace
TEXT:”Greece”
with
TEXT:”${my:country}”
where ${my:country} will be substituted by the value of property my:country, a query which presumably will return cities (work with me on this).
In order to do this, I am going to further enhance the constraint code. I introduced a new class called SearchBasedDependencyListConstraint, which sits between SearchBasedListConstraint and LuceneSearchBasedListConstraint. I could have added the methods to existing classes, but wanted to make the code a bit more modular, with clearer responsibilities for each class.

NOTE: the way the code is written, my constraints don’t actually check whether the submitted value conforms to the constraint. However, the UI should take care of that – further integrity checking is left as an exercise to the reader.
inside SearchBasedDependencyListConstraint, I created some methods that are able to take a string and replace tokens in that string with values from the node. My class has those methods, which I made public static for reusability, and a property that saves the Node object, so I can pull out the current property values. The key method is resolveDependenciesOnProperties. It first pulls out all the property names that need to be looked up by using regexp, then creates a map by pulling out the properties it found from the Node object, and finally replacing the tokens in the query with values.
[sourcecode language="java"]
protected String resolveDependenciesOnProperties(String query)
{
List<String> propNames = getPropertyNames(query, getTokenExpression());
Map<String, String> map = populateNodeValues(propNames, node);
String newQuery = replaceQueryParametersWithValues(query, map);
return newQuery;
}
[/sourcecode]
So far so good. However, there are a few issues that come up.
1. No Access to Node Object from Constraint
The first one is that substituting node values required access to the node object, so you can lookup properties. However, the constraints interface API does not receive a node object. This is a problem. We are going to get around it with a bit of trickery. Like I said before, the rendering of the UI happens in a TextFieldGenerator, which is a Component Generator. Because creation of the UI component happens before the component is asked for the drop down values, that means that if we can intercept that call, we can pass in the node reference to the constraint. We already have the property in the constraint, so we just need to set it before the component UI is rendered.
Now, let’s write our component generator. We will be able to reuse most of TextFieldGenerator, so we need to subclass it. It has a method
protected ListOfValuesConstraint getListOfValuesConstraint(FacesContext context, UIPropertySheet propertySheet, PropertySheetItem item)
which is responsible for returning the constraint. We will copy the original implementation, and add a few lines specific for our needs. We are only going to set the Node for the constraints that can receive it:
if (constraint instanceof LuceneSearchBasedListConstraint)
{
Node currentNode = (Node)propertySheet.getNode();
// This is a workaround for the fact that constraints do not have a reference to Node.
((LuceneSearchBasedListConstraint)constraint).setNode(currentNode);
lovConstraint = (SearchBasedListConstraint)constraint;
break;
}
Note that I am inserting currentNode, which is the unsaved version of the node. The properties of the node won’t get persisted until save is called.
The second issue to overcome is that Alfresco property sheet was not designed to have dependent properties, so we need to refresh the screen after the country drop down changes. In our component generator, we’ll change the UI box that gets rendered to have a javascript fragment that auto-submits the page. In CustomListComponentGenerator, in method createComponent, I pass in a parameter into the drop down box rendered:
protected UIComponent createComponent(FacesContext context, UIPropertySheet propertySheet, PropertySheetItem item)
{
UIComponent component = super.createComponent(context, propertySheet, item);
if (component instanceof UISelectOne && isAutoRefresh())
component.getAttributes().put(“onchange”, “submit()”);
return component;
}
2. Refreshing the Screen
This introduces another issue – when the page is refreshed, all the UI elements do NOT get re-created from scratch. To work around this one is a bit more difficult. We need to modify the behavior of the property sheet. Luckily, since we have access to the source through the SDK, we can study the out of the box property sheet implementation, class UIPropertySheet. It has a method encodeBegin which is responsible for creation of the actual UI. I overrode the implementation with my own and will reconfigure Alfresco to use mine.
In this case, I have a crude implementation, where I simply clear out all the existing JSF UI elements, forcing the code to recreate the elements from scratch (which will have a side effect of resolving all the dependencies as the UI elements are getting created). The rest is handled by default implementation in the superclass.
public void encodeBegin(FacesContext context) throws IOException
{
if (getChildren().size() != 0)
this.getChildren().clear();
super.encodeBegin(context);
}
We now need to make Alfresco pick up our new implementation (I called it RefreshableUIPropertySheet). To do this, we need to go into the WAR file, and replace PropertySheet section in faces-config-repo.xml with:
<component>
<component-type>org.alfresco.faces.PropertySheet</component-type>
<component-class>org.alfresco.sample.web.RefreshableUIPropertySheet</component-class>
</component>
Note: There may be a more extensible way to do this – doing it this way means that every time you upgrade, you have to reintroduce this change.
AutoRefresh or Not AutoRefresh?
In the code for CustomListComponentGenerator, you probably noticed a reference to isAutoRefresh() method. I needed to add this since I am reusing the same Constraint for both Country and City properties, but I only need the Country property change to trigger auto-refresh. I handled this through this parameter, and also introducing two components into faces-config-beans.xml. The first one sets autoRefresh to True, the second one, to false.
<managed-bean>
<description>
Bean that generates a custom generator component
</description>
<managed-bean-name>CustomListComponentGeneratorWithRefresh</managed-bean-name>
<managed-bean-class>org.alfresco.sample.web.CustomListComponentGenerator</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>autoRefresh</property-name>
<value>true</value>
</managed-property>
</managed-bean>
<managed-bean>
<description>
Bean that generates a custom generator component
</description>
<managed-bean-name>CustomDependentListComponentGenerator</managed-bean-name>
<managed-bean-class>org.alfresco.sample.web.CustomListComponentGenerator</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>autoRefresh</property-name>
<value>false</value>
</managed-property>
</managed-bean>
Installing It & Packaging it all up.
Building the sample should be easy.Copy the sample into the SDK samples directory, and run ant compile package-jar. Copy the jar file into WEB-INFlib, and config files into extension folder. Also follow instructions in AddToFacesConfig.txt to edit the JSF bean config files.
Unfortunately, because we need to change the bindings of some core components (i.e. PropertySheet, we can’t avoid but to go into the WAR, or editing some files. Additionally, this is not currently packaged up as an AMP file, which means the jar file needs to be manually copied into the WAR. I hope to repackage the example better in a future post.
Conclusion.
We now have some code that does quite a lot. It is able to perform a search query, substitute values in that query based on other parameters, render the UI appropriately, and refresh the UI every time there is a dropdown. Pretty cool. Another way to implement the whole project would have been through a much more AJAX-heavy compontent generator talking to web script components. Possibly in a future post
.
P.S. Sorry for the code fragments being a bit ugly. If you know of a good tool that can format Java as HTML for blogging consumption, let me know in comments – in a few searches I did I couldn’t find anything good enough, and the one Eclipse plug-in I did find stopped working for me (spent over an hour figuring it out and gave up).