Yii 1.1: CGridView, CListView and CActiveDataProvider

36 followers

CGridView (or CListView) together with CActiveDataProvider is a very powerful combination of the built-in tools of Yii. But how do they work together to accomplish their fantastic functions? And what are you expected to do to use them and to customize their behaviors? This article explains the very basics of them.

Introduction

Using CGridView (or CListView) with CActiveDataProvider, you can very easily implement a complex page that can show a number of items with the abilities of:

  • filtering (searching)
  • sorting
  • and paginating

It would be a great loss of your time if you would try to go without these wonderful tools.

You can see the working examples of them in gii-generated CRUD pages.

  • "index" page uses CListView with CActiveDataProvider.
  • "admin" page uses CGridView with CActiveDataProvider.
  • "admin" page uses the search method of the model to get the CActiveDataProvider.

This article tries to explain how they work together to achieve their functions like filtering, sorting and paginating, and what you are expected to do to customize their behaviors.

I assume that the reader is relatively new to Yii. But the more advanced readers may find this article interesting.

CActiveDataProvider

CActiveDataProvider is a kind of query executor that CGridView or CListView uses to get a list of items. It returns an array of AR objects retrieved from the database according to the specified criteria.

Creating CActiveDataProvider is like using CActiveRecord::findAll() in many ways, because in both of them you will usually use CDbCriteria to specify the conditions. But there are some important differences between them.

  • CActiveRecord::findAll()
    • It is you (the programmer) that executes the query by calling this method.
    • You may specify order of the criteria directly.
    • You may specify offset and limit of the criteria directly.
  • CActiveDataProvider
    • It is CGridView or CListView that executes the query.
      • Usually when you use it for a CGridView or a CListView, you are not supposed to call CActiveDataProvider::getData() method directly to get the result.
    • You are not allowed to specify order in the criteria directly.
      • order should be set by CGridView or CListView using the sort property of the CActiveDataProvider.
      • You can optionally customize the sort property.
    • You are not allowed to specify offset and limit in the criteria directly.
      • offset and limit should be set by CGridView or CListView using the pagination property of the CActiveDataProvider.
      • You can optionally customize the pagination property.

The "search" method in the model

Gii should have created a method called search in your model code. It is a vital part of the code that you need when you want to use CGridView or CListView in your application. It returns an instance of CActiveDataProvider which is to be used by CGridView or CListView.

There are two common misunderstanding regarding this method:

  • It returns an instance of CActiveDataProvider.
    • It doesn't return such an array of AR objects as Model::findAll() does.
  • $this refers to a model instance that holds the search parameters.
    • It is not a model instance that has been retrieved from the database.
public function search()
{
    $criteria=new CDbCriteria;
    ...
    $criteria->compare('name', $this->name, true);
    $criteria->compare('address', $this->address, true);
    ...
    return new CActiveDataProvider(get_class($this), array(
        'criteria' => $criteria,
    ));
}

In the above, if $this->name is not empty, then a corresponding LIKE condition will be added to the WHERE clause. If it is empty, then no condition will be added. It is also the same with $this->address. And multiple conditions are merged using AND by default.

For example, when you call search with a model with all attributes set to empty, then it will return the data provider that searches for all the model instances without any conditions. And if you call it with a model whose name attribute set to 'John', then it will return the data provider that searches for all the model instances that has a name like 'John'. (See CDbCriteria::compare() for details.)

You may note that you can take this method as a skeleton or a template. You can freely customize it to satisfy your needs. Or you can also write the customized versions of it if you want.

"admin" page line by line

In order to understand how an instance of CActiveDataProvider is created and how it is used with CGridView, let's examine the gii-generated code of "admin" page line by line.

actionAdmin controller method

public function actionAdmin()
{
    $model = new MyModel('search');
    $model->unsetAttributes();  // clear any default values
    if (isset($_GET['MyModel'])) {
        $model->attributes = $_GET['MyModel'];
    }
    $this->render('admin', array(
        'model' => $model,
    ));
}

The code of actionAdmin is simple and straightforward.

In the first 2 lines, we are creating a model instance of 'MyModel' as a container of search parameters. We call unsetAttributes to ensure the initial search parameters are all empty.

And then we are checking the user input of $_GET['MyModel']. If the action has been called with $_GET['MyModel'], then we will do the massive assignment of the attributes from the user input. But when the page has been loaded for the first time, we will skip it because $_GET['MyModel'] should not be set yet.

And at last we will render the "admin" view passing $model to the view script. Remember that $model is a container of the search parameters.

admin.php view scripts

...
<div class="search-form" style="display:none">
<?php $this->renderPartial('_search',array(
    'model' => $model,
)); ?>
</div><!-- search-form -->
...
<?php
$this->widget('zii.widgets.grid.CGridView', array(
    'id' => 'my-model-grid',
    'dataProvider' => $model->search(),
    'filter' => $model,
    'columns' => array(
        'name',
        'address',
        ...
    ),
));
?>

_search.php partial view script

<div class="form">
<?php $form=$this->beginWidget('CActiveForm', array(
    'action' => Yii::app()->createUrl($this->route),
    'method' => 'get',
)); ?>
<div class="row">
<?php echo $form->label($model, 'name'); ?>
<?php echo $form->textField($model, 'name'); ?>
</div>
<div class="row">
<?php echo $form->label($model, 'address'); ?>
<?php echo $form->textField($model, 'address'); ?>
</div>
...
<div class="row buttons">
<?php echo CHtml::submitButton('Search'); ?>
</div>
<?php $this->endWidget(); ?>
</div><!-- form -->

The view scripts may be much longer, but I simplified them by showing only the relevant parts.

$model in the view scripts

In those view scripts, we use $model in 3 places.

  1. As the "advanced" search form, we create a CActiveForm using $model.
    • The search form is implemented using _search partial view.
    • The form will be submitted using get method.
    • It is initially hidden from the end user.
  2. We provide the grid with an instance of CActiveDataProvider by calling $model->search().
    • The data provider will tell the grid the total count of items when the grid will display the summary text.
    • Also the data provider will provide the grid with an array of AR objects when the grid will show the items one by one.
    • The content of the grid should vary according to the criteria of the data provider that we have made in the search method.
    • Initially the grid should display the items without any filterring, because $model should have the attributes all set to empty.
  3. We also set the filter property of the grid to $model.
    • This is for the inline search filter that is located between the header and the body of the grid table by default.
    • The inline filter will work almost the same as the "advanced" search form.

Now the rendering has been completed and the page will be sent to the user's browser.

Searching by the user

When the user has input some word and hit the return key, either in the "advanced" search form or in the inline filter, the page will sumbit the search parameters using get method.

Then, in the next cycle of the HTTP request, the actionAdmin controller method will get those search parameters in $_GET['MyModel'], and construct a CActiveDataProvider instance with the specified parameters to refresh the grid.

Advanced Topics

So far, it's the very basics of CGridView and CActiveDataProvider.

Although the "index" page and CListView have not been discussed, you may easily understand how to use CListView, because both CGridView and CListView have been extended from the same base class of CBaseListView and behave almost the same in many ways.

Now, let's see some advanced topics.

Disabling Ajax Updating

Before we are going any further, we would like to temporarily disable the ajax updating of the CGridView which is enabled by default. You can do it by setting the ajaxUpdate property to false like the following.

<?php
$this->widget('zii.widgets.grid.CGridView', array(
    'id' => 'my-model-grid',
    'dataProvider' => $model->search(),
    'filter' => $model,
    'ajaxUpdate' => false,  // This is it.
    'columns' => array(
        'name',
        'address',
        ...
    ),
));
?>

By this configuration, CGridView will begin to do all the jobs of refreshing its content in non-ajax mode. You will be able to see the parameters clearly in the query string part of the url that the grid calls for updating itself. This will greatly help you understand how the searching, sorting and paginating are performed using $_GET parameters. In other words, you will see the naked CGridView.

And disabling the ajax updating is also a very useful trick when you want to debug a page with a CGridView.

Sorting

Usually the sorting of CGridView and CListView is requested with a query string like MyModel_sort=attributeName or MyModel_sort=attributeName.desc.

We have no code at all to handle this request either in our controller or in our model. We don't check $_GET['MyModel_sort'] and we don't change the criteria of CActiveDataProvider to accomplish the sorting.

In fact, the checking and the handling of the sorting is done inside the CGridView or CListView code, using the sort property (an instance of CSort) of the CActiveDataProvider.

It's important that we can not (and should not) include order property in our criteria. It's reserved for the CSort of the CActiveDataProvider.

Instead, we can specify some properties of CSort to configure its bahaviors.

public function search()
{
    $criteria=new CDbCriteria;
    ...
    $criteria->compare('name', $this->name, true);
    $criteria->compare('address', $this->address, true);
    ...
    return new CActiveDataProvider(get_class($this), array(
        'criteria' => $criteria,
        'sort' => array(
            'defaultOrder' => 'name, address',
            'attributes' => array(
                'name' => array(
                    'asc' => 'name address',
                    'desc' => 'name desc, address',
                ),
                'address' => array(
                    'asc' => 'address, name',
                    'desc' => 'address desc, name',
                ),
                '*',
            ),
        ),
    ));
}

In the above, we specify defaultOrder and attributes properties of sort when we instantiate the CActiveDataProvider.

Look up the reference for details: CSort

Pagination

Usually the pagination of CGridView and CListView is requested with a query string like MyModel_page=N where N refers to the page number.

Just like the sorting mentioned above, we don't have much to do with it. We should let CGridView or CListView handle this request using the pagination property (an instance of CPagination) of the CActiveDataProvider. We can not (and should not) set offset and limit properties in our criteria. They are reserved for the CPagination of the CActiveDataProvider.

Instead, we can specify some properties of CPagination to configure its bahaviors.

public function search()
{
    $criteria=new CDbCriteria;
    ...
    $criteria->compare('name', $this->name, true);
    $criteria->compare('address', $this->address, true);
    ...
    return new CActiveDataProvider(get_class($this), array(
        'criteria' => $criteria,
        'pagination' => array(
            'pageSize' => 25,
        ),
    ));
}

In the above, we specify pageSize property of pagination when we instantiate the CActiveDataProvider.

Look up the reference for details: CPagination

Ajax Updating

By default, CGridView and CListView are ajax-enabled. Let's go back to the default by removing the setting of ajaxUpdate (or you may set it to null, because null is the default value).

The refreshing of the page caused by filtering (searching), sorting or paginating is done via an ajax call so that the end user will enjoy a smooth browsing through the listed items.

But we don't have any dedicated code either in our controller or in our model to handle this ajax request. How is it possible, then?

The answer is in the CGridView's built-in javascript 'jquery.yiigridview.js' which has a function called '$.fn.yiiGridView.update'. (For CListView, they are 'jquery.listview.js' and '$.fn.yiiListView.update' respectively.) It does all the tricks.

I can not explain it in details here, but the normal workflow of ajax call would be something like the following:

  1. The javascript catches the events that will trigger the refreshing of the page.
    • The submission of the search form.
    • The clicks on the pager buttons.
    • The clicks on the sorters (e.g. sortable header cells).
    • The changes in the inline filters.
  2. The javascript fires an ajax request.
    • Usually the current URL is used for ajax request.
    • All the necessary parameters are passed to the server using get method.
    • It will wait for a response in html format.
  3. The controller responds to the ajax request and acts in the same way as the normal request.
    • It renders the whole html of the page as usual.
    • (Actually, the controller doesn't have to render the whole page for the ajax request. You can optimize the controller code if you are performance conscious. See Comment #9696.)
  4. The javascript receives the response and updates only the widget.
    • It receives all the html code that the controller has created, but it will use only the part that renders the widget (grid or list). The rest of the html code is ignored.
    • It uses the id of the widget to distinguish the relevant part.

Usually you don't have to mind the details of ajax updating of CGridView and CListView. It will work like a charm without your interventions.

But baring in mind the basic workflow of the ajax updating, you will be able to cope with the possible problems when things get more complicated.

More to Read

CGridView, CListView and CActiveDataProvider have far much more to be discussed.

In fact, in order to use CGridView or CListView effectively, you will need a very wide range of knowledge because they have many relevant classes to cooperate.

But never be afraid. We have the Class Reference and it's an excellent resource to be referred to. The following is a list of links to the pages in the reference which we should read for the relevant topics regarding CGridView, CListView and CActiveDataProvider.

Tip: Look up the reference, before you google around in vain.

Total 20 comments

#16929 report it
softark at 2014/04/14 06:57am
RE: CListview pagination takes almost 20s

Hi @jcagentzero,

Are you using CActiveDataProvider for the list view? If so, then the performance issue can be boiled down to the performance of the SQL that the data provider creates. And usually CActiveDataProvider creates a decent SQL with proper offset and limit. So you would be better reconsider the design of the database table ... does it have proper indices for every columns that you may want to sort by?

#16926 report it
jcagentzero at 2014/04/14 04:38am
CListview pagination takes almost 20s

Hi @softark,

I know that you mentioned that we cannot do more about the clistview pagination, but I have a very large database and one of the tables has 100,000+ items. When clicking next and previous, it takes 4-5s which can be tolerated but when clicking last and first it takes 20s.

Do you have any idea?

Thank you in advance!

#16668 report it
VincentM at 2014/03/17 01:01pm
thanks a lot.

thanks a lot for this post!! It helps me to understand how all of this works. :):)

but I still have an error in my code ... I posted all my code here if you wanna try to help me : http://www.yiiframework.com/forum/index.php/topic/52488-how-to-have-a-grid-with-filtered-data/#entry242587

thx in advance and have a nice day :)

#16080 report it
newscloud at 2014/01/17 03:41am
Example of CGridView with CCheckboxColumn, Select All and an Action Button

Here's an example of CGridView with CCheckboxColumn, Select All and an Action Button: http://jeffreifman.com/yii/cgridview/

#15216 report it
softark at 2013/10/18 11:18am
re: Search form is initially hidden ?

I see. Sorry for your confusion.

The search form is not a part of CGridView. It's just a CActiveForm that submits search parameters. What you have to see about this form are:

  • it uses "GET" method
  • it shares the same model with the 'filter' property of the grid

The visibility of the form is not an important point here.

#15215 report it
tomvdp at 2013/10/18 10:37am
re: Search form is initially hidden ?

Thank you :-) I got confused because you explicitly mention the visibility of the search form, but then do not reference it anymore further in the document. Not having the admin.php code at hand, I thought that maybe the grid itself contained logic to somehow show/hide a search form. All cleared up now!

#15214 report it
softark at 2013/10/18 10:26am
re: Search form is initially hidden ?

The following lines in "admin.php" view file do the trick of toggling the search form visibility.

Yii::app()->clientScript->registerScript('search', "
$('.search-button').click(function(){
    $('.search-form').toggle();
    return false;
});
...
");

Probably you may want to learn a little about jQuery. Not so much, but a little. :)

#15212 report it
tomvdp at 2013/10/18 08:31am
Search form is initially hidden ?

I am confused about the style="display:none" of the search form. How and where will the visibility be toggled ?

#14054 report it
softark at 2013/07/15 06:52pm
RE: Errata report

Thank you. Fixed.

#14053 report it
Alex D. at 2013/07/15 04:37pm
Errata report

It seems like there's an error in the sample. In part

'sort' => array(
            'defaultOrder' => 'name address',

should be comma-sign between 'name address':

'sort' => array(
            'defaultOrder' => 'name, address',

otherwise you'll get an "Exception: 1064 You have an error in your SQL syntax"

#11996 report it
softark at 2013/02/19 06:23pm
ajaxUrl

You can specify "ajaxUrl" property. Please look it up in the reference.

#11988 report it
lxvi at 2013/02/19 04:38am
I want to use a different URL for the ajax request

Hello @softark,

In the section above called Ajax Updating, under workflow step 2 ("The javascript fires an ajax request") the first bullet point reads "Usually the current URL is used for [the] ajax request."

My question is how would I specify a different URL for the ajax request? The reason is that I have a CGridView in a _view that is sometimes rendered "normally" in a page's view, and other times it is imported into a page through another widget's ajax request, and in that second case, the ajax sent by the CGridView for filtering, sorting, etc is (incorrectly) going to the controller action that did the import rather than the controller action that renders the page, so I am forced to duplicate the CGridView handler code in multiple places. This is going to get even worse as I include the ability to import on more and more pages. It would be much nicer for me to be able to specify an independent URL that I can count on no matter who is rendering the CGridView's _view. I hope this makes sense.

#11760 report it
softark at 2013/02/01 11:15pm
RE: Sorting

@clapas

OK, I got it.

So you are passing a CSort object instance to the constructor of CActiveDataProvider.

$sort = new CSort();
$sort->defaultOrder = 't.name';
 
return new CActiveDataProvider('YourModel', array(
    'criteria' => $criteria,
//  'sort' => array(
//      'defaultOrder' => 't.name',
//  ),
    'sort' => $sort,
    'pagination' => array(
        'pageSize' => $page_size,
    ),
));

I confirmed that in this case just 'order' is used instead of 'YourModel_order'.

Thank you for your useful comment.

#11758 report it
softark at 2013/02/01 10:37pm
RE: Sorting

@clapas

Thank you for the comment.

By setting 'ajaxUpdate' to false, you will see the query string clearly. Are you sure you see just "sort" instead of "MyModelName_sort"? What I see is "MyModelName_sort" when I'm working with CActiveDataProvider.

It might be that you are using CArrayDataProvider or CSqlDataProvider, I guess.

[EDIT]My guess has been wrong. Please see the next comment.[/EDIT]

BTW, we usually don't have to know the query string parameter name. Just out of curiosity, What do you do with it?

#11752 report it
clapas at 2013/02/01 08:23am
Sorting

Thank you for this valuable wiki post.

After some testing, I realized the sentence

Usually the sorting of CGridView and CListView is requested with a query string like MyModel_sort=attributeName or MyModel_sort=attributeName.desc.

is not correct; at least it won't work for me. I need the query string to be sort=attributeName or sort=attributeName.desc in order for sorting to work, i.e., without the leading model name portion.

#10623 report it
softark at 2012/11/08 12:05pm
Thanks

@seb7

Yeah, you are right. Thanks for the correction.

#10609 report it
seb7 at 2012/11/07 12:37pm
code improvement for ajax & renderPartial (?)

I suppose it's better to use Yii::app()->request->isAjaxRequest instead of $_GET['ajax']. Because if the widget has a custom ajax variable, controller will fail recognizing the request as ajax. CListView::ajaxVar

if (Yii::app()->request->isAjaxRequest)
    $this->renderPartial("admin", array("model" => $model));
else
    $this->render("admin", array("model" => $model));
#9696 report it
softark at 2012/09/04 05:52am
RE: renderPartial

@yJeroen

Thank you for the good suggestion.

Yes, that's true. We don't have to render the whole page when the request is ajax. We can optimize the controller code using renderPartial as you suggest:

if (isset($_GET['ajax']))
    $this->renderPartial("admin", array("model" => $model));
else
    $this->render("admin", array("model" => $model));

This way we can skip the rendering of the layout that is not needed for the ajax response.

#9683 report it
yJeroen at 2012/09/03 05:28am
renderPartial

Softark, maybe you can give the tip to do a renderPartial of the gridview in the controller if $_GET['ajax'] has been set.

That way, the ajax call that updates the gridview is more CPU&data efficient. :)

#9664 report it
softark at 2012/09/02 05:58am
@seenivasan

Thank you for your comment. I really appreciate it. It's my great pleasure to have a reader like you who understand what I have in mind.

I wanted to write a kind of introduction to CGridView and CActiveDataProvider for the new comers, which I would have really wanted to read in the days when I started learning Yii myself.

Leave a comment

Please to leave your comment.

Write new article