Friday, April 04, 2008

Eureka....

I wanted to share a little DEAPI related discovery. If I wait to type it up the 'right' way I may not ever get it done, so here it is. I have been doing a lot of work with the DEAPI inside of a report.

If you have not worked with the DEAPI inside of a report, I should remind you that using the DEAPI is only valid within the OnPrepare method of the reportItems or from the BeforeFactory method of the ReportDesign.

When you work with the DEAPI you need to be aware of three basic types of objects in the report engine. Event handlers are passed objects that are part of the script api package. Script objects have limited functionality. For instance, an ICell script object can tell how many cells the column is spanning cell.getColumnsSpan(), but does not allow you to set the column span.

The type of object you want to work with is the DesignElementHandle, which for a cell is CellHandle. DesignElementHandles are defined in the Model API and give you access to all of the functionality in the model. Once you have the DesignElementHandle, you can change the report design in almost any manner.

In between the DesignElementHandles and the script objects are the DesignElements. The DesignElements objects represent the actual instance of the object in the report. DesignElement objects are not exposed by the report engine, and are not particularly useful except for one key role, they provide a link between the script objects and the DesignElementHandles.

If a report were a house: the DesignElementHandles are the blue prints, the DesignElements are the house itself and the script objects are pictures of the house. If you modify the blue print (DesignElementHandle) early in the build process, you can completely change the way the report is built. To stretch the metaphor you are looking at a picture of someone else house (script object) and using that to change the blue prints (DesignElementHandle) for the house that you are building.

Clear as mud? Good...

I have always navigated to the DesignElementHandle by using the name value of the script object.

   DesignElementHandle designHandle = rptContext.getReportRunnable().getDesignHandle();
ModuleHandle moduleHandle = designHandle.getModuleHandle();
TableHandle tableHandle = (TableHandle) moduleHandle.findElement(inTable.getName());
//modify the table handle as I want



Some script objects (cells, rows, columns) do not support the name value, in that case I can use the getParent() method to navigate to a container which supports the name value. Typically I end up back at the table.

From the tableHandle you can then navigate to the appropriate, row, column, group within the report. This works okay, but it can be tricky. In some cases, if you are looking to modify one particular cell it is down right hard. What I wanted to be able to do is get the CellHandle directly from the scriptable object that that is passed to the event handler. You end up having to write a bunch of code to get to navigate back to the object where you started.

It turns out that the ScriptObject has a protected field named designElementImpl which is a handle to the DesignElement object. The DesignElement object has a field "handle" which is the DesignElementHandle. The problem is that neither of the fields are exposed through the API.

Don't you hate it when you know something is there but you just can't figure out how to access it?

I finally got tired of writing all kinds of code to navigate back to an object. It turns out you can use reflection to get access to the fields that are there, but not exposed through the API.

I have created a simple generic method that will get the associated DesignElementHandle from the script object. This is fairly fresh, and has not been fully wrung out for all issues, but it is a decent place to start.

/**
* Modify the columnSpan of a cell item
*/
public void onPrepare(ICell cell, IReportContext reportContext) {

Integer unitSpan = reportContext.getGlobalVariable("cell_span");

try {
// call generic method to get DesignElementHandle
DesignElementHandle cellHdl = getDesignElementFromScript(cell);
if (cellHdl instanceof CellHandle) {
((CellHandle) cellHdl).setColumnSpan(unitSpan);
} catch (Exception e) {
// trap errors
}

/**
* Generic method to access DesignElementHandle from a scriptable object
*
* @param scriptObject
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public DesignElementHandle getDesignElementFromScript(IDesignElement scriptObj)
throws NoSuchFieldException, IllegalAccessException {

// The designElementImpl field is a protected field
// that is declared on the super class
Field fieldFromScript = scriptObj.getClass().getSuperclass()
.getDeclaredField("designElementImpl");
if (fieldFromScript == null){
return null;
}

// Access the DesignElement field from the Script object
fieldFromScript.setAccessible(true);
Object designElement = fieldFromScript.get(scriptObj);

// The DesignElement object has a field named "handle"
// which is the DesignElementHandle for that DesignElement
// Use reflection again to get this field
Field fieldFromSimpleApi = designElement.getClass().getSuperclass()
.getDeclaredField("handle");
if (fieldFromSimpleApi == null){
return null;
}

// Now access the DesignElementHandle from the DesignElement
fieldFromSimpleApi.setAccessible(true);
return (DesignElementHandle)fieldFromSimpleApi.get(designElement);
}

No comments: