Introduction
As described here, you can have a Stored Procedure returning data to a query. If you then would like to have row operations for this data, you will run into the issue that Aware does not have the BOs itself (see this post. As Jaymer pointed out in that post, you can make your own Row Operations via Javascript.
Below an example where I created a row operation "View" with a css class "ep-buttonview"
widget.bind("dataBound", onDataBound);
function onDataBound(e) {
grid.tbody.find("a.aw-link.ep-buttonview").click(onViewClick);
}
function onViewClick(e) {
let currentRow = $(this).parents("tr");
let record = grid.dataItem(currentRow);
if (record) {
AwareApp.startProcess2(
"AwareProcessName",
"BOName",
record["BO_ID"],
"main"
);
}
}
This works but it has a few drawbacks:
- If there are multiple row operations, you need to create an event handler for each row
- The event handler must refer to the correct Aware Process. If the process name is changed, the render script must be updated accordingly. This makes the code harder to maintain and may create future bugs if you would change the configuration but forget to update the render script.
Generic event handler for row operation buttons displayed separately in each row
I developed the following alternative, which is fully generic. This means that the JS code does not need to have any knowledge about how many row operations are defined and what the required AIM process is for each operation.
// Regular Expression to find the process name for data row operations. The first word after 'process:' in the itemOperand will be matched.
const processRegExp = new RegExp("(?<=process:)\\s*\\w+");
widget.bind("dataBound", onDataBound);
function onDataBound(e) {
grid.tbody.find("a.aw-link[data-operidx]").click(onDataOperationClick);
}
function onDataOperationClick(e) {
let currentRow = $(this).parents("tr");
let record = grid.dataItem(currentRow);
let itemOperIdx = Number($(this).attr("data-operidx"));
let processNameMatch =
parser.m_itemOpers[itemOperIdx].operand.match(processRegExp);
if (record && processNameMatch) {
try {
AwareApp.startProcess2(
processNameMatch[0].trim(),
"BOName",
record["BO_ID"],
"main"
);
} catch (e) {
console.log("Error calling process. Matched name: ", processNameMatch," - Error: ", e);
}
}
}
This uses the fact that the item operations are described in parser.m_itemOpers - there is an object for each row operation, containing information such as the name of the operation, the type, the css class, and of course the process to run. If you select as type "Execute Javascript", then the javascript for the row operation will also be stored in parser.m_itemOpers. This provides a convenient way to pass the name of the AIM process to the custom event handler. The applicable AIM process name is retrieved from the javascript provided with the row operation, using the RegExp "(?<=process🙂\s*\w+". This gets the first word following "process:". In the example below, I defined the process name 'AccountHolder_ViewFromJS'. If for any reason I would change the name of the process, or wanted to call another process, the only thing to do is change the name of the applicable process in the settings for the operation and the event handler automatically calls the correct process. There are no changes required to the actual event handler in the render script.

Because the regular Aware handler will also be triggered when clicking on the operation button, I put the information in a JS comment. This way, nothing happens when the Aware handler executes the Javascript. (In principle, it would be possible to attach an event handler to a lower DOM object and then prevent the event triggering at the higher level (no event bubbling), but that is more complicated and error-prone.)
Note that the custom javascript defined for the row operation can also contain other information or actual JS code (i.e. not commented out). This will be ignored by the event handler, but will of course be run by Aware if not commented out. For example, you could add additional clarification comments or even pass additional parameters to the event handler in the render script.
Each row operation button has an attribute "data-operidx", which is the index to the parser.m_itemOpers array. So by getting that index, the requested row operation can be found.
You need to ensure that each row in the grid contains the ID of the applicable BO that you need to pass to the AIM process. But the ID can be in a hidden column, so it isn't visible in the grid but can be easily accessed through the dataItem method for the grid.
Of course, the event handler must know what type of AIM object must be passed to the AIM process. But usually any query, although generated through the Stored Procedure, will be related to one existing AIM object so it is clear that this must be put into context for the row operation process.
Generic event handler for row operation buttons combined behind a single menu button

The above works fine for row operations that are specified as separate buttons on the row. It does not work if the row operations are included in one menu button, i.e. when the option "Create menu button for operations" is ticked (see screenshot above.). The reason is that this creates separate menus that are put in a different place in the DOM, not in the table rows. If row operations are combined behind a menu button, the code below can be used as a generic event handler:
const processRegExp = new RegExp("(?<=process:)\\s*\\w+");
widget.bind("dataBound", onDataBound);
const menuButtons = {
menuIDs: [], // holds unique IDs for the row menus
operations: {}, // hashtable for the row operations (operation name, AIM process name)
listItems: "", // selector for the listitems in the menu, to make sure that only menubuttons that have an operation class are included
// Store operation names and corresponding process names in a hashtable
// Create a selector for the css classes of the menu buttons
// NOTE: each menu button must have a css class, but the class can be identical for all menu buttons
initialise: function () {
parser.m_itemOpers.forEach((operation) => {
let processNameMatch = operation.operand.match(processRegExp);
if (processNameMatch) {
menuButtons.operations[operation.operName] = processNameMatch[0].trim();
if (operation.cssClass && !(menuButtons.listItems.includes(operation.cssClass))) {
menuButtons.listItems += ", li." + operation.cssClass;
}
}
});
if (menuButtons.listItems.length > 2) {
menuButtons.listItems = menuButtons.listItems.substring(2); // Remove first ", "
}
// find all popup menus (one for each row) and store their IDs in an array
if (this.listItems) {
$("body")
.find('div.k-menu-popup[data-role="buttonmenu"] ul.k-menu-group')
.has(this.listItems)
.each(function () {
menuButtons.menuIDs.push($(this).attr("id"));
$(this).click(onDataOperationMenuClick);
});
}
},
};
function onDataBound(e) {
menuButtons.initialise();
}
function onDataOperationMenuClick(e) {
let rowIndex = menuButtons.menuIDs.indexOf($(this).attr("id"));
if (rowIndex >= 0) {
let currentRow = grid.tbody.find("tr").eq(rowIndex);
let record = grid.dataItem(currentRow);
let menuTextElement = $(e.target)
.closest("li")
.find("span.k-menu-link-text");
let operName = menuTextElement ? menuTextElement.text() : null;
if (record && operName && menuButtons.operations[operName]) {
try {
AwareApp.startProcess2(
menuButtons.operations[operName],
"BOName",
record["BO_ID"],
"main"
);
} catch (e) {
console.log("Error calling process. Matched name: ", processNameMatch," - Error: ", e);
}
}
}
}
This is slightly more complex than the first generic event handler, caused by the different structure of the menu buttons, but it works well.
The event handler also takes into account the possibility that for some rows, operations may be excluded as a result of the "Applicable" condition. If in the settings for the row operation an "Applicable" condition is set and for any particular row that condition is not fulfilled, the menu button for that operation will not be included in the generated HTML. This means that the index of the menu button cannot be used to find the relevant row operation. Instead, the event handler creates a hashtable of all row operations, with a key equal to the operation name and value equal to the process defined in the script settings. Note that this works only when the operation name is actually displayed on the button, otherwise it is not included in the HTML. But I think that you would always want to include the operation name on the button if the row operations are behind a separate row menu button. If only an image (icon) and no text would be displayed, it may make more sense to put those icons in the row instead of a separate menu.
Furthermore, note that you need to provide a custom css class for every row operation. This is used by the initialisation code in the script to ensure that the event handler is only bound to the custom menus for the row operations and not to any other menus that may be on the screen (for example top bar menu or left panel menu). But all row operations can have the same css class, they do not have to be different for each operation. If none of the row operation buttons for a particular record have a css class defined, that menu will not be included in the event handler and the menus for the other rows will not work correctly.
A few caveats
For the menu button version:
Apply custom css class to each row operation
Make sure that 'Display operation name on the button' is ticked
Operation names must be unique for the same grid
Last but not least:
Normally, if you change the name of a process then all references to that process are automatically updated by Aware. This does not apply to the reference in the javascript for the row operation (the line "// process: process name"), you will need to change that manually. And unfortunately that reference will not show up when you do a Search Version (Alt-V) for the name. So if you change the name of the process and forget where the reference(s) to that process are, you may have a hard time finding them. It's best to flag/document that the process should not be renamed without care, for example (as in the screen shot above) by adding JS to the process name to alert you that this is called from javascript. What I have done now is put all processes that are called directly from Javascript in a separate category (which becomes a folder under Processes) and added in the description of the process the the name of the query and the row operation that calls the process, to make it easier to find and amend the reference.
Summary
With this code you can create generic event handlers in the render script for queries displaying data output from a Stored Procedure. This enables row operations to be defined from the configurator. The regular AIM process receives the required BO as input, notwithstanding that the records in the query are not persisted in the database. You can even combine both code examples in the same render script, which has the effect that the script can deal with row operations regardless whether they are specified as separate buttons on the row or combined behind a single menu button.