Perhaps one of the most common requests on any Magento project is to make lists of things in the admin. The grid block supplied for this is a wonderfully versatile tool but woefully under-appreciated. Be sure to read about admin controllers before taking on this task.


Model, View & Controller

This is a going to be a demonstration of adminhtml grid capabilities so let’s not dwell on the model part of this – We’re going to use the Mage_​Admin_​Model_​Mysql4_​User_​Collection class because it’s an existing collection for a flat table that is guaranteed to have some data in it, even on a fresh install.

A pair of blocks

Let’s get the container quickly out of the way, it’s the part of the page that shows a title, some buttons and the all important grid block. This is boring work so copy-paste the code and alter as needed.

class Knectar_Example_Block_Adminhtml_Adminusers
  extends Mage_Adminhtml_Block_Widget_Grid_Container
{

	public function __construct() {
		// This module's block alias as defined in config.xml
		$this->_blockGroup = 'knectarexample';

		// The part of this class' name after "Block"
		$this->_controller = 'adminhtml_adminusers';

		// User visible text
		$this->_headerText = $this->__('Example Header Text');
		$this->_addButtonLabel = $this->__('Add New User');

		parent::__construct();

		// If buttons are not needed they must
		// be removed after the parent's constructor.
		// Otherwise disable this line.
		$this->removeButton('add');
	}

}

At last the interesting stuff! Here is the glorious grid with a lot going on:

class Knectar_Example_Block_Adminhtml_Adminusers_Grid
  extends Mage_Adminhtml_Block_Widget_Grid
{

	public function __construct()
	{
		parent::__construct();
		$this->setDefaultSort('user_id');
		$this->setDefaultDir('ASC');

		$this->setId('knectar_example_adminusers');
		$this->setUseAjax(true);
	}

	protected function _prepareCollection()
	{
		$collection = Mage::getResourceModel('admin/user_collection');
		$this->setCollection($collection);
		return parent::_prepareCollection();
	}

	protected function _prepareColumns()
	{
		$this->addColumn('user_id', array(
			'index' => 'user_id',
			'header' => $this->__('ID'),
			'width' => '25px'
		));
		$this->addColumn('username', array(
			'index' => 'username',
			'header' => $this->__('User Name')
		));
		$this->addColumn('firstname', array(
			'index' => 'firstname',
			'header' => $this->__('First Name')
		));
		$this->addColumn('lastname', array(
			'index' => 'lastname',
			'header' => $this->__('Last Name')
		));

		return parent::_prepareColumns();
	}

	protected function _prepareLayout()
	{
		$this->addExportType('*/*/exportCsv', $this->__('CSV'));
		$this->addExportType('*/*/exportXml', $this->__('Excel XML'));
		
		return parent::_prepareLayout();
	}

	public function getGridUrl($params = array())
	{
		return $this->getUrl('*/*/grid', $params);
	}

	public function getRowUrl($row)
	{
		if ($row->getUserId()) {
			return $this->getUrl('*/*/view',
				array('user_id' => $row->getUserId())
			);
		}

		// false means row will not be clickable.
		return false;
	}

}

In the constructor a default sort is given rather than letting the database’s natural order show through. That would approximately be the order that records are stored on disc and it is bad form to allow users to see that. Also a conscientious user might notice they are unable to return to that order after clicking on any column header and get annoyed.

It’s important to set an ID if AJAX is to be used, the ID will be used by Javascript to find the correct table element so it needs to be consistent. Because setUseAjax is true the grid will need an URL to fetch updated copies of itself from, that is what the function getGridUrl() is for. You can see it simply generates an URL on the same controller as this page with a “grid” action. More on that later… Notice also that URLs are returned for exporting to different file types and for clicking on individual rows, these are also in the same controller.

Even more important is to prepare a collection, this is where the Mage_​Admin_​Model_​Mysql4_​User_​Collection class gets assigned. The parent::​_prepareCollection() function does all the hard work of fetching search terms and sort orders then applying them to the collection that was just set. It also tries to count the total number of records (in case there are more that can fit on the page) so if there is a problem with the collection this is where an error will occur. Because it reads POST parameters for you this saves an awful lot of work for your controller. If you must alter the collection please don’t do it here, extend the collection to make a custom one and assign that in it’s stead.


Back to the Controller

Controllers should contain as little code as possible, they are liable to grow very large very quickly. If you find any logic being done here split it off into it’s own helper or model. For example saving a model should be no more than a $model​->addData($data)​->save();

class Knectar_Example_Adminhtml_ExampleController
  extends Mage_Adminhtml_Controller_Action
{

	public function indexAction()
	{
		// Load the layout handle <adminhtml_example_index>
		$this->loadLayout();

		// Sets the window title to "Example / Knectar / Magento Admin"
		$this->_title($this->__('Knectar'))
			->_title($this->__('Example'))
		// Highlight the current menu
			->_setActiveMenu('knectar/example');

		$this->renderLayout();
	}

	public function gridAction()
	{
		$this->getResponse()->setBody($this->_getGridBlock()->toHtml());
	}

	public function exportCsvAction()
	{
		$fileName  = 'example.csv';
		$this->_prepareDownloadResponse($fileName,
			$this->_getGridBlock()->getCsv()
		);
	}

	public function exportXmlAction()
	{
		$fileName  = 'example.xlsx';
		$this->_prepareDownloadResponse($fileName,
			$this->_getGridBlock()->getExcel($fileName)
		);
	}

	protected function _getGridBlock()
	{
		$this->loadLayout(array('default', 'adminhtml_example_index'));
		return $this->getLayout()->getBlock('adminhtml_adminusers.grid');
	}

}

Remember at the beginning we made a container for the grid? It set _controller to “adminhtml_​adminusers” and that was used to name the grid block as “adminhtml_​adminusers.​grid”. Now the grid block is retrieved by that name and used for several things, grids come with functions for exporting to file and by calling it’s toHtml() we get the content for our AJAX. Remember also that grids read in POST parameters for you, when a search is performed it automatically filters the data and all you need to do is output that. The correct way to output anything is to assign it to the response object’s body, this is a Zend thing to avoid getting errors. When renderLayout() is called it is doing the same thing behind the scenes.

Grids remember their filter and sort settings for the lifetime of the session so those same settings are also used when exporting, one more thing that you don’t need to do.

Finally we get to the indexAction().  This loads and renders the layout but not much more. The layout is the last missing piece of the MVC pattern (it’s an invisible L but it’s definitely there).


Layout

Go back to the module’s config.xml and add in this section:

<config>
    <!-- ...continue from existing content... -->
    <adminhtml>
        <layout>
            <updates>
                <knectarexample>
                    <file>knectarexample.xml</file>
                </knectarexample>
            </updates>
        </layout>
    </adminhtml>
</config>

This will cause the renderer to look for a file “app/​design/​adminhtml/​default/​default/​layout/​knectarexample.xml“, create that file like thus:

<?xml version="1.0" encoding="UTF-8"?>
<layout>

	<adminhtml_example_index>
		<reference name="content">
			<block type="knectarexample/adminhtml_adminusers" />
		</reference>
	</adminhtml_example_index>

</layout>

And so we have come full circle. This layout file loads the container, which loads the grid, which uses AJAX to load the controller. The AJAX actions use $this​->loadLayout() with a “adminhtml_​example_​index” handle which is in this layout file.


With practice and a bit of copy-pasting setting up a whole page should take no more than an hour. Of course there is the collection to think about but that is coming up next…