PDA

View Full Version : PagedGridView



Animal
25 Sep 2006, 6:47 AM
I'm trying to use a PagedGridView.

My DataModel returns true from isPaged()

But it's getting

this.events[eventName.toLowerCase()] has no properties.


I think it's these two lines in PagedGridView.prototype.render{}


this.grid.dataModel.addListener('beforeload', this.beforeLoad, this, true);
this.grid.dataModel.addListener('load', this.onPageLoaded, this, true);


My Data model extends AbstractDataModel which only has:



this.events = {
'cellupdated' : this.onCellUpdated,
'datachanged' : this.onTableDataChanged,
'rowsdeleted' : this.onRowsDeleted,
'rowsinserted' : this.onRowsInserted,
'rowsupdated' : this.onRowsUpdated,
'rowssorted' : this.onRowsSorted
};


So it can't add listeners for those events.

What should trigger these events? How can I fix my grid to use a paged view?

jack.slocum
25 Sep 2006, 1:26 PM
You will need to derived your DataModel from LoadableDataModel instead of AbstractDataModel. It defines all the paging/loading functionality. If you don't want to/can't extend LoadableDataModel, you can also implement all the page related functions/events manually in your model, but that wouldn't be fun.

I plan on writing an article tonight on using paging. In the meantime, there are two key properties, one of which must be present.

In your schema, totalTag defines in what tag to look for the total number of records. If you don't want to define it in the xml results and instead you want to provide it manually through script when the grid is created, you either set a property totalCount on your DataModel or override getTotalRowCount to return whatever you want.

Example schema from the new forums:


dm = new YAHOO.ext.grid.XMLDataModel({
tagName: 'Topic',
totalTag: 'TotalCount',
id: 'id',
fields: ['title', 'author', 'totalPosts', 'lastPost', 'lastPoster']
});


You could also do it manually:



dm = new YAHOO.ext.grid.XMLDataModel({
tagName: 'Topic',
id: 'id',
fields: ['title', 'author', 'totalPosts', 'lastPost', 'lastPoster']
});
dm.getTotalRowCount = function(){
return 500;
}


Jack

Animal
26 Sep 2006, 1:32 AM
I'm, not using XML (although I could). I'm using DWR to effectively call


Object[][] getBlock(int startRow, int blockSize)

on the stored QueryLister object.

Which returns the specified block of data for the grid.

I could write a servlet which could accept the required parameters, call the QueryLister's methods and return XML.

The Grid is not showing the proper page count even though I've implements getTotalRowCount() to return the full result of the SELECT COUNT(*) query.

Also, the data is not being displayed divided into columns. The headers are, but the data in each row just flow into each other, and do not move when the column is resized.

jack.slocum
26 Sep 2006, 1:52 AM
Also, the data is not being displayed divided into columns. The headers are, but the data in each row just flow into each other, and do not move when the column is resized.

Sounds like an issue with the CSS. Did you put in to new grid.css file from the resources directory? Also, do you have your columns defined (if using multiple grids in same page)?

Your best bet is going to be to extend LoadableDataModel and override the load() (and maybe loadPage, loadData) method to get the data from DWR instead of XHR. That would give you the standard LoadableDataModel interface for paging but will actually load from DWR.

The Grid is not showing the proper page count even though I've implements getTotalRowCount() to return the full result of the SELECT COUNT(*) query.

The page count code is pretty simple. After checking to make sure neither is 0, it has this code:


return Math.ceil(this.getTotalRowCount()/this.getPageSize());


There's really nothing that can bomb in that code. Are you sure both those are returning the correct values?

Animal
26 Sep 2006, 3:58 AM
It's definitely returning the correct counts, I've just called the methods usnig Firebug.

The load() method takes a URL. It assumes direct use of the XHR.

Perhaps it should be abstracted to a higher level?

I'm currently overriding loadPage instead which just receives details about which page to load. I use these to calculate the block start and block size to call the DWR function.

The initial load is not being called unless I call it in the DataModel constructor.

Looking at the JSONDataModel, that doesn't do an initial load. How can I make the grid load itself?

It could be something to do with the getTotalRowCount() problem.

If I press the next page button, the page count flips to the correct value of 6, and I get page 2. Prev page then gets page 1.

As for the data all being crunched up. The ygrid-col-n spans were being set to position:static

When I added the rule



.ygrid-col {
position:absolute;
}


to my stylesheet, it looks good.

Animal
26 Sep 2006, 5:05 AM
When the text overflows the column width, it's displayed rather than truncated. I have 29 columns (Some are bound to be hidden by the user as not useful, but by default, all are displayed initially)

It autosizes these to fit the available width rather than to contain the data.

And the data is shown overlaying other data. The headers behave well and truncate.

Animal
26 Sep 2006, 6:44 AM
Autosize is persistently truncating column widths to fit the total available width rather than sizing them exactly as a double click on the resize handle would.

Also, I'm storing the row id as element 0 in each row.

This means that the default getValueAt(row, col) method returns the row id as the data at column zero. Surely this shouldn't happen? I've overriden it to add 1 to the column.

It seems to have an extra blank column on the end though! How is it getting the column count?

Animal
26 Sep 2006, 7:05 AM
Here is my DWRDataModel. It is passed the ID of the object stored in the HTTP session that the DWR "RemoteEntityLister" delegates the calls to.

Am I doing it right, returning the info correctly, setting internal properties correctly, and firing events correctly. Because I can't get it to see the correct number of pages until I've pressed pre/next page once!



YAHOO.ext.grid.AspicioDataModel = function(listerId)
{
YAHOO.ext.grid.AspicioDataModel.superclass.constructor.call(this);
this.listerId = listerId;
this.remoteSort = true;
this.pageSize = 40;
this.init();
};

YAHOO.extend(YAHOO.ext.grid.AspicioDataModel, YAHOO.ext.grid.LoadableDataModel);

/**
Is all I need at this stage just the numnber of rows???
*/
YAHOO.ext.grid.AspicioDataModel.prototype.init = function()
{
var me = this;

// DWR calls are asynch by default. We can't return until this.totalCount has been returned
DWREngine.setAsync(false);

// Return value from DWR calls are passed as a parameter to a callback function which is
// a final, additional parameter to the call.
RemoteEntityLister.getRowCount(this.listerId, function(c)
{
me.totalCount = c;
});
DWREngine.setAsync(true);
};

YAHOO.ext.grid.AspicioDataModel.prototype.loadPage = function(pageNum, callback, keepExisting)
{
this.fireEvent('beforeload');
var me = this;
RemoteEntityLister.getBlock(this.listerId, (pageNum - 1) * this.pageSize, this.pageSize, function(rowData)
{
try
{
if (keepExisting !== true)
{
me.removeAll();
}
me.addRows(rowData);
me.loadedPage = pageNum;
if (typeof callback == 'function')
{
callback(me, true);
}
me.fireLoadEvent();
}
catch(e)
{
me.fireLoadException(e, null);
if(typeof callback == 'function')
{
callback(me, false);
}
}
});
};

/**
* Returns the column data for the specified row.
* @return {Array}
*/
YAHOO.ext.grid.DefaultDataModel.prototype.getRow = function(rowIndex){
return this.data[rowIndex].slice(1); // Correct for the presence of teh row ID
};

/**
* Returns the value at the specified data position
* @param {Number} rowIndex
* @param {Number} colIndex
* @return {Object}
*/
YAHOO.ext.grid.DefaultDataModel.prototype.getValueAt = function(rowIndex, colIndex){
return this.data[rowIndex][colIndex + 1]; // Correct for the presence of teh row ID
};

/**
* Sets the specified value at the specified data position
* @param {Object} value The new value
* @param {Number} rowIndex
* @param {Number} colIndex
*/
YAHOO.ext.grid.DefaultDataModel.prototype.setValueAt = function(value, rowIndex, colIndex){
this.data[rowIndex][colIndex + 1] = value; // Correct for the presence of teh row ID
this.fireCellUpdated(rowIndex, colIndex + 1); // Correct for the presence of teh row ID
};

YAHOO.ext.grid.AspicioDataModel.prototype.isCellEditable = function(rowIndex, colIndex)
{
return false;
};

/**
* Interface method - Re-creates the table sorted on the specified column.
* @param {YAHOO.ext.grid.AbstractColumnModel} columnModel The column model
* @param {Number} col The column index
* @param {String} direction "ASC" or "DESC"
* @return {Boolean}
*/
YAHOO.ext.grid.AspicioDataModel.prototype.sort = function(columnModel, col, direction)
{
this.columnModel = columnModel;
this.sortColumn = col;
this.sortDir = direction;
var me = this;

// The order by column number is 1 based in SQL/HQL. Also, the query implicitly includes the uuid.
RemoteEntityLister.orderBy(this.listerId, col + 2, direction, 0, this.pageSize, function(data)
{
// Go back to the top
me.view.getBodyTable().parentNode.scrollTop = 0;

me.removeAll();
me.addRows(data);
me.loadedPage = 1;
});
};

jack.slocum
26 Sep 2006, 3:11 PM
The code looks fine. Two things though:

1. When is init() called? The way you have it set up it would have to be called before loadPage is ever called. It seems like it isn't being called first if the value is incorrect until you go to next/prev pages.

or

The first call to loadPage should come after the grid has been rendered to make sure the PageGridView is listening for the load event and can update it's paging values.

This code seems odd for a data model:



me.view.getBodyTable().parentNode.scrollTop = 0;


Where is view coming from? Are you setting on the model manually? Having your data model reference the UI isn't great practice, a better solution would be for your sort method to fire the rowssorted event (which you want to do anyway to update the header state) with noRefresh true (so the view doesn't re-render the rows). In your code you listen for this event and also scroll to the top. This way your model isn't tied to the view directly.

You could also do something like this:


YAHOO.ext.grid.GridView.prototype.handleSort = function(dataModel, sortColumnIndex, sortDir, noRefresh){
this.grid.selModel.syncSelectionsToIds();
if(!noRefresh){
this.updateRows(dataModel, 0, dataModel.getRowCount()-1);
}
this.updateHeaderSortState();
this.wrap.scrollTop = this.wrap.scrollLeft = 0; // scroll to top left
}

Animal
27 Sep 2006, 1:02 AM
Thanks for that, I've removed the reference to the view from the model (it was being poked in by my Grid) and overriden handleSort as you suggested.

Not sure about the init() call.

I have



YAHOO.ext.grid.AspicioDataModel = function(listerId)
{
YAHOO.ext.grid.AspicioDataModel.superclass.constructor.call(this);
this.listerId = listerId;
this.remoteSort = true;
this.pageSize = 40;
this.init();
};

YAHOO.extend(YAHOO.ext.grid.AspicioDataModel, YAHOO.ext.grid.LoadableDataModel);

/**
Set the model up to initially grab a "window" of the first 40 rows of data.
*/
YAHOO.ext.grid.AspicioDataModel.prototype.init = function()
{
var me = this;

// DWR calls are asynch by default. We can't return until this.totalCount has been returned
DWREngine.setAsync(false);

// Return value from DWR calls are passed as a parameter to a callback function which is
// a final, additional parameter to the call.
RemoteEntityLister.getRowCount(this.listerId, function(c)
{
me.totalCount = c;
});
DWREngine.setAsync(true);
};



So init() is being called from the constructor, and finds the size of the dataset.

I have put an alert() in, and the model correctly returns 6 from getTotalPages() before it's passed into the constructor of the



this.columnModel = new YAHOO.ext.grid.AspicioColumnModel(this.listerId);
this.dataModel = new YAHOO.ext.grid.AspicioDataModel(this.listerId);
alert("Created data model with " + this.dataModel.getTotalPages() + " pages");
this.grid = new YAHOO.ext.grid.Grid(this.container, this.dataModel, this.columnModel, new YAHOO.ext.grid.SingleSelectionModel());
this.grid.trackMouseOver = true;
this.grid.autoSizeColumns = true;
this.grid.minColumnWidth = 50;
this.grid.maxRowsToMeasure = 40;
this.grid.render();


It displays the message that there are 6 pages. But the initial display of the Grid shows no data, and the toolbar says "Page 1 of 1" (But the next/prev/first/last buttons are enabled, so it can then jump to page 2).

How do I force it to load page 1 on construction?

jack.slocum
27 Sep 2006, 2:37 PM
The buttons being enabled before any data is loaded is a bug. I have corrected it and it will be in .32.1.

After calling render(), add this line:

this.dataModel.loadPage(1);

This way it loads the first page *after* the PagedGridView is listening will update it's paging state.

I have fixed this requirement in .32.1, and on render the PagedGridView internally fires onPageLoaded and renders with the correct values from the dataModel if ithey are already available. This means loadPage(1) could be called anywhere in the script instead of having to be after the render() call.

Jack

Animal
27 Sep 2006, 10:58 PM
AHAH!

Got it. It's working beautifully now.

I just needed to



grid.loadPage(1);


After constructing and rendering the grid.

I now have the remote dataset being called up through DWR calls. It's very fast. Remote sorting re-issues the query sorted by the column, column resizes are relayed to the server through DWR as are column hide/show operations.

There's a YAHOO.widget.ContextMenu triggered on the grid.hwrap.dom (Perhaps that could be exposed in a more future-proof way?

Column move operations will be too.

The server maintains a List<ListColumn> the ListColumn object is a bean which is an analog of the DefaultColumnModel's config object. DWR converts beans easily to javascript objects who's properties match the properties of the bean. So i can just retrieve the column's config in a single call.

jack.slocum
27 Sep 2006, 11:10 PM
There's a YAHOO.widget.ContextMenu triggered on the grid.hwrap.dom (Perhaps that could be exposed in a more future-proof way?

You could use a standard YAHOO.widget.Menu + show() and rowcontextmenu or headercontextmenu events.

Your setup sounds sweet. Any chance of making it viewable?

Animal
27 Sep 2006, 11:37 PM
Unfortunately, I can't expose this because we don't have Java 5.0, JBoss or MySQL on the external servers. I'll have a talk with our network/security guru to see if there's a way I can expose port 8080 on my machine to the outside world.

If anyone is interested in hiding/showing columns, here is a method which creates that menu:

It's in the context of a wrapper object which encapsulates an HQL query, and displays the results in a grid, so this refers to that wrapper object. It has previously created this.grid, this.columnModel and this.dataModel.



createColumnMenu: function()
{
// Create a context menu triggered on the header
this.columnMenu = new YAHOO.widget.ContextMenu(this.listerId + "ContextMenu",
{
trigger:this.grid.getView().hwrap.dom,
zIndex:32767,
xy:[0,0]
});

// First item in column menu autosizes all columns.
// The server must be notified of each new column size.
var itemCfg = {
text:"Autosize columns",
url:"#"
};
var item = new YAHOO.widget.ContextMenuItem("Autosize columns", itemCfg);
item.clickEvent.subscribe(function()
{
this.grid.getView().autoSizeColumns();

// All column resize events are relayed to the server
// batchnig them queues the calls up into one HTTP request
DWREngine.beginBatch();
for (var i = 0; i < this.columnModel.getColumnCount(); i++)
{
RemoteEntityLister.setColumnWidth(this.listerId, i, this.columnModel.getColumnWidth(i), function callback(){});
}
DWREngine.endBatch();
}, this, true);
this.columnMenu.addItem(item);

// Loop through all columns:
// Autosize those with width:-1
// Add show/hide ContextMenuItem
for (var i = 0; i < this.columnModel.getColumnCount(); i++)
{
// If the width was passed from the server as -1, calculate it.
var columnWidth = this.columnModel.getColumnWidth(i);
if (columnWidth == -1)
{
this.grid.getView().autoSizeColumn(i);
}

// Create a show/hide ContextMenuItem for the column
var colHeader = this.columnModel.getColumnHeader(i);
itemCfg = {
text:colHeader,
columnModel:this.columnModel,
listerId:this.listerId,
columnIndex:i,
checked:!this.columnModel.isHidden(i),
url:"#"
};
item = new YAHOO.widget.ContextMenuItem(colHeader, itemCfg);
itemCfg.item = item;
item.clickEvent.subscribe(function()
{
var newHideFlag = this.checked;
this.checked = !newHideFlag;
this.columnModel.setHidden(this.columnIndex, newHideFlag);
this.item.cfg.setProperty("checked", this.checked);

// Relay show/hide operations to the server
RemoteEntityLister.hideColumn(this.listerId, this.columnIndex, function callback(){});
}, itemCfg, true);
this.columnMenu.addItem(item);
}
this.columnMenu.render(document.body);
},


You could make that available in the standard Grid if you like. Have an extra config parameter to set before the render operation:


grid.enableHeaderContextMenu = true;

To enable that. Users who wanted to relay the events to the server could subscribe to the columnresize event, or a new columnhidden/columnshown event.

jack.slocum
28 Sep 2006, 9:28 AM
I am doing it slightly different (1 menu shared) but this should be in the next release. I also added Sort Ascending and Sort Decending menu items.

Animal
28 Sep 2006, 10:56 AM
I'll switch to using your menu then when that version is released...

Can you do my job for me? :lol: :lol: 8)

BTW, do you have a schedule for being able to load a large, remote dataset by simply scrolling the grid and having it load new data "just in time"?

jack.slocum
28 Sep 2006, 11:33 AM
Not yet. I have one last example to post (the code is written, I just have to write the article) and then I can get back to work. It should make it into .33 but I can't commit on it yet. It's going to require a bit of work because I want to do it right not create empty rows for every possible record (like Rico). So if you wanted to browse a dataset of say 40,000 records (like my demo DB of city/state/zip) it would have to create 40,000 SPAN table rows. That's not gonna work! :)

The way I want to do it is to have sparse row population positioned absolute. This way you can have as many records as you want and the grid will always load the same speed (fast). Unfortunately sparse rows means I will have to keep track of them manually and I can no longer use bwrap.childNodes. That's probably not a bad thing though since I will have cached dom elements and the browser won't have to do DOM lookups.

kovtik
12 Oct 2006, 4:49 AM
Jack, as I understood, now there is no a possibility to load a data "on the fly" during grid scrolling? If there is, could you provide me a simple example?

kovtik
16 Oct 2006, 1:02 AM
Jack, sorry for disturbing you, but I worry that you didn't read my question.

jack.slocum
16 Oct 2006, 3:04 AM
kovtik, it hasn't been implemented yet. The events are there though and it would be very easy to implement it similar to Google Reader until the built-in solution is in place.

kovtik
16 Oct 2006, 8:28 AM
kovtik, it hasn't been implemented yet. The events are there though and it would be very easy to implement it similar to Google Reader until the built-in solution is in place.Thank you for your answer.