Child pages
  • Foundset property type

Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Code Block
languagejs
titleBrowser side provided property content in model
myFoundset: {
    foundsetId: 2, // an identifier that allows you to use this foundset via the 'foundsetRef' type;
                   // when a 'foundsetRef' type sends a foundset from server to client (for example
                   // as a return value of callServerSideApi) it will translate to this identifier
                   // on client (so you can use it to find the actual foundset property in the model if
                   // server side script put it in the model as well); internally when sending a
                   // 'foundset' typed property to server through a 'foundsetRef' typed argument or prop,
                   // it will use this foundsetId as well to find it on server and give a real Foundset

    serverSize: 44, // the size of the foundset on server (so not necessarily the total record count
                    // in case of large DB tables)
    viewPort: {
        // this is the data you need to have loaded on client (just request what you need via provided
        // loadRecordsAsync or loadExtraRecordsAsync)
        startIndex: 15,
        size: 5,
        rows: [ { _svyRowId: 'someRowIdHASH1', name: "Bubu", type: 2 },
                { _svyRowId: 'someRowIdHASH2', name: "Ranger", type: 1 },
                { _svyRowId: 'someRowIdHASH3', name: "Yogy", type: 2 },
                { _svyRowId: 'someRowIdHASH4', name: "Birdy", type: 3 },
                { _svyRowId: 'someRowIdHASH5', name: "Wolfy", type: 4 } ]
    },
    selectedRowIndexes: [16], // array of selected records in foundset; indexes can be out of current
                              // viewPort as well
	sortColumns: 'orderid asc', // sort string of the foundset, the same as the one used in scripting for 
								// foundset.sort and foundset.getCurrentSort
    multiSelect: false, // the multiselect mode of the server's foundset; if this is false,
                        // selectedRowIndexes can only have one item in it
    hasMoreRows: false, // if the foundset is large and on server-side only part of it is loaded (so
                        // there are records in the foundset beyond 'serverSize') this is set to true;
                        // in this way you know you can load records even after 'serverSize' (requesting
                        // viewport to load records at index serverSize-1 or greater will load more
                        // records in the foundset)
    columnFormats: { name: (...), type: (...) }, // columnFormats is only present if you specify
                        // "provideColumnFormats": true inside the .spec file for this foundset property;
                        // it gives the default column formatting that Servoy would normally use for
                        // each column of the viewport - which you can then also use in the
                        // 	browser yourself

    /**
     * Request a change of viewport bounds from the server; the requested data will be loaded
     * asynchronously in 'viewPort'
     *
     * @param startIndex the index that you request the first record in "viewPort.rows" to have in
     *                   the real foundset (so the beginning of the viewPort).
     * @param size the number of records to load in viewPort.
     *
     * @return {promise} a $q promise that will get resolved when the requested records arrived browser-
     *                   side. As with any promise you can register success, error callbacks, finally, ...
     */
    loadRecordsAsync: function(startIndex, size),

    /**
     * Request more records for your viewPort; if the argument is positive more records will be
     * loaded at the end of the 'viewPort', when negative more records will be loaded at the beginning
     * of the 'viewPort' - asynchronously.
     *
     * @param negativeOrPositiveCount the number of records to extend the viewPort.rows with before or
     *                                after the currently loaded records.
     * @param dontNotifyYet if you set this to true, then the load request will not be sent to server
     *                      right away. So you can queue multiple loadLess/loadExtra before sending them
     *                      to server.
     *
     * @return {promise} a $q promise that will get resolved when the requested records arrived browser-
     *                   side. As with any promise you can register success, error callbacks, finally, ...
     *                   That allows custom component to make sure that loadExtra/loadLess calls from
     *                   client do not stack on not yet updated viewports to result in wrong bounds.
     */
    loadExtraRecordsAsync: function(negativeOrPositiveCount, dontNotifyYet),
 
    /**
     * Request a shrink of the viewport; if the argument is positive the beginning of the viewport will
     * shrink, when it is negative then the end of the viewport will shrink - asynchronously.
     *
     * @param negativeOrPositiveCount the number of records to shrink the viewPort.rows by before or
     *                                after the currently loaded records.
     * @param dontNotifyYet if you set this to true, then the load request will not be sent to server
     *                      right away. So you can queue multiple loadLess/loadExtra before sending them
     *                      to server.
     *
     * @return {promise} a $q promise that will get resolved when the requested records arrived browser
     *                   -side. As with any promise you can register success, error callbacks, finally, ...
     *                   That allows custom component to make sure that loadExtra/loadLess calls from
     *                   client do not stack on not yet updated viewports to result in wrong bounds.
     */
    loadLessRecordsAsync: function(negativeOrPositiveCount, dontNotifyYet),
 
    /**
     * Sort the foundset by the dataproviders/columns identified by sortColumns.
     *
     * The name property of each sortColumn can be filled with the dataprovider name the foundset provides
     * or specifies. If the foundset is used with a component type (like in table-view) then the name is
     * the name of the component on who's first dataprovider property the sort should happen. If the
     * foundset is used with another foundset-linked property type (dataprovider/tagstring linked to 
     * foundsets) then the name you should give in the sortColumn is that property's 'idForFoundset' value
     * (for example a record 'dataprovider' property linked to the foundset will be an array of values
     * representing the viewport, but it will also have a 'idForFoundset' prop. that can be used for 
     * sorting in this call; this 'idForFoundset' was added in version 8.0.3).
     *
     * @param {JSONArray} sortColumns an array of JSONObjects { name : dataprovider_id,
     *                    direction : sortDirection }, where the sortDirection can be "asc" or "desc".
     * @return {promise} (added in Servoy 8.2.1) a $q promise that will get resolved when the new sort
     *                   will arrive browser-side. As with any promise you can register success, error
     *                   and finally callbacks.
     */
    sort: function(sortColumns),
 
    /**
     * Request a selection change of the selected row indexes. Returns a promise that is resolved
     * when the client receives the updated selection from the server. If successful, the array
     * selectedRowIndexes will also be updated. If the server does not allow the selection change,
     * the reject function will get called with the 'old' selection as parameter. 
     *
     * If requestSelectionUpdate is called a second time, before the first call is resolved, the
     * first call will be rejected and the caller will receive the string 'canceled' as the value
     * for the parameter serverRows.
     * E.g.: foundset.requestSelectionUpdate([2,3,4]).then(function(serverRows){},function(serverRows){});
     */
    requestSelectionUpdate : function(selectedRowIdxs),
 
    /**
     * Sets the preferred viewPort options hint on the server for this foundset, so that the next
     * (initial or new) load will automatically return that many rows, even without any of the loadXYZ
     * methods above being called.
     *
     * You can use this when the component size is not known initially and the number of records the
     * component wants to load depends on that. As soon as the component knows how many it wants 
     * initially it can call this method.
     *
     * These can also be specified initially using the .spec options "initialPreferredViewPortSize" and
     * "sendSelectionViewportInitially". But these can be altered at runtime via this method as well
     * because they are used/useful in other scenarios as well, not just initially: for example when a
     * related foundset changes parent record, when a search/find is performed and so on.
     *
     * @param preferredSize the preferred number or rows that the viewport should get automatically
     *                      from the server.
     * @param {boolean} sendViewportWithSelection if this is true, the auto-sent viewport will contain
     *                                            the selected row (if any).
     * @param {boolean} centerViewportOnSelected if this is true, the selected row will be in the middle
     *                                           of auto-sent viewport if possible. If it is false, then
     *                                           the foundset property type will assume a 'paging'
     *                                           strategy and will send the page that contains the 
     *                                           selected row (here the page size is assumed to be
     *                                           preferredSize).
     */
    setPreferredViewportSize: function(preferredSize, sendViewportWithSelection, centerViewportOnSelected),

    /**
     * Add a change listener that is interested in knowing of any incoming changes (from server)
     * for this foundset property. See the "Adding a change listener" section below for more information.
     */
    addChangeListener : function(listener),

    /**
     * Removes the given change listener from this foundset property.
     */
    removeChangeListener: function(listener),
 
    /**
     * Receives a client side rowID (taken from myFoundsetProp.viewPort.rows[idx]
     * [$foundsetTypeConstants.ROW_ID_COL_KEY]) and gives a Record reference, an object
     * which can be resolved server side to the exact Record via the 'record' property type;
     * for example if you call a handler or a $scope.svyServoyapi.callServerSideApi(...) and want
     * to give it a Record as parameter and you have the rowID and foundset in your code,
     * you can use this method. E.g: $scope.svyServoyapi.callServerSideApi("doSomethingWithRecord",
     *                     [$scope.model.myFoundsetProp.getRecordRefByRowID(clickedRowId)]);
     *
     * NOTE: if in your component you know the whole row (so myFoundsetProp.viewPort.rows[idx])
     * already - not just the rowID - that you want to send you can just give that directly to the
     * handler/serverSideApi; you do not need to use this method in that case. E.g:
     * // if you have the index inside the viewport
     * $scope.svyServoyapi.callServerSideApi("doSomethingWithRecord",
     *           [$scope.model.myFoundsetProp.viewPort.rows[clickedRowIdx]]);
     * // or if you have the row directly
     * $scope.svyServoyapi.callServerSideApi("doSomethingWithRecord", [clickedRow]);
     *
     * This method has been added in Servoy 8.3.
     */
	getRecordRefByRowID: function(rowId)

}

 

  • foundsetId is controlled by the server; you should not change it

     

  • serverSize is controlled by the server; you should not change it

  • viewPort initially has size 0, and startIndex 0. When the component detects that records are available (serverSize > 0) it care request viewPort contents using one of the two load async methods
    • viewPort.startIndex and viewPort.size will have the values requested by the async load methods. But if for example you are using data at the end of the foundset and records are deleted from there then viewport.size will be corrected/decreased from server (as there aren't enough records). A similar thing can happen to viewPort.startIndex. Do not modify these directly as that will have no effect. Use the load async methods instead.
    • viewPort.rows contains the viewPort data. Each item of the array represents data from a server-side record. Each item will always contain a "_svyRowId" ($foundsetTypeConstants.ROW_ID_COL_KEY in angular world) entry that uniquely identifies the record on server. Then there's one entry for every dataprovider that the component needs to use (how those are selected is described below). You should never change the "_svyRowId" entry, but it is possible to change the values of any of the other entries - the new values will be pushed back into the server side record that they belong to (if pushToServer is set on the foundset property to allow/shallow or deep; see "Data synchronization" section of https://wiki.servoy.com/display/public/DOCS/Specification).
  • selectedRowIndexes is an array of selected foundset record indexes. This can get updated by the server if foundset selection changes server side. You can change the contents of this array to change foundset selection (new selection will be pushed to server). However, the preferred way of changing the record selection is by using "requestSelectionUpdate".
  • sortColumns is a string containing the sort columns of the foundset, like 'columnA desc,columnB asc'
  • multiselect represents the foundset multiselect state; do not change it as it will not be pushed to server.
  • columnFormats represents the default column formats for the columns given in the viewport; do not change this - only server pushes this information to the client if asked to do so by the .spec file. It is only present if you specify "provideColumnFormats": true inside the .spec file for this foundset property.
  • hasMoreRows true if the server side foundset has loaded only a part/chunk of it's records (in case of very large foundsets). In that case there are records even after 'serverSize'. It is controlled and updated by the server; you should not change it.

...

Code Block
languagejs
// elements.myFoundsetBasedBean.myFoundset is the foundset typed property
var myNewFoundset = ...; // some Servoy foundset
elements.myFoundsetBasedBean.myFoundset = myNewFoundset; // this will create a new foundset type property value
// that only sends the rowId (no other columns as dataproviders were not specified) and uses defaults for sendSelectionViewportInitially and initialPreferredViewPortSize

Combining Foundset Property Type, Foundset Reference Type, Record

...

type and client-to-server scripting calls
Anchor
advanced-foundset-usage
advanced-foundset-usage

You might wonder - "why is setting a complete new foundset into a foundset typed property from server side scripting helpful?". This is helpful for example in implementing more advanced tree-like components, that need to operate with multiple foundsets.

Expand
titleFor Servoy < 8.3 expand this section.

In combination with Foundset Reference type ("foundsetRef"), Record Finder type ("rowRef") and calls from client-side scripting to server-side component scripting, such components can query/create foundsets on server on-the-fly according to different requirements, put them in the model of the component (for example in a foundset array property that is initially empty []). Then they also store in the properties the "unique id" using the Foundset Reference type and return that id as well from the server-side scripting call. This means that on the client it has access to the new foundset and it can identify it via the "unique id". Also if server-side scripting needs a record from a foundset that is already on the client to create it's new foundset (maybe they need to be related in some way), then all the client has to do is send to the server the foundset reference "unique id" together with the rowId (from the foundset property type's viewport) of that record and on the server you will be able to find the record using the Record Finder type.

Here is a partial example of what a tree-table might need to do in order to handle large amounts of data properly on all levels:

Code Block
languagejs
titleClient-side .js
function getChildFoundSetHash(parentFoundsetHash, rowId, parentLevelGroupColumnIndex,
                                                  newLevelGroupColumnIndex) {
	// parentFoundsetHash comes from the foundset referece type property
    // rowId comes from the foundset property type's viewport
    // parentLevelGroupColumnIndex and newLevelGroupColumnIndex are indexes in
    // an array property that holds dataproviders
    var childFoundsetPromise;

    if (newLevelGroupColumnIndex) {
        childFoundsetPromise = $scope.svyServoyapi.callServerSideApi("getGroupedChildFoundsetUUID",
                      [parentFoundsetHash, rowId, parentLevelGroupColumnIndex, newLevelGroupColumnIndex]);
    } else {
        childFoundsetPromise = $scope.svyServoyapi.callServerSideApi("getLeafChildFoundsetUUID",
                      [parentFoundsetHash, rowId, parentLevelGroupColumnIndex]);
	}

    childFoundsetPromise.then(function(childFoundsetUUID) {
        var childFoundset = getFoundSetByFoundsetUUID(childFoundsetUUID);
        mergeData(..., childFoundset);
    }, function() {
        // some error happened
        (...)
    });
}
(...)
function getFoundSetByFoundsetUUID(foundsetHash) {
    if ($scope.model.hashedFoundsets)
        for (var i = 0; i < $scope.model.hashedFoundsets.length; i++) {
            if ($scope.model.hashedFoundsets[i].foundsetHash == foundsetHash)
                return $scope.model.hashedFoundsets[i].foundset;
		 
    return null;
}
Code Block
languagejs
titleServer-side .js
$scope.getGroupedChildFoundsetUUID = function(parentFoundset, parentRecordFinder, parentLevelGroupColumnIndex,
                                                              newLevelGroupColumnIndex) {
        if (!parentFoundset) parentFoundset = $scope.model.myFoundset.foundset;
        var childQuery = parentFoundset.getQuery();
		
        if (parentLevelGroupColumnIndex == undefined) {
            // this is the first grouping operation; alter initial query to get all first level groups
            (...)
        } else {
			// this is an intemediate group expand; alter query of parent level for the child level
			childQuery.groupBy.clear();
            childQuery.groupBy.add(childQuery
                      .columns[$scope.model.columns[newLevelGroupColumnIndex].dataprovider]);
            var parentGroupColumnName = $scope.model.columns[parentLevelGroupColumnIndex].dataprovider;
            childQuery.where.add(childQuery.columns[parentGroupColumnName]
                      .eq(parentRecordFinder(parentFoundset)[parentGroupColumnName]));
        }
		
        var childFoundset = parentFoundset.duplicateFoundSet();
		childFoundset.loadRecords(childQuery);
		
		var dps = {};
		for (var idx = 0; idx < $scope.model.columns.length; idx++) {
			dps["dp" + idx] = $scope.model.columns[idx].dataprovider;
		}
		
		$scope.model.hashedFoundsets.push({ foundset: {
			foundset: childFoundset,
			dataproviders: dps,
			sendSelectionViewportInitially: false,
			initialPreferredViewPortSize: 15
		}, foundsetUUID: childFoundset}); // send it to client as a foundset property with a UUID
		
		return childFoundset; // return the UUID that points to this foundset (return type will make it UUID)
	};

For versions prior to Servoy 8.2 please use "api" instead of "internalApi" below:

Code Block
languagejs
title.spec file
	"serverscript": "mycomppck/mycompname/mycomp_server.js",
(...)
	"model": 
	{
        "columns": { "type": "columnDef[]", "droppable": true },
        "hashedFoundsets": { "type": "hashedFoundset[]", "default": [] }
(...)
	"types": 
	{
        "columnDef": {
            "dataprovider": { "type": "dataprovider", "forFoundset": "myFoundset" }
            (...)
        },
        "hashedFoundset" : {
	  		"foundset": "foundset",
	  		"foundsetUUID": "foundsetRef"
		}	
	},
	"internalApi" : {
		"getGroupedChildFoundsetUUID" : {
			"returns" : "foundsetRef",
			"parameters" : 
			[{
					"name" : "parentFoundset",
					"type" : "foundsetRef"
				}, {
					"name" : "parentRecordFinder",
					"type" : "rowRef"
				}, {
					"name": "parentLevelGroupColumnIndex",
					"type": "int"
				}, {
					"name": "newLevelGroupColumnIndex",
					"type": "int"
				}
			]
		},
(...)

For Servoy 8.3 and higher:

In combination with Foundset Reference type ("foundsetRef"), Record type ("record") and calls from client-side scripting to server-side component scripting, such components can query/create foundsets on server on-the-fly according to different requirements, put them in the model of the component (for example in a foundset array property that is initially empty []). Then they return from the server-side scripting call the "foundsetId" using the Foundset Reference return type (so return a Foundset on an api call that has return type 'foundsetRef'). This means that on the client it has access to the new foundset and it can identify it via the "foundsetId" in the array-of-foundsets-property.

If server-side scripting needs a record from a client-side foundset in order to create it's new foundset (maybe they need to be related in some way), then all the client has to do is send to the server the row from client side foundset property's viewport and on the server it will automatically be translated to a Record by the 'record' property type that is used as argument. Once you have the Record on server you have the foundset as well via Record.foundset.

Similarly, if one needs to send (from client-side) only a foundset as argument to server-side code, it can just give the value of the foundset property to an argument of type 'foundsetRef' and it will automatically be translated on server to a Foundset.

Here is a partial example of what a tree-table might need to do in order to handle large amounts of data properly on all levels:

Code Block
languagejs
titleClient-side .js
function getChildFoundSet(rowObjFromFoundsetsViewport, parentLevelGroupColumnIndex,
                                                  newLevelGroupColumnIndex) {
    // 'rowObjFromFoundsetsViewport' comes from the foundset (that contributed the expanded row)
    //     property type's viewport so equivalent to something like
    //     $scope.model.foundsetProps[i].viewPort.rows[expandedRowIndex]
    // 'parentLevelGroupColumnIndex' and 'newLevelGroupColumnIndex' are indexes in
    //     an array property that holds the grouping column dataproviders
    // if 'newLevelGroupColumnIndex' is undefined, then we are requesting tree leafs (not groups)
    //     and then the server-side query is a bit different

    // foundset query needed for leaf level: Select pk from orders where Country = ? and City = ? and ...
    // foundset query needed for intermediate grouped level; ie. when you want to expand a country group
    //     to next level that is grouped by city: SELECT DISTINCT MIN(pk) FROM Customers Where Country = 
    //     "Mexico" GROUP BY City;
                    
    // as you can see below I try to send abstract things to the server (the client shouldn't really know
    // real datasource names, real column/dataprovider names and so on (those can be determined on server)
    // so both client and server code should be done so that these kinds of information never reach the
    // client in the first place - only as abstract ids or indexes/names of component properties)
                    
    // so server needs to be given the expanded row (it can get the foundset of the Record from that) and
    // the groupColumn (index of the column) of the child level if that one is an grouped intermediate level
    // otherwise, if it is going to request leafs it should set undefined for "newLevelGroupColumnIndex"

    // NOTE: if we could get it nicely from the parent foundset's query there would be no use sending the
    // expanded node's group column because that is already available on the server from that foundset's
    // query (group by clause)

    var childFoundsetPromise;

    if (newLevelGroupColumnIndex) {
        childFoundsetPromise = $scope.svyServoyapi.callServerSideApi("getGroupedChildFoundsetId",
                      [rowObjFromFoundsetsViewport, parentLevelGroupColumnIndex, newLevelGroupColumnIndex]);
    } else {
        childFoundsetPromise = $scope.svyServoyapi.callServerSideApi("getLeafChildFoundsetId",
                      [rowObjFromFoundsetsViewport, parentLevelGroupColumnIndex]);
	}

    childFoundsetPromise.then(function(childFoundsetId) {
        var childFoundset = getFoundSetByFoundsetId(childFoundsetId);
        mergeData(..., childFoundset);
    }, function() {
        // some error happened
        (...)
    });
}
(...)
function getFoundSetByFoundsetId(foundsetId) {
    if ($scope.model.childFoundsets)
        for (var i = 0; i < $scope.model.childFoundsets.length; i++) {
            if ($scope.model.childFoundsets[i].foundsetId == foundsetId)
                return $scope.model.childFoundsets[i];
		 
    return null;
}
Code Block
languagejs
titleServer-side .js
$scope.getGroupedChildFoundsetId = function(parentRecord, parentLevelGroupColumnIndex,
                                            newLevelGroupColumnIndex) {

        var parentFoundset = parentRecord.foundset;
        var childQuery = parentFoundset.getQuery();
		
        if (parentLevelGroupColumnIndex == undefined) {
            // this is the first grouping operation; alter initial query to get all first level groups
            (...)
            return;
        } else {
			// this is an intemediate group expand; alter query of parent level for the child level
			childQuery.groupBy.clear();
            childQuery.groupBy.add(childQuery
                      .columns[$scope.model.columns[newLevelGroupColumnIndex].dataprovider]);
            var parentGroupColumnName = $scope.model.columns[parentLevelGroupColumnIndex].dataprovider;
            childQuery.where.add(childQuery.columns[parentGroupColumnName]
                      .eq(parentRecord[parentGroupColumnName]));
        }
		
        var childFoundset = parentFoundset.duplicateFoundSet();
		childFoundset.loadRecords(childQuery);
		
		var dps = {};
		for (var idx = 0; idx < $scope.model.columns.length; idx++) {
			dps["dp" + idx] = $scope.model.columns[idx].dataprovider;
		}
		
		$scope.model.childFoundsets.push({
			foundset: childFoundset,
			dataproviders: dps,
			sendSelectionViewportInitially: false,
			initialPreferredViewPortSize: 15
		}); // send it to client as a foundset property in the array of foundsets
		
		return childFoundset; // return the foudnsetId that points to this foundset (return type
                              // 'foundsetRed' will make a foundsetID from the childFoundset)
	};
Code Block
languagejs
title.spec file
	"serverscript": "mycomppck/mycompname/mycomp_server.js",
(...)
	"model": 
	{
        "columns": { "type": "columnDef[]", "droppable": true },
        "childFoundsets": { "type": "foundset[]", "default": [] }
(...)
	"types": 
	{
        "columnDef": {
            "dataprovider": { "type": "dataprovider", "forFoundset": "myFoundset" }
            (...)
        }
	},
	"internalApi" : {
		"getGroupedChildFoundsetId" : {
			"returns" : "foundsetRef",
			"parameters" : 
			[ { "name" : "parentRecord", "type" : "record" },
              { "name": "parentLevelGroupColumnIndex", "type": "int" },
              { "name": "newLevelGroupColumnIndex", "type": "int" } ]
		},
(...)