Thursday, December 25, 2008

Disabling buttons based on table selection

The easiest way to do this is to simply put an EL expression in the disabled attribute of the button that checks the size of the table's selectedRowKeys. You would also need to set a partialTrigger on the button with the ID of the table. The only problem with this is that it requires a trip to the server to evaluate the expression. Our application is going to be used by customers that are going to have low bandwidth and therefore need as few trips to the server as possible. So, we had to come up with a way to disable our toolbar buttons on the client (i.e. through javascript). We knew that a panelCollection did this very thing, so we just needed to tap into the panelCollection's functionality in order to duplicate this behavior.

We also had a need to arrange the buttons on the toolbar differently than what the panelCollection allows for. So, using a template (to replace all of our current implementations of panelCollections) we overhauled the panelCollection to suit our needs.

We tapped into the AdfDhtmlPanelCollectionPeer and found two important methods, updateStandardMenuToolbarItems and updateRowSelectionContext. The first is called when a column is selected, and the second is called when a row is selected.

Our template basically hid the original toolbar and added one on top that we could customize as much as we wanted, one that gets updated by chaining our own custom methods onto the two mentioned above. I am sure there is a proper design pattern named for what we're doing; it's something that is done with the onload function in many apps. Anyway, here is the code that we used to get our client code called every time a row or column is selected on a table that is within our template (which of course includes a panelCollection):

/* Allows the 'extra' toolbar to function as if it is part of the
* panelcollection. */
function initGridScript() {
if (typeof AdfDhtmlPanelCollectionPeer.prototype.updateStandardMenuToolbarItemsORIG != 'function') {
AdfDhtmlPanelCollectionPeer.prototype.updateStandardMenuToolbarItemsORIG = AdfDhtmlPanelCollectionPeer.prototype.updateStandardMenuToolbarItems;

AdfDhtmlPanelCollectionPeer.prototype.updateStandardMenuToolbarItemsCustom = updateMenuButtons;

//Update the 'extra' toolbar's buttons when the normal toolbar is updated
AdfDhtmlPanelCollectionPeer.prototype.updateStandardMenuToolbarItems = function () {
this.updateStandardMenuToolbarItemsORIG();

if(typeof this.getDomElement().className != 'undefined'){
if (this.getDomElement().className.search('CustomGridWithToolbar') > -1) {
this.updateStandardMenuToolbarItemsCustom();
}
}
}

AdfDhtmlPanelCollectionPeer.prototype.updateRowSelectionContextORIG = AdfDhtmlPanelCollectionPeer.prototype.updateRowSelectionContext;

AdfDhtmlPanelCollectionPeer.prototype.updateRowSelectionContextCustom = updateTableButtons;

AdfDhtmlPanelCollectionPeer.prototype.updateRowSelectionContext = function () {
this.updateRowSelectionContextORIG();

if(typeof this.getDomElement().className != 'undefined'){
if (this.getDomElement().className.search('CustomGridWithToolbar') > -1) {
this.updateRowSelectionContextCustom();
}
}
}
}
}


The AdfDhtmlPanelCollectionPeer.prototype.updateStandardMenuToolbarItemsORIG = AdfDhtmlPanelCollectionPeer.prototype.updateStandardMenuToolbarItems; saves off the original functionality so that we can invoke it later.

The typeof AdfDhtmlPanelCollectionPeer.prototype.updateStandardMenuToolbarItemsORIG != 'function' ensures that we are not appending our own functions more than once.

The this.updateStandardMenuToolbarItemsORIG(); ensures the original functionality is invoked.

The check for 'CustomGridWithToolbar' ensures that we are only calling our custom code when the panelCollection is the one from our template.

This function, initGridScript(), is called at the bottom of the javascript file on which it is declared, and the javascript file itself included as part of the template. So, it gets called whenever the template is written out to the response.

The updateStandardMenuToolbarItemsCustom method disables/enables menu items based column selection. In updateRowSelectionContextCustom, the code checks the RichTable client component's getSelectedRowKeys() to disable and enable the buttons in the toolbar accordingly.

Thursday, December 11, 2008

How do I tell if they clicked a row in the table versus its body?

In our app, we have a dblClick clientListener as a direct child of a table. If the user clicks a row, we want to show them the edit popup for that row, if they click in white space (i.e. the are beneath the rows), we want to create a new row and show the popup to edit that new row. The source for either click will be the table itself, however, the target will be what we are interested in. To ensure that our code remains cross-browser compatible, we'll use methods made available by client-side ADF.

From the ADF js file, I have plucked the following:

var agent = AdfAgent.AGENT, domEvent = evt.getNativeEvent();
var target = agent.getEventTarget(domEvent);
var tablePeer = table.getPeer();
var attrs = tablePeer.GetRowKeyAndRow(target,tablePeer.getDomElement());


If attrs is null, then the user clicked within the table body and not on a row. If attrs is not null then the user clicked on an actual row.

Monday, December 1, 2008

Where'd my frickin' log window go?

I lost my log window in JDev, and no matter how many times I hit 'View > Log', it never appears; found the answer to resurrecting it on Duncan Mills' site.

  1. Shutdown JDev

  2. Delete windowinglayout.xml. This file is in the o.ide folder under the jdeveloper system folder, system11.1.1.0.31.51.56.

  3. Restart JDev


This file will get rebuilt with the layout defaults, which includes the log window, or any other window you may have misplaced, when JDeveloper restarts.

Monday, November 24, 2008

Determining and Verifying Relative Partial Trigger Ids

Getting a component to refresh based upon the actions or events generated by other components requires that the partial target (the component you want to be refreshed) declare the id (via a relative or absolute path) of the trigger component of which to listen for a partial submit. Sometimes, determining this path can be a guesstimate at best, especially if your page contains included fragments, regions, or templates. Generally, if your target and trigger are within the same jsp fragment or jsp page, you can use the 'Edit Property' dialog on the partialTrigger attribute. For those tricky cases, read on ...

The method which I've discovered works best for me is to use Firebug and ADF's client side 'findComponent' function.

Step 1: Be sure to have Firebug installed. I currently am using Firefox 2.0.0.16 with Firebug 1.0.5, though this should work with any version of Firefox\Firebug.

Step 2: Determine the client id, including the full naming container path, of your target. This can be done by opening the HTML tab of the Firebug console and doing a search for the component's declared id. If you have a component which is several naming containers deep, this can be a lot easier than tracing the path through the jsp pages. We'll call this the target id from here on.

page:tmplt:rgn:panCol:tableA

Step 3: Determine the id, including the full naming container path, of your trigger. Use the same method as in Step 2.

page:tmplt:buttonA

Step 4: Paste these two ids into a text file and determine their last point of commonality. In the example below, the target and trigger share 'page:tmplt' as their base path. This means that this part of the path can be ignored, because these components will be relative to each other at this point.

page:tmplt:rgnA:panCol:tableA [target]

page:tmplt:rgnB:buttonA [trigger]

Step 5: Determine what the relative path of the trigger from the target should be. This can be tricky, but at this point all you really need is a good guess that we can test in the next step. For this example we can see that the trigger is up two naming containers and then down one.

So, for the first naming container we climb up from the target, we begin our relative path with two colons (if we were to put only one, we would be signaling that our path is absolute, and should start from the page root. No colons would be signaling to search for other components that are under the same naming container path as the target).

::
gets us to -> page:tmplt:rgnA
(Keep in mind that findComponent really starts from the current component's parent naming container if it's not a naming container itself OR itself, if it is a naming container. In this case the target is not a naming container, so the search begins from 'panCol', the target's parent naming container.)

To get above the next naming container, we add another :

:::
gets us to -> page:tmplt

Now we are at our point of commonality, and we need to begin to traverse down the naming container path into a different branch. Appending the down stream naming container path gets us to the level of our trigger.

:::rgnB
gets us to -> page:tmplt:rgnB

Now we just need to append the id of the trigger component. My use of id here refers to the id attribute that is declared on the component in the jsp. The client id of the component includes the full naming container path, as we saw when searching for the component in Step 1.

:::rgnB:buttonA
gets us to -> page:tmplt:rgnB:buttonA

Step 6: Now we use the ADF javascript method, findComponent, to get a handle to the target component and verify that we have calculated the the relative client id accurately. Open up the console tab and pass in the target id as the only parameter to the findComponent function.

AdfPage.PAGE.findComponent('page:tmplt:rgnA:panCol:tableA');

This should return an object; in my local example this returns an AdfRichTable object. Now, from this object we will use the same function and pass in the relative path we determined in Step 4.

AdfPage.PAGE.findComponent('page:tmplt:rgnA:panCol:tableA).findComponent(':::rgnB:buttonA');

If this function returns our trigger, then we know we have the correct relative path, :::rgnB:buttonA, to assign as the value for the partialTrigger attribute on our target. If not, we can test other paths rather quickly by plugging in different trigger ids. Most of my errors are either too few or too many leading colons, and I can easily test those by using this function.

Sunday, November 16, 2008

Sticky Popup Values

Most of the problems that I will post about come from the project on which I am currently working. This post will be about one that has plagued us since the early days of our project, one which we thought we had fixed on more than one occasion; popups holding on to stale data.

Let's say a user clicks a button to edit values in a given table, in our app we use a popup to display a form in which the user can update the data. The user can make changes and click 'Ok' to save or click 'Cancel' to dismiss the changes; both actions dismiss the popup. Our problem was that if the user used 'Cancel', which has its immediate attribute set to 'true' (so that the validation does not occur), the next time they request for the popup to be displayed, it will display the values that were in the popup which we dismissed. This means that if we were editing row A in the table the first time, then cancelled, then decided to edit row B, the values from row A would appear. And every subsequent showing and canceling of the dialog would display the values from row A.

The issue turns out to be that since we skip the validation phase of ADF lifecycle, the values never get submitted out of the JSF model into the ADF model. The values therefore remain as values waiting to be submitted and are subsequently never excused from the fields in the popup. These values then replace whatever data is being displayed from the current row. So, if we were to click 'Ok' in the popup, they would then replace the values of the current row, and the next time the popup would be shown, it would actually be displaying the values from the current row as we expect (now that there are no stale values, because they have finally been submitted).

The trick we used to work around this issue is to basically clear out these un-submitted values from fields in the popup. We do this by iterating over all of the editable components that are in the dialog within the popup and using resetValue(). We generally pass in the dialog as the rootComponent.

public void resetStaleValues(UIComponent rootComponent){
Iterator iter = rootComponent.getFacetsAndChildren();
while (iter.hasNext()) {
UIComponent component = iter.next();
if (component instanceof UIXEditableValue) {
UIXEditableValue uiField = (UIXEditableValue)component;
uiField.resetValue();
}
resetStaleValues(component);
}

Tuesday, November 4, 2008

En cuál manera debemos codificar?

Code is my life. Ok, not really, it just provides an engaging, meaningful career, and keeps my mind churning during the day, and occasionally while I sleep. Does anyone else dream in code? When I do, I seem to run through the same lines of code over and over.

Oh well, that is not what this blog is about. I would like to share pieces to some of the many coding puzzles on which I work. Currently, I am learning how to use the latest release of Oracle's JDeveloper, 11G, and like many new (to me) technologies, there is a steep learning curve. I am hoping to provide some of the tidbits that I pick up along the way with others who are new to ADF, and hopefully provide some solutions that may prove useful.

We'll see how well I keep this up. Oh, and as for the title ... I, of course, wanted something witty, but just went with a phrase similar to that of the one on my t-shirt instead: "En cuál manera debemos vivir?," which translates to "How ought we to live?." So, how ought we to code? Well, it's a difficult balance between quality and quantity. We don't always have enough time to pump out the best code, but as we all know, poor coding can cost time later. Anyways, I will claim that my posts will not necessarily be THE answer to the problems at hand, but they just might save you some time. And I would encourage anyone perusing my so called solutions to share any alternate/better solutions, so that we can all become better coders, and not just code monkeys.