How to maintain criteria filters when sorting with CGridView

Hi All,

I have a page where I show a “report” of data using CGridView. The user can filter the data through using some dropdown fields at the top of the report. (Filters include a date range, for example.) When the report filter(s) are set and the form is submitted, I grab the filter values, load them into a CDbCriteria object, then use the criteria object to make a CActiveDataProvider object. I also use a CSort object to define which columns are sortable. However, whenever I click on a column header to sort by that column, my criteria filters are lost, and the report returns to its default state without the filters values added to the criteria. Here’s an example of my code:




//* controller

class MyController extends Controller

{

    public function actionReport()

    {

        //* set default filter values

        $startDate = date('Y-m-01',strtotime('-1 month'));

		$endDate = date('Y-m-d');

		

        //* get filter values from form post

        if(isset($_POST['filter']))

		{

			$startDate = $filters['start_date'];

			$endDate = $filters['end_date'];

        }

        

        //* set up criteria object based on filter values

        $criteria = new CDbCriteria;

        $criteria->addCondition('collection_date BETWEEN :startdate AND :enddate');

        $criteria->params = array(

            ':startdate' => $startDate,

            ':enddate' => $endDate,

        );


        //* set sort options

        $sort = new CSort;

		$sort->attributes = array(

			'field_one',

            'field_two'.

		);

		$sort->defaultOrder = 'field_one DESC';

		$sort->multiSort = true;


        //* create data provider

        $dataProvider=new CActiveDataProvider('Report', array(

			'criteria'=>$criteria,

			'sort'=>$sort,

			'pagination'=>array(

				'pageSize'=>50,

			),

		));

		

        //* render view

        $this->render('report',array(

            'dataProvider' => $dataProvider,

        ));

    }

}


//* view

....

<?php

$this->widget('zii.widgets.grid.CGridView', array(

	'dataProvider'=>$dataProvider,

	'columns'=>array(

        'field_one',

        'field_two',

    )

));

?>



I think that this has something to do with the fact that my action code only applies the user-defined filter values to the criteria if the form was posted. In the case of a sort, the action is run via ajax and without the form being posted. I’m just not sure of the best way to go about fixing the issue. Can I append filtered parameters to my sort column headers so that the filter data gets passed through with the ajax sort call? What’s the best way to solve this problem – I’m sure I’m not the first to need to figure this out, but I haven’t found anything in the forums yet. Thanks in advance for any assistance!

With no tips as to the "right" way to maintain filter values when sorting (or paging) a CGridView component yet, I started taking a stab at one possible solution. Since the CGridView column header calls the original controller and action, I added code to my action to place my criteria object into the session when the report is run initially. Then, added an "if(Yii::app()->request->isAjaxRequest)" section to grab the original criteria object from the session, apply the new sort value, then render and echo the CGridView component. Code looks like this:




//* ....within action

if(Yii::app()->request->IsAjaxRequest)

{

	$criteria = unserialize(Yii::app()->session['criteria']);

	$sort = unserialize(Yii::app()->session['sort']);

	$newOrder = $_GET['sort'];

	$sort->defaultOrder = $newOrder;

	$dataProvider=new CActiveDataProvider('Collection', array(

		'criteria'=>$criteria,

		'sort'=>$sort,

		'pagination'=>array(

			'pageSize'=>50,

		),

	)); 

	echo $this->renderPartial('_collectionsdetail', array(

			'data'=>$dataProvider,

			'userorgtype'=>Yii::app()->session['userorgtypeid'],

			'charityid'=>Yii::app()->session['charityid'],

			'marketid'=>Yii::app()->session['marketid'],

			'storetypeid'=>Yii::app()->session['storetypeid'],

		),true,true

	);

}



When I run the report all works fine, but as soon as I try to sort or page the CGridView disappears. Even if I just place an “echo(‘hello world’);” in my code above in place of rendering the view I still get nothing. I’m really hitting a wall here. Any help would be greatly appreciated.

Hi. I’m having the same issue.

I’ve found the “problem” is in the javascript behavior. When it does the ajax requests used in pagination/sorting, it only sends back the parameters in the URL, and, as you mentioned, it doesn’t send back any fields posted in your form (using method POST).

A quick & really dirty fix would be to set your form to use the GET method, which has many nasty side effects.

A workaround I found was to override the javascript "update" function in the gridview, so it uses "custom" data fields and sends it using the POST method so your $_POST still works.

This is my JS code, it still needs optimizing but hopefully it helps




<script type="text/javascript">

/*<![CDATA[*/

    $.fn.yiiGridView.originalUpdate = $.fn.yiiGridView.update;

    $.fn.yiiGridView.update = function(id, options) {

        var data = {

            '<?php echo CHtml::activeName($sform, 'sname') ?>': $('#sform #<?php echo CHtml::activeId($sform, 'sname') ?>').val()

        };

        options = $.extend({

            data:data,

            type:'POST'

        }, options || {});

        $.fn.yiiGridView.originalUpdate(id, options);

    };

/*]]>*/

</script>



Here, $sform is my search form model which only has a “sname” attribute. Ideally you should serialize your form and put its name/value pairs in the “data” object. I’m manually using CHtml::activeName and activeId to replicate the form field names being submitted as if they were submitted from a normal form (you shouldn’t need this if you serialize the form, I just didn’t find a proper way yet).

Also, I’m setting the request method (type) to Post, as it defaults to Get.

Note that this approach only works if you use Ajax to refresh the grid, since the regular non-Ajax way doesn’t use the Update method and relies only on URL values.

I’m not very experienced with jQuery (or Yii) so I hope someone helps with a better code, this is just a start.

It also may be worth mentioning that, in this case, I’m creating a hidden form with its (hidden) fields reflecting the contents of the submitted search form, I do this so the search fields are consistent between page/sort changes, otherwise you may change a search value without submitting the form and the resulting data would change when you change the page/sort, which is usually an undesired behavior.

Hope this helps. Sorry for bad/funny english.

Thanks for the response, Mwg. I finally solved my problem. Like you, I don’t know if it was the best method or not, but it works… I realized that when CGridView was calling my action, my filter vars weren’t being assigned to the criteria object because the code was assigning these values only when $_POST[‘filter’] was set. To solve the problem, I stored my criteria and sort objects in the session (as described in my code above) each time the action was run. Then, if the action was hit by an ajax request (presumably from the CGridView sort or page call), I pulled the criteria and sort vars out of the session, updated the sort object if a sort param was in the $_GET scope, and allowed my view to render with those vars. This was what I was trying to do above, except that in my example above I was rendering the CGridView manually. I guess the CGridView ajax call is expecting the “normal” render call to be made. I’m still not sure why this is the case, but as long as my problem is solved maybe I don’t need to know right now. =)

It would be great if there were a "filter" object that could be passed to the CGridView so that key/value pairs from the filter object would get appended to all sorting and paging queries. (Or maybe just have the CGridView component preserve the criteria object from the CDataProvider in the first place.)

Glad to see you solved your problem.

I also found out the problem you had about partial render not being displayed could be a bug in the javascript code, if you still want to use partial renders, a quick workaround would be to enclose your entire view content inside a <div>.

I’ve posted more detail in the bugs section of the forum

good works and useful :lol: