Tuesday, 17 February 2015

How CGridView works in YII


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 callCActiveDataProvider::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 WHEREclause. 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 whosename 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 callunsetAttributes 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 $_GETparameters. 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 likeMyModel_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 thesort 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=Nwhere 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 ofajaxUpdate (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

No comments:

Post a Comment