[NEW] Servoy Tutorials

I have started putting together some tutorials on object-oriented programming with Servoy, as well as other Servoy development topics that may be of interest. It’s still a work in progress, and I hope to post some more in the near future.

Here is the current list (1/2014):

Thnaks a lot GaryDotzlaw

Best regards. Roberto Blasco.

This is great stuff!! Thanks Gary

Hi,

Good tutorials especially the foundset tip is great, I did some testing with 677000+ records,
here are some results run with this code,

var $start = new Date()
/** @type {JSFoundSet<db:/northw/orders>} */
var $fs = databaseManager.getFoundSet('db:/northw/orders');
$fs.loadAllRecords();

/**this line is used in the first test only**/
$fs.getRecord(databaseManager.getFoundSetCount($fs))  
/**this line is used in the first test only**/
	
for(var i = 1 ; i <= $fs.getSize() ; i++){
    $ff = $fs.getRecord(i).shipname + 'a'
}
	
	var $end = new Date();
	
	
	application.output('Loading 200 records every time ' + (new Date() - $start)+'' + 'ms');
	application.output('Records : '  + $fs.getSize())

this one is from the java console

The Load all records 75749ms
Records : 677011
Loading 200 records every time 403611ms
Records : 677011

This whats happening on the servoy server using your foundset tip, the Performance Data page has only 1 “Load foundset” entry
[attachment=1]2013-11-24_1750_cons.png[/attachment]

This is what happens when not using your foundset tip the list is very long , the Performance Data page shows 200
[attachment=0]2013-11-24_1753_cons1.png[/attachment]

Regards,

Peter

2013-11-24_1750_cons.png

Thanks Guys.

To expand on Peter’s observations:

The tutorial on Using an Object as a Cache demonstrates how to load the entire foundset one-time, store it, and retrieve it, whenever you need it. This further reduces load tremendously. It contains all the methods you need to get started and can easily be extended to cache more than just foundsets.

The tutorial on Function Memoization is also a key one to boasting performance. The tutorial shows you how to cache values in an object within your function, so that if you call the method multiple times, you can look-up the computed value from the function’s cache, rather than recomputing it all over again. This one has proven itself as a massive performance boast over and over again.

I will put up some more articles soon.

Gary. This is excellent work.

I think you should publish these tutorials on ServoyForge. (ServoyForge is a repository or clearinghouse for Servoy related tutorials and open source projects.)

For example, Patrick Talbot published there a link and intro to his Servoy-Stuff Tutorials. He also published there the article: “Where is your code executed?”

I think that ServoyForge is the best clearinghouse for the Servoy community… Also your work will have more exposure at ServoyForge because, after a couple of month, your tutorials will not be readily visible in he forum, but they will be visible on the home page of ServoyForge as long as you want. This makes it easy for newbies and no-so newbies to find these magnificent tutorials.

Anyway, thank you very much for all of this.
Carlos

Peter,

I’m curious if you will also see even more performance improvements if you cache the foundset.getSize() before your loop like this:
code edited to include loading the max records

iMax = databaseManager.getFoundSetCount($fs);
$fs.getRecord(iMax) ;
   
for(var i = 1 ; i <= iMax ; i++){
    $ff = $fs.getRecord(i).shipname + 'a'
}

rather than how you ran it in your tests, like this:

databaseManager.getFoundSetCount($fs);

for(var i = 1 ; i <= $fs.getSize() ; i++){
    $ff = $fs.getRecord(i).shipname + 'a'
}

My testing in Optimizing Code Performance found that the for loop ran slower if you used the foundset.getSize() in the loop. This would be because it has to check the foundset size every time through the loop, which is unnecessary if you are not adding records. This is similar to optimizing a loop through an array; cache array.length before the for loop and avoid Javascript having to go and look it up every iteration.

With small foundsets, this might be a micro-optimization, but I found it translates to significant performance improvements when working with larger foundsets. Bottom-line is that I now force myself to write all for loops with cached values; I think its a good habit to get into because you just never know when someone is going to shove a large volume of records down your method’s throat.

if you cache getSize() it will return 200 for a foundset that can have millions of records.
So your loop will stop at 200 and not iterate upon the whole foundset… quite a performance enhancement though! ;)

You should do
var iMax = databaseManager.getFoundsetCount($fs);
to get the real number of records if you want that cached…

Agreed. He had loading all the records in his test scenario, but he should test it using the cached value in his loop. Best would be to rewrite his test like this:

iMax = databaseManager.getFoundSetCount($fs);
$fs.getRecord(iMax) ;
   
for(var i = 1 ; i <= iMax ; i++){
    $ff = $fs.getRecord(i).shipname + 'a'
}

I posted two additional tutorials, and updated the list at the top of this thread. The two new tutorials are:

Take back the UI with OOP

Adding forms to Tap Panels using a Map

I hope you enjoy them.

GaryDotzlaw:
Object-Oriented Programming

function Person (firstName, lastName, title){
    this.firstName = firstName;
    this.lastName = lastName;
    this.title = title;
}

var Person_proto = function(){
    Person.prototype = {
        fullName : function(){
            return this.firstName + " " + this.lastName;
        },
        fullNameTitle : function(){
            return this.fullName() + ", " + this.title; 
        }
    }
}();

var person = new Person('Gary','Dotzlaw','JavaScript Servoy Blogger');

Is it possible to get code completion for the Person.prototype functions?

[attachment=0]2013-11-26 09_46_21-Servoy_Developer_person_prototype_code_completion.png[/attachment]

P.S.: Thanks, for the tutorials. :)

Sebastian,

I’m glad you are enjoying the articles. There are more to come, and the next few will kick it up a notch.

I am not aware of any way to get Servoy to do code completion on the prototype functions. Servoy does not handle prototype very elegantly, at this time. The problem is demonstrated by the fact that we have to use a self-invoked, immediate function call, that we assign to a variable, just to add to the prototype. Servoy doesn’t know the prototype is there, and what functions you have added, until the client is running and it reads the .js file. As a result, in the editor, you are left with no code completion for the functions, and you will have your typical annoying JSDoc warnings (suppress or use bracket notation like person"fullName").

Honestly, I think Servoy needs to support objects and the prototype better. There is really no good reason it can not see the functions; every other editor can.

Gary

I’m just catching up on these Tutorials and really appreciate you taking the time to share.

When I’m struggling on a problem or trying to optimise code it is really useful to see how others have approached similar situations. Your combination of code and explanations are excellent.

Thanks again.

Graham

Thanks Graham. I appreciate the feedback.

To start with, very nice write-ups! Not easy to be clear and understandable with complex topics and you’re nailing it.

GaryDotzlaw:
Honestly, I think Servoy needs to support objects and the prototype better. There is really no good reason it can not see the functions; every other editor can.

I think you missed the relatively recent Servoy addition of the @constructor jsdoc tag. Basically fixes all of your workarounds with the prototype.

From a Servoy Magazine article that we just haven’t gotten around to writing up (so I’ll just post it here and let you explain in english!), some code. The constructor function “Resolution” is used to build up a a popup menu of items with each item passing along associated properties to the popup menu’s action method. Which just so happens to be the same method so eventually at the end of the method, you have webutils stuff happening with parameters based on the selected menu item’s instance of the prototype. Code credits to Troy:

/**
 * @param {JSEvent|Number} [event] the event that triggered the action/item index
 * @param {Number} [menuParentIdx]
 * @param {Boolean} [menuSelected]
 * @param {String} [menuParentTxt]
 * @param {String} [menuTxt]
 * @param {String} [name] Name of item selected
 * @param {Number} [width]
 * @param {Number} [height]
 * @param {Boolean} [getDims] Return dimensions for selected option
 * 
 * @return {{width: Number, height:Number}|undefined}
 *
 * @properties={typeid:24,uuid:"2EFD8325-BF6F-40CC-8188-C445DAB04ECC"}
 */
function ACTION_resize(event,menuParentIdx,menuSelected,menuParentTxt,menuTxt,name,width,height,getDims) {
	/**
	* @constructor 
	* @param {String} name Display value of resolution
	* @param {Number} big Largest side of resolution
	* @param {Number} little Smallest side of resolution
	*/
	function Resolution(name, big, little) {
		/** 
		 * Display value of resolution
		 * @type {String}
		 */
		this.name = name
		/**
		 * Largest side of resolution
		 * @type {Number}
		 */
		this.big = big
		/**
		 * Smallest side of resolution
		 * @type {Number}
		 */
		this.little = little
		/**
		 * Display "Name (horizontal x portrait)"
		 * @return {String}
		 */
		this.getName = function() {
			return name + ' (' + this.getDimHoriz() + ' x ' + this.getDimVert() +')'
		}
		/**
		 * Returns horizontal dimension
		 * @return {Number}
		 */
		this.getDimHoriz = function() {
			return _resizeOrient ? little : big
		}
		/**
		 * Returns vertical dimension
		 * @return {Number}
		 */
		this.getDimVert = function() {
			return !_resizeOrient ? little : big
		}
	}
	
	if (application.getApplicationType() != APPLICATION_TYPES.WEB_CLIENT) {
		plugins.dialogs.showErrorDialog(
				'Error',
				'Resize works only in the web'
			)
	}
	else {
		var map = {
				phone: [
					new Resolution('Normal',480,320),
					new Resolution('iPhone 5+',568,320),
					new Resolution('nHD',640,360)
				],
				tablet: [
					new Resolution('iPad',1024,768),
					new Resolution('7-inch',1024,600),
					new Resolution('Large',640,480),
					new Resolution('Ginormous',960,720)
				],
				desktop: [
					new Resolution('Most common',1366,768), //as of 2013 Oct
					new Resolution('Square',1280,1024),
					new Resolution('20-incher',1680,1050),
					new Resolution('24-incher',1920,1200)
				],
				tv: [
					new Resolution('720p HD',1280,720),
					new Resolution('1080p Full HD',1920,1080),
					new Resolution('4K Ultra HD',3840,2160)
				]
			}
		map.phone.icon = 'media:///ssstandard_mobile_small.png'
		map.tablet.icon = 'media:///ssstandard_tablet_small.png'
		map.desktop.icon = 'media:///ssstandard_desktop_small.png'
		map.tv.icon = 'media:///ssstandard_landscape_small.png'
		
		/**
		 * @param {String} type
		 */
		function mapLoop(type) {
			var displayName = type == 'tv' ? 'TV' : utils.stringInitCap(type)
			
			//divider with name and icon
			if (map[type].icon) {
				item = menu.addMenuItem(displayName)
				if (solutionModel.getMedia(utils.stringReplace(map[type].icon,'media:///',''))) {
					item.setIcon(map[type].icon)
				}
				item.enabled = false
			}
			
			for (var i = 0; i < map[type].length; i++) {
				/** @type {Resolution} */
				var res = map[type][i]
				if (res instanceof Resolution) {
					if (res.name == _resizeSelected) {
						item = menu.addCheckBox(res.getName())
						item.selected = true
					}
					else {
						item = menu.addMenuItem(res.getName())
					}
					
					item.setMethod(ACTION_resize)
					item.methodArguments = [res.name,res.getDimHoriz(),res.getDimVert()]
				}
			}
		}
		
		//when right clicked, give a moment to grab focus elsewhere
		if (event instanceof JSEvent) {
			var menu = plugins.window.createPopupMenu()
			var item
			
			//fluiditize
			item = menu.addCheckBox('Fit to window')
			item.setIcon("media:///ssstandard_move_small.png")
			item.setMethod(ACTION_resize)
			item.methodArguments = ['Fluid']
			if (_resizeSelected == 'Fluid') {
				item.selected = true
			}
//			menu.addSeparator()
			//orientate
			item = menu.addMenuItem('Flip orientation')
			item.setIcon("media:///ssstandard_rotate_small.png")
			item.setMethod(ACTION_resize)
			item.methodArguments = ['Orient']
			
			menu.addSeparator()
			
			mapLoop('phone')
			mapLoop('tablet')
			mapLoop('desktop')
			mapLoop('tv')
			
			
			menu.show(elements.lbl_resize)
			
			TOGGLE_block_popup(true)
		}
		else {
			TOGGLE_block_popup(false)
			
			var id = plugins.WebClientUtils.getElementMarkupId(forms.WEB_0F_page__live__web__view.elements.lbl_page)
			
			//grab default name if nothing passed in
			if (!name) {
				name = _resizeSelected
				
				outer:
				for (var i in map) {
					for (var j = 0; j < map[i].length; j++) {
						if (map[i][j].name == name) {
							width = map[i][j].getDimHoriz()
							height = map[i][j].getDimVert()
							break outer
						}
					}
				}
			}
			
			//return out selected dimensions
			if (getDims) {
				if (name != 'Fluid') {
					return {
						width: width,
						height: height
					}
				}
			}
			else {
				//orientation
				if (name == 'Orient') {
					//konami code (toggle orientation 3 times in a row)
					ACTION_resize.konami = typeof ACTION_resize.konami == 'number' ? ACTION_resize.konami + 1 : 1
					
					//flip orientation
					_resizeOrient = _resizeOrient ? 0 : 1
					
					//toggle orientation for selected resolution
					if (_resizeSelected != 'Fluid') {
						ACTION_resize()
					}
					return
				}
				//fluid
				else if (name == 'Fluid') {
					plugins.WebClientUtils.executeClientSideJS(
						'$("#' + id + '_cms").animate({width:"100%",height:"100%"}).removeClass("drunk");'
					)
				}
				//specific dimension
				else {
					//activate konami code
					var konami = ''
					if (event) {
						ACTION_resize.konami = null
					}
					if (ACTION_resize.konami == 3) {
						konami = '.addClass("drunk")'
					}
					
					plugins.WebClientUtils.executeClientSideJS(
						'$("#' + id + '_cms").animate({width:"' + width + 'px",height:"' + height + 'px"})' + konami + ';'
					)
				}
				
				//save down what resolution we're on
				_resizeSelected = name
			}
		}
	}
}

Code completion on the constructor function:

[attachment=1]constructor-code-completion.png[/attachment]

End result in real world app:

[attachment=0]constructor-in-action.png[/attachment]

Thanks for the feedback.

We were looking for a solution to get code-completion working for the object’s prototype; if you add functions to the prototype Servoy will not see them in code complete.

So, I agree, this works fine. I don’t need anything special; Servoy can code complete everything in the constructor shown below including Person.fullName() (no JSDoc warnings either). I don’t need any @constructor tag.

function Person (firstName, lastName, title){
    this.firstName = firstName;
    this.lastName = lastName;
    this.title = title;
    this.fullName = function(){
        return this.firstName + " " + this.lastName;
    }
}

However, if you take the function and put it on the prototype for the object, then Servoy cannot see it (no code complete and you get JSDoc warning), but it still works fine. You have to do it in the prototype like this:

var Person_proto = function(){
    Person.prototype = {
        fullName : function(){
            return this.firstName + " " + this.lastName;
        },
        fullNameTitle : function(){
            return this.fullName() + ", " + this.title; 
        }
    }
}();

As well, Servoy cannot see functions that are in a Revealing Module pattern, an object constructor that hides private variables / functions and only exposes certain things to the external world, like an api. In this case, although the functions are in the object constructor, Servoy does not see the “type” or “getInfo” functions.

function Fruit2(name, color, rating, price) { 
    var _name = name, 
        _color = color,
        _rating = rating,
        _price = price,
        type = function () { 
            return 'I am a ' + _color + ' ' + _name; 
        },
        getInfo = function(){
            return 'Rating: ' + _rating + ', Price: ' + _price
        };
        // Servoy does not see the public functions exposed here
        return {
            type: type,
            getInfo: getInfo
        };
}

I think the JSDoc @constructor tag is used more for documentation (let others know what it is or maybe auto-generate API docs). I don’t think Servoy does anything with it.

GaryDotzlaw:
The tutorial on Using an Object as a Cache demonstrates how to load the entire foundset one-time, store it, and retrieve it, whenever you need it. This further reduces load tremendously. It contains all the methods you need to get started and can easily be extended to cache more than just foundsets.

You can add “pseudo” data broadcasting functionality to cached objects. Add serialize(), save() and get() methods to store/retrieve the object, and fire whenever the object is accessed or changed. This allows you to share a cached object around to all clients.

We use this to keep our slickgrid component data up-to-date across all clients. A cached object maintains a list of “dirty” records by client ID. Any action on a client slickgrid checks this object for changes from other clients and updates the client-side datastore with just the changes. Client CRUD operations write to the cached object for other clients to pick up as needed.

This is how we get high performance (million+ records) grids in Servoy web client that stay updated. Same technique can be used for any client-side component (ie, jquery calendar, etc).

A couple of catches: this does not actively “push” to all clients like you’re used to with Servoy’s data broadcasting. And record locking is something you have to manage yourself (ie, timestamps). So some tradeoffs. When you start coding this functionality yourself you gain a new appreciation for how much complexity Servoy’s foundsets implement.

GaryDotzlaw:
I think the JSDoc @constructor tag is used more for documentation (let others know what it is or maybe auto-generate API docs). I don’t think Servoy does anything with it.

https://wiki.servoy.com/display/SERV61/Function :wink:

(edit)
Just to be clear, Servoy does do something with the @constructor jsdoc tag: “Specifies the function that creates an object’s prototype.” So no need to add methods on the prototype anymore.

Way back I argued against the use of jsdoc tags for more than just documentation (I think it came up over the “@AllowToRunInFind” tag) specifically because of the potential that people wouldn’t naturally assume that some tags do something. But in the end, it’s better than separate system for IDE/compiler/parser directives.

And then just use this wherever you need code completion:

/** @type {SomeConstructor} */

In this example, variable “res” gets code completion based on the constructor function:

for (var i = 0; i < map[type].length; i++) {
	/** @type {Resolution} */
	var res = map[type][i]
	if (res instanceof Resolution) {
		if (res.name == _resizeSelected) {
			item = menu.addCheckBox(res.getName())
			item.selected = true
		}
		else {
			item = menu.addMenuItem(res.getName())
		}
		
		item.setMethod(ACTION_resize)
		item.methodArguments = [res.name,res.getDimHoriz(),res.getDimVert()]
	}
}

It’s a pretty slick feature.

And I bet if you tag a function/property in a constructor with “@private” (possibly “@ignore” too?) it wouldn’t show up in code completion.