CGridView: Render customized/complex datacolumns

You are viewing revision #13 of this wiki article.
This version may not be up to date with the latest version.
You may want to view the differences to the latest version.

next (#14) »

  1. Example
  2. B) Lazy loading - load the categories for each product
  3. C) The fun part - render view files into a cell
  4. Access column properties
  5. Tip

If you have to display a overview of related data in a table, you can use the CListView, create your view with table/tr/td tags and implement db-queries in the view to display detaildata of the current (master-)record.

But the CGridView offers the possibility to add custom columns too. In the class reference of the CGridView you will find this example.

$this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'columns'=>array
	(
        ...
        array(            // display 'create_time' using an expression
            'name'=>'create_time',
            'value'=>'date("M j, Y", $data->create_time)',
        ),
        array(            // display 'author.username' using an expression
            'name'=>'authorName',
            'value'=>'$data->author->username',
        ),
        array(            // display a column with "view", "update" and "delete" buttons
            'class'=>'CButtonColumn',
        ),
    ),
));

Because Yii checks the 'value' of a column definition by using CComponent.evaluateExpression you can assign a PHP expression string - that's great.

If you take a look at the source of CComponent.evaluateExpression you will see, that also PHP call_user_func_array() is supported - and that's very great.

You can add a column that displays data generated by a object method, for example a method of your controller.

All you have to do is to assign the object method to the value:

$this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'columns'=>array(
        ...
        array(            
            'name'=>'newColumn',
            //call the method 'gridDataColumn' from the controller
            'value'=>array($this,'gridDataColumn'), 
        ),
        array(            
            'name'=>'Address',
            //call the method 'renderAddress' from the model
            'value'=>array($model,'renderAddress'), 
        ),
    ),
));
class MyController extends Controller 
{
    ...

     //called on rendering the column for each row 
     protected function gridDataColumn($data,$row)
     {
          // ... generate the output for the column

          // Params:
          // $data ... the current row data   
	     // $row ... the row index	  
	     return $theCellValue;	  
    }		
   ...	

}
class Address extends CActiveRecord 
{
    ...

     //called on rendering the column for each row 
     public function renderAddress($data,$row)
     {
          // ... generate the output for a full address

          // Params:
          // $data ... the current row data   
	     // $row ... the row index	  
	     return $theCellValue;	  
    }		
   ...	

}

It's easier to handle/manage the cell value assigned by a method than to implement a PHP expression string. If you want you can use renderPartial to maintain the datacolumn.

Example

Assume you have related tables 'product' and 'category' with a MANY_MANY relation 'product_category' and want to display a table with the product and the related categories. You have to generate the product model and your dataProvider by using 'with', 'join', 'order' ...

I don't explain the working with the database and related models here. Please take a look a the Relational Active Record tutorial if you need more information.

A) Eager loading - load all data at once in a single query

You will (internally) generate a SQL statement like

SELECT p.id,p.name, p.description, c.name as category FROM product p JOIN product_category pc ON pc.product = p.id JOIN category c ON c.id = pc.category

Your standard gridview with the columns (id not displayed) product, description category will look like this:

PRODUCT    | DESCRIPTION           | CATEGORY
----------------------------------------------
iPhone 4S  | Dual Core A5 Chip ... |  Phone
----------------------------------------------
iPhone 4S  | Dual Core A5 Chip ... |  Offer
----------------------------------------------
iPhone 4S  | Dual Core A5 Chip ... |  Apple
----------------------------------------------
iPhone 4S  | Dual Core A5 Chip ... |  Communication
----------------------------------------------
iPhone 4S  | Dual Core A5 Chip ... |  Lifestyle
----------------------------------------------

Now we want display the production information only in the first row of each product:

PRODUCT   |     DESCRIPTION         | CATEGORY
----------------------------------------------
iPhone 4S |  Dual Core A5 Chip ...  | Phone
----------------------------------------------
          |                         | Offer
----------------------------------------------
          |                         | Apple
----------------------------------------------

If you need to hide the repeated value you have to create your own GridView and override the CGridView.renderTableRow method.

Or you use the power of the 'value' property:

Create two protected methods 'gridProductName' and 'gridProductDescription' in your controller and keep the latest rendered row as private variable.

class ProductController extends Controller 
{
    $_lastProductId = null;
    ....
	
   //called on rendering a grid row
   //the first column
   //the params are $data (=the current rowdata) and $row (the row index)
    protected function gridProductName($data,$row)
    {
       return $this->_lastProductId != $data->id ? $data->name : '';			
    }
	
    //called on rendering a grid row
    //the second column
    protected function gridProductDescription($data,$row)
    { 
        if($this->_lastProductId != $data->id)
        {
            $this->_lastProductId = $data->id; //remember the last product id
            return $data->name;
        }
        else
             return '';
    }	

}

Add the custom columns to the gridview:

$this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'columns'=>array(
        array(            
            'name'=>'product',
			//call the method 'gridUniqueProductName' of the controller
            //the params extracted to the method are $data (=the current rowdata) and $row (the row index)
            'value'=>array($this,'gridProductName')
        ),
        array(            
            'name'=>'description',
            'value'=>array($this,'gridProductDescription')
        ),

        'category', //display the category as a default column
    ),
));

B) Lazy loading - load the categories for each product

You can load you products and select the assigned categories in an extra query for each row. I know you may think this is a bad performance, but it depends how complex your db schema is (how many joins you need) or type/size of the redundant data of the single query. See the remarks at 7. Relational Query Performance of the Relational Active Record tutorial. If you load product images from the db too, lazy loading can be the better choice.

And you can display a grid like below. The categories should be displayed as links to a page that lists all products with the specified category.

PRODUCT            |    CATEGORIES
----------------------------------------------
iPhone 4S          |  Phone
Dual Core A5 Chip  |  Offer
....               |  Apple
....               |  Communication
....               |  Lifestyle
----------------------------------------------
Motorola Milestone |  ...

The controller code:

class ProductController extends Controller 
{
    ...
	
    //return the value for the product column
    protected function gridProduct($data,$row) 
    {
      return  CHtml::encode($data->name) .'<br/>' . CHtml::encode($data->description);
    }
	
	//called on rendering a single gridview row
	protected function gridProductCategories($data,$row)
    {
      $sql = 'SELECT c.id,c.name FROM product_category pc JOIN category c ON c.id = pc.category ';
      $sql .= 'WHERE pc.product = ' . data->id; //the product id 	
      $rows = Yii::app()->db->createCommand($sql)->queryAll();
	   
      $result = '';
	   if(!empty($rows))
	    foreach ($rows as $row) 
           {
    	      $url = $this->createUrl('bycategory',array('category'=>$row['id']));
              $result .= CHtml::link($row['name'],$url) .'<br/>'; 
	   }	  
	   return $result;	  
    }
	
        ...

     public function actionByCategory($category) 
     {
           ... display a page with all products of the specified category ...
     }
	
	...	

}

Render the CGridView in your view like this:

$this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'columns'=>array(
        array(            
            'name'=>'product',
			'type'=>'raw', //because of using html-code <br/>
            //call the controller method gridProduct for each row
            'value'=>array($this,'gridProduct'), 
        ),
        array(            
            'name'=>'category',
			'type'=>'raw', //because of using html-code
            'value'=>array($this,'gridProductCategories'), //call this controller method for each row
        ),
    ),
));

C) The fun part - render view files into a cell

In a controller method called on rendering a row of the gridview you can do more ...

You can use 'renderPartial' to manage the view of a datacell in a viewfile. For example, try to reuse the default 'view' generated by gii.

If you want to render the full address of a user in a cell you can do like this. The user has an attribute 'address', the addresses are stored in a table 'Address'

class UserController extends Controller 
{

  //the default admin action
  public function actionAdmin()
  {
		$model=new User('search');
		
		$model->unsetAttributes();  
		if(isset($_GET['User']))
			$model->attributes=$_GET['User'];

		$this->render('admin',array(
			'model'=>$model,
		));
  }

  //assume you have generated the Address model with gii too
  protected function gridAddress($data,$row)
  {		
    $model = Address::model()->findByPk($data->address); //$data->address is the FK from the user table
	
   //get the view from the address CRUD controller (generated with gii)
    return $this->renderPartial('../address/view',array('model'=>$model),true); //set $return = true, don't display direct
  }

}

The CGridView in the view: admin.php

$this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'columns'=>array(
	    
		... user attributes here ...
		
		// add a column to display the full address
        array(            
            'name'=>'address',
			'type'=>'raw', //because of using html-code from the rendered view
            'value'=>array($this,'gridAddress'), //call this controller method for each row
        ),
    ),
));

Now you have rendered an embedded CDetailView inside the cells of a CGridView. You can try to render a CListView (default actionIndex of a CRUD controller) into a cell too.

Access column properties

In the examples above, I use different methods for each gridcolumn.

If you want to use a single method for each column or need more information about the currently rendered column, add a third param (for example $dataColumn) to your called method:

//view
$this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'columns'=>array(
        array(            
            'name'=>'product',
            'value'=>array($this,'renderGridCell')
        ),
        array(            
            'name'=>'description',
            'value'=>array($this,'renderGridCell')
        ),

        'category', //display the category as a default column
    ),
));


class ProductController extends Controller 
{
    ...
	

    protected function gridProduct($data,$row,$dataColumn) 
    {
      //var_dump($dataColumn);
      //$dataColumn is an instance of a [CDataColumn](http://www.yiiframework.com/doc/api/1.1/CDataColumn "CDataColumn")
      //you have access to the properties and methods here (name, id, type, value, grid ...)
       
      //implement the rendering
      switch($dataColumn->name) {
          case 'product': ....
             break;

          case 'description': ....
             break;

         ....
      } 

      ....

    }

Tip

Generate you own views for the grid datacolums. You can add buttons, links, ajax/CJuiDialog in the viewfile which were rendered into a datacell: See my other CGridview articles.