CGridView sorting causes multiple ajax calls

Hi,

I have a sub page, which is loaded via ajax, and there’s a grid view on that fragment. When the entire subpage reloads via ajax, the javascript yiiGridview plugin rebinds all of sorting and paging links:




$(document).on('click', settings.updateSelector, function () {

	$('#' + id).yiiGridView('update', {url: $(this).attr('href')});

	return false;

});



This behavior causes multiple ajax request at sorting. This initialization should run only once per normal page load.

Is there any clean workaround which solve this issue?

These resources should help:

http://www.yiiframework.com/doc/api/1.1/CController#renderPartial-detail

http://www.yiiframework.com/extension/nlsclientscript/

I think you thought serving the ajax request with renderPartial and set $processOutput to false. But that won’t work. The problem is that all gridview functionality binded to the current DOM element except paging and sorting functions, so when you reinsert the same grid you have to initialize it, otherwise you can’t use the built in ajax update, and when you do that, that link’s javascript click event will be binded to the document one more time, because that events didn’t disappear when you removed the grid from the DOM.

UPDATE

Ok, I’ve just checked, the code again. There’s no functionality or settings what is bounded to the current DOM element. In this case, the problem is little tricky, because every grid should be initialized only one time. This can be solved by set processOutput false after the first time, which is not good, if you have an activeform in the view, you have to intialize it, and how do you know on server side, that you will have to initialize the gridview or not?

To solve the above mentioned "if you have activeform in the view" you can put the gridview in a separate view.

As for how to know on server side when to initialize it:

if the grid can be rendered with the initial page (no ajax) so that only updates (sorting,filtering) are on ajax, you can use isAjaxRequest - http://www.yiiframework.com/doc/api/1.1/CHttpRequest#getIsAjaxRequest-detail

on the other side if the grid should be rendered by ajax even the first time, you can always have a custom JS logic that would send a special parameter just the first time to the action and there you can check for that parameter and decide if to render or renderpartial

Yes, that view always requested via ajax, so the first option is not good.

The second can solve the problem, but I think this is not a ‘clean’ solution, and not general. If I could do that, I modify the gridview plugin, because I think that is the right place, but that is a framework provided code.

I have a complete admin system depending on ajax calls and gridviews, so I should find the right place.

I don’t see why would that be “not clean” it’s not a hack or anything like that…

if you have a good solution to be implemented in the core you can suggest it… just don’t come with the undelegate (off) before delegate (on) solution as this has already been extensively discussed and is not proper.

Not a hack, but I think not the right place, this solution hide the problem not solve it.

Yes, my first idea was the ‘off’ method, I think I have to check up why not proper.

Thank you…

Ok, I have a solution, which works for me.

The idea was:

We bind events when the the current id is not present in the settings array. And in the event callbacks, we use the settings array instead of a local copy, because in this case we can overwrite grid settings in a future request.

I can’t attache JS files (“Error You aren’t permitted to upload this kind of file”).

How can I share it?

Here’s the diff (line numbers are from a large concatenated file).




# This patch file was generated by NetBeans IDE

# It uses platform neutral UTF-8 encoding and \n newlines.

--- Remotely Modified (Based On HEAD)

+++ Locally Modified (Based On LOCAL)

@@ -3304,7 +3304,8 @@

 			return this.each(function () {

 				var $grid = $(this),

 					id = $grid.attr('id'),

-					inputSelector = '#' + id + ' .' + settings.filterClass + ' input, ' + '#' + id + ' .' + settings.filterClass + ' select';

+					inputSelector = '#' + id + ' .' + settings.filterClass + ' input, ' + '#' + id + ' .' + settings.filterClass + ' select',

+					initialized = gridSettings[id];

 

 				settings.tableClass = settings.tableClass.replace(/\s+/g, '.');

 				if (settings.updateSelector === undefined) {

@@ -3313,8 +3314,10 @@

 

 				gridSettings[id] = settings;

 

+				if(!initialized) {

+

 				if (settings.ajaxUpdate.length > 0) {

-					$(document).on('click', settings.updateSelector, function () {

+						$(document).on('click', gridSettings[id].updateSelector, function () {

 						$('#' + id).yiiGridView('update', {url: $(this).attr('href')});

 						return false;

 					});

@@ -3322,15 +3325,15 @@

 

 				$(document).on('change', inputSelector, function () {

 					var data = $(inputSelector).serialize();

-					if (settings.pageVar !== undefined) {

-						data += '&' + settings.pageVar + '=1';

+						if (gridSettings[id].pageVar !== undefined) {

+							data += '&' + gridSettings[id].pageVar + '=1';

 					}

 					$('#' + id).yiiGridView('update', {data: data});

 				});

 

-				if (settings.selectableRows > 0) {

+					if (gridSettings[id].selectableRows > 0) {

 					selectCheckedRows(this.id);

-					$(document).on('click', '#' + id + ' .' + settings.tableClass + ' > tbody > tr', function (e) {

+						$(document).on('click', '#' + id + ' .' + gridSettings[id].tableClass + ' > tbody > tr', function (e) {

 						var $currentGrid, $row, isRowSelected, $checks,

 							$target = $(e.target);

 

@@ -3343,23 +3346,23 @@

 						$checks = $('input.select-on-check', $currentGrid);

 						isRowSelected = $row.toggleClass('selected').hasClass('selected');

 

-						if (settings.selectableRows === 1) {

+							if (gridSettings[id].selectableRows === 1) {

 							$row.siblings().removeClass('selected');

 							$checks.prop('checked', false);

 						}

 						$('input.select-on-check', $row).prop('checked', isRowSelected);

 						$("input.select-on-check-all", $currentGrid).prop('checked', $checks.length === $checks.filter(':checked').length);

 

-						if (settings.selectionChanged !== undefined) {

-							settings.selectionChanged(id);

+							if (gridSettings[id].selectionChanged !== undefined) {

+								gridSettings[id].selectionChanged(id);

 						}

 					});

-					if (settings.selectableRows > 1) {

+						if (gridSettings[id].selectableRows > 1) {

 						$(document).on('click', '#' + id + ' .select-on-check-all', function () {

 							var $currentGrid = $('#' + id),

 								$checks = $('input.select-on-check', $currentGrid),

 								$checksAll = $('input.select-on-check-all', $currentGrid),

-								$rows = $currentGrid.children('.' + settings.tableClass).children('tbody').children();

+									$rows = $currentGrid.children('.' + gridSettings[id].tableClass).children('tbody').children();

 							if (this.checked) {

 								$rows.addClass('selected');

 								$checks.prop('checked', true);

@@ -3369,14 +3372,15 @@

 								$checks.prop('checked', false);

 								$checksAll.prop('checked', false);

 							}

-							if (settings.selectionChanged !== undefined) {

-								settings.selectionChanged(id);

+								if (gridSettings[id].selectionChanged !== undefined) {

+									gridSettings[id].selectionChanged(id);

 							}

 						});

 					}

 				} else {

 					$(document).on('click', '#' + id + ' .select-on-check', false);

 				}

+				}

\ No newline at end of file

 			});

 		},

 




Interesting concept… would it make sense to not process at all the whole file if the ID is set ?

How do you mean "the whole file"?

In my version this will happen when you reinitialize a gridview:




init: function (options) {

			var settings = $.extend({

					ajaxUpdate: [],

					ajaxVar: 'ajax',

					pagerClass: 'pager',

					loadingClass: 'loading',

					filterClass: 'filters',

					tableClass: 'items',

					selectableRows: 1

					// updateSelector: '#id .pager a, '#id .grid thead th a',

					// beforeAjaxUpdate: function (id) {},

					// afterAjaxUpdate: function (id, data) {},

					// selectionChanged: function (id) {},

					// url: 'ajax request URL'

				}, options || {});


			return this.each(function () {

				var $grid = $(this),

					id = $grid.attr('id'),

					inputSelector = '#' + id + ' .' + settings.filterClass + ' input, ' + '#' + id + ' .' + settings.filterClass + ' select',

					initialized = gridSettings[id];


				settings.tableClass = settings.tableClass.replace(/\s+/g, '.');

				if (settings.updateSelector === undefined) {

					settings.updateSelector = '#' + id + ' .' + settings.pagerClass.replace(/\s+/g, '.') + ' a, #' + id + ' .' + settings.tableClass + ' thead th a';

				}


				gridSettings[id] = settings;


				

			});

		},



As you can see only some settings initialization and settings overwrite will happen, when you call $(’#some_id’).yiiGridView({…options…}), the plugin registering is necessary as the grid can be new in the DOM.

I was thinking on something like


if(initialized) return;

In that context basically




if(initialized) return;



and




if(!initialized) {

 // do initialization

} // else nop



is the same.

Return at any other point of the init method is wrong, as the init function does 2 thing: add/update current settings, and bind events. In my version we don’t bind events when the grid was initialized before. The init method should be invoked, as only the plugin itself can decide what to do in different situation, for example overwrite settings.

Why assigning again the settings to gridSettings[id] and why change all the “settings” with “gridSettings[id]”, aren’t they the same at all times?

The settings of one grid should not change between ajax calls or do you have a different use case?

If we know that grid settings are not changed between ajax calls, then yes, all the $(’#some_id’).yiiGridView({…}) initialization is redundant and unnecessary. In my case the settings won’t change.

If this is a by design decision, we should examine the possibility of solving this issue at server side by not registering the init script in the widget, but at first this doesn’t seem a good solution. What do you think?

Makes no sense to change any settings between sorting/filtering/pagination calls…

As for the server side that would be the best solution, but I still got no idea on how to do it… i.e. how to detect that a grid has already been initialized…

that’s why your idea seems nice to me.

No, the problematic ajax calls are those ones which aren’t triggered by the grid. The grid generated calls (paging/sorting/fitering/updating) don’t reinitialize the grid itself, so the settings can’t change as you said.

But other calls, for example there are two different menu which calls the same action but with different params, can cause settings change. Theoretically.

In this case we don’t have to decide on server side.