« [Tip] Function "getAsPlainText" explained | Main | [Showcase] myCalendar module »

January 18, 2005

[Challenge] Week 1/10/05: Code challenge (answer)

by David Workman

The quiz last week is an example of how to wrap functionality that is used on many forms -- many times -- into one function. In one solution alone, this cuts out well over a 100 methods for me. As a result, I consider this approach to coding fairly essential.

The code in question does something fairly simple: it sorts columns in list view when you click on a header label. It then displays a directional arrow to give the user feedback on what column is sorted and which direction it is sorted in:

List1

One-off coding

"One-off" coding is the term I use to describe how I would code something if it was only going to be used once. This is by far the most direct method. It also becomes a pain in the ass when you find yourself duplicating the same code all over your solution later on. Heaven forbid that you then change the underlying principle to this one piece of code at a later date! Been there? We all have.

Let's start at this point to show you what is going on. To make this work, I need three objects: the header label (which triggers the routine with the onAction event), the ascending sort arrow, and the descending sort arrow. When you click the header, the following needs to happen:

  • check to see if the ascending arrow is visible
  • if yes, sort the column descending, turn off the ascending arrow and turn on the descending arrow
  • if no, sort the column ascending, turn of the descending arrow and turn on the ascending arrow

Note that this assumes that you have a method that turns off both sort arrows when the form shows up.

If the column data provider (the field) is "name_first" and the arrow objects are called "name_first_asc" and "name_last_asc" respectively, here would be the code to do this:

//sort name_first column

if (elements.name_first_asc.visible)
{
    controller.sort('name_first desc');

    elements.name_first_asc.visible = false;
    elements.name_first_desc.visible = true;
}

else

{
    controller.sort('name_first asc');

    elements.name_first_asc.visible = true;
    elements.name_first_desc.visible = false;
}

If you continue to go about things this way, to sort on another column you would duplicate this method and apply to a new set of sort arrows for the new column. Nothing fancy but you can quickly see how much work you are duplicating. If you have ten columns to sort, that is a lot of methods doing the same thing.

What is much worse (and not obvious until you start testing the second column sort method) is that each method will need to turn off the sort arrows to all the other columns. Each method above then becomes something like this:

//sort multiple columns

if (elements.name_first_asc.visible)
{
    controller.sort('name_first desc');

    elements.name_first_asc.visible = false;    
    elements.name_first_desc.visible = true;
    
    elements.name_last_asc.visible = false;
    elements.name_last_desc.visible = false;
    
    elements.another_column_asc.visible = false;
    elements.another_column_desc.visible = false;
    
    (etc....)
}

else

{
    controller.sort('name_first asc');

    elements.name_first_asc.visible = true;
    elements.name_first_desc.visible = false;
    
    elements.name_last_asc.visible = false;
    elements.name_last_desc.visible = false;
    
    elements.another_column_asc.visible = false;
    elements.another_column_desc.visible = false;
    
    (etc....)
}

Ouch. You have now officially programmed yourself into a corner just to get multiple columns to sort:

List2

Abstract programming

Simply put, abstract programming is the process of coding for reusable functionality and then passing to the code the data for the code to operate on. In our "one-off" example above, the data takes the form of hard-coded object references (example: "elements.name_asc.visible"). If you use this code anywhere else, you will need to edit the code to change all the object reference names.

There are four concepts in Servoy that you will need to understand in order to abstract your code.

CONCEPT 1: Functions

First is the concept of a "function." I'm using the word "function" here to denote a particular Servoy method that has a specific purpose and is called by another Servoy method to accomplish its purpose. Additionally, the calling method can pass "parameters" (data) to the function and get resulting data back. A function call is initiated by simply applying the name of the function in a Servoy method and passing any parameters between the parens:

//this function is a global Servoy method named "fx_SORT_columns"
globals.fx_SORT_columns(sortImages, formName, columnNum);

Notice that I am passing several variables of data to the function but not returning any data from the function. To return data, the function call would look like this:

var x = globals.fx_SORT_columns(sortImages, formName, columnNum);

The last line in the function would also need to be:

return x; //where x is a variable with any kind of data in it.

A function itself captures data passed to it via the "arguments[x]" array:

var sortImages = arguments[0];
var formName = arguments[1];
var columnNum = arguments[2];

CONCEPT 2: Object referencing

The second important concept is understanding how to reference objects. In our example, we are using two graphical objects to display which direction a column is sorted. I have given each object a name: "name_first_asc" and "name_first_desc." They are both "element" objects and belong to the "form" object (with specific properties and functions that can be set by a method) so an object would be referenced by the name of "elements.name_first_asc."

However, this is not quite the full name! If a form name is not given, the element is assumed to be on the current form. You have to add the explicit form name to reference an object on another form bringing our full name to "forms.form_name.elements.name_first_asc."

There is a pattern here that resembles a tree with branches and leaves. To reference the third leaf on the tenth branch of the elm tree in my backyard it is simply: "tree.myElmTree.branch.branch10.leaf.leaf3". The pattern is [object][object name][obect][object name][etc]. Note that there is enforced "scoping" to this pattern. You can't reference the leaf by getting things out of order: "leaf.leaf3.tree.myElmTree.branch.branch10".

To set the color property of this leaf it would be: "tree.myElmTree.branch.branch10.leaf.leaf3.color = yellow".

Now for the cool part. You can use variables for the object names. To set the color property of any leaf on my tree depending on variable values it would look like this:

var treeName = "myElmTree";
var branchName = "branch10";
var leafName = "leaf3";

tree[treeName].branch[branchName].leaf[leafName].color = yellow;

To turn this code into a function, capture the variables from the calling function and name the function "fx_leaf_color_set" and save as a global method:

var treeName = arguments[0];
var branchName = arguments[1];
var leafName = arguments[2];

tree[treeName].branch[branchName].leaf[leafName].color = yellow;

The calling method code would then be:

var tree = "myElmTree";
var branch = "branch10";
var leaf = "leaf3";

globals.fx_leaf_color_set(tree, branch, leaf);

CONCEPT 3: Capturing the form and element names

The third concept is where the abstraction begins. Even the last bit of code above had the data hard coded into the function. What I need to happen for the one method to work for all leaves is to capture the name of the leaf, the name of the branch, and the name of the tree when a particular leaf is selected. I need something that will do the following in Servoy terms:

var tree = getTreeTriggerName();
var branch = getBranchTriggerName();
var leaf = getLeafTriggerName();

globals.fx_leaf_color_set(tree, branch, leaf);

Assuming every leaf, branch and tree have unique names we now have a function that works when any leaf is selected.

In Servoy, the two functions are "application.getMethodTriggerElementName()" and "application.getMethodTriggerFormName()". (We only need these two as we can't reference an object in another Servoy solution.)

CONCEPT 4: Naming conventions

This last concept is the cornerstone to making it all work. In our example, each column has two element objects to denote sort direction. Each one of these objects has to a have a unique name. The key is to use systematic names for these objects so you can use a loop to reference all the same type of objects instead of hard coding a line for each object.

For example, say I have ten leaves that I want to change the color on. I can use ten lines of code (one for each leaf) or I can use a loop like this:

for (var i; i < 10; i++)
{
    leafReference.color = blue;
}

Referencing the leaf correctly is the important part. One way to do it is to name each leaf in a sequential manner (leaf_1, leaf_2, leaf_3 ...). Our code then becomes:

var leafName = null;

for (var i; i < 10; i++)
{
    leafName = "leaf_" + (i + 1);
    tree.myElmTree.branch.branch10.leaf[leafName].color = yellow;
}

The answers

With these basic concepts in mind, you should be able to figure out how I am using one function, one method attached to the onAction even of column header labels and naming conventions to provide sort functionality for any list in my solution. The function you have seen already:

//NAME: fx_SORT_columns
//this fx sorts column headers in list views and displays the correct sort directional graphic

var sortImages = arguments[0];
var formName = arguments[1];
var columnNum = arguments[2];

var displayImages = new Array();
var tempString = new Array();

for ( var i = 0 ; i < sortImages.length ; i++ )
{
    tempString = sortImages[i].split(':::');
    displayImages[i] = tempString[0];
}

//sort id column
var columnAsc = sortImages[columnNum - 1].split(':::');
var columnDesc = sortImages[columnNum].split(':::');

if (forms[formName].elements[columnAsc[0]].visible == false)
{
    forms[formName].controller.sort(columnAsc[1]);

    for ( var j = 0 ; j < displayImages.length ; j++ )
    {
        forms[formName].elements[displayImages[j]].visible = false
    }

    forms[formName].elements[columnAsc[0]].visible = true;
}
else
{
    forms[formName].controller.sort(columnDesc[1]);

    for ( var j = 0 ; j < displayImages.length ; j++ )
    {
        forms[formName].elements[displayImages[j]].visible = false
    }

    forms[formName].elements[columnDesc[0]].visible = true;
}

Next is the method attached to the onAction event of column headers:

//load sort images for all columns
var sortImages = new Array(    'id_asc:::pk_client asc',
                            'id_desc:::pk_client desc',
                            'first_asc:::name_first asc',
                            'first_desc:::name_first desc',
                            'last_asc:::name_last asc',
                            'last_desc:::name_last desc',
                            'phone_asc:::phone_cell asc',
                            'phone_desc:::phone_cell desc',
                            'email_asc:::email asc',
                            'email_desc:::email desc',
                            'type_asc:::client_type asc',
                            'type_desc:::client_type desc');

//form name                            
var formName = 'lvl1_client_list';

//column number
var btnName = application.getMethodTriggerElementName();
var columnNum = utils.stringRight(btnName, 2);

if (columnNum.charAt(0) == '_')
{
    columnNum = columnNum.substr(1,2);
}

//fx to perform sort and display column sort direction graphics
globals.fx_SORT_columns(sortImages, formName, columnNum);

The key to making this all work is how I name my column header objects and my sort graphics objects. Not the easiest thing to see, but here is a layout view:

Layout1

Each column header is named "btn_sort_x" where x is a series that starts at 1 and goes up skipping every other number. I do this to line up the correct column with the data array in the method called. The sort graphic images are also named to match with the data array.

Conclusion

This is not the simplest code to start with if you are new to these ideas but hopefully you have gained enough understanding of the concepts involved to start applying to your own solutions.

Screen1_4

| Posted by David Workman on January 18, 2005 at 09:20 PM in Challenge | Permalink

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d8341c8d8153ef00d8344067b253ef

Listed below are links to weblogs that reference [Challenge] Week 1/10/05: Code challenge (answer):

Comments

Stunning!! The description is exceptionally clear. Will start using this technique. Appreciated.

Posted by: Morley | Jan 19, 2005 6:01:12 PM

This article was the catalyst for a fundamental breakthru in my work. Took a bit of consulting JS books and tinkering with syntax but the first 30 lines i wrote replaced 600 lines of codes and 16 methods. I was stunned. Over last couple days i have done this translation on enuf methods that, for me, many end up being about 10% of the original size and one method works throughout the solution.
One different thing i do is have a table with fields filled with arrays to hold the data that i split out to sling at the methods... and that is how it feels to me. I learned scripts in FM and was always branching down to ever increasing granularity. This new way seems to be working up from concrete data to abstract method.
Anyway, its a new day. Any time you invest knocking your head against this article will be repaid 100 fold.
Thank you David.

Posted by: Jim Cavanaugh | Feb 19, 2005 10:04:36 AM

Genuis!! Although i've stumbled upon this challenge one year later..but i can said it have improve my programming
in Javascript.

Thanks

Posted by: vincent | May 10, 2006 4:02:16 AM

Fantastic piece of code, David! A real eye-opener...

Posted by: Ben Savignac | Mar 21, 2008 9:58:17 AM

Post a comment