* @link http://www.pradosoft.com/
* @copyright Copyright © 2005-2014 PradoSoft
* @license http://www.pradosoft.com/license/
* @package Prado\Web\UI\WebControls
*/
namespace Prado\Web\UI\WebControls;
use Prado\Exceptions\TInvalidDataTypeException;
use Prado\Exceptions\TInvalidDataValueException;
use Prado\Collections\TMap;
use Prado\Collections\TList;
/**
* TRepeater class.
*
* TRepeater displays its content repeatedly based on the data fetched from
* {@link setDataSource DataSource}.
* The repeated contents in TRepeater are called items, which are controls and
* can be accessed through {@link getItems Items}. When {@link dataBind()} is invoked,
* TRepeater creates an item for each row of data and binds the data row to the item.
* Optionally, a repeater can have a header, a footer and/or separators between items.
*
* The layout of the repeated contents are specified by inline templates.
* Repeater items, header, footer, etc. are being instantiated with the corresponding
* templates when data is being bound to the repeater.
*
* Since v3.1.0, the layout can also be specified by renderers. A renderer is a control class
* that can be instantiated as repeater items, header, etc. A renderer can thus be viewed
* as an external template (in fact, it can also be non-templated controls).
*
* A renderer can be any control class.
* - If the class implements {@link \Prado\IDataRenderer}, the Data
* property will be set as the data row during databinding. Many PRADO controls
* implement this interface, such as {@link TLabel}, {@link TTextBox}, etc.
* - If the class implements {@link IItemDataRenderer}, the ItemIndex property will be set
* as the zero-based index of the item in the repeater item collection, and
* the ItemType property as the item's type (such as TListItemType::Item).
* {@link TRepeaterItemRenderer} may be used as the convenient base class which
* already implements {@link IDataItemRenderer}.
*
* The following properties are used to specify different types of template and renderer
* for a repeater:
* - {@link setItemTemplate ItemTemplate}, {@link setItemRenderer ItemRenderer}:
* for each repeated row of data
* - {@link setAlternatingItemTemplate AlternatingItemTemplate}, {@link setAlternatingItemRenderer AlternatingItemRenderer}:
* for each alternating row of data. If not set, {@link setItemTemplate ItemTemplate} or {@link setItemRenderer ItemRenderer}
* will be used instead.
* - {@link setHeaderTemplate HeaderTemplate}, {@link setHeaderRenderer HeaderRenderer}:
* for the repeater header.
* - {@link setFooterTemplate FooterTemplate}, {@link setFooterRenderer FooterRenderer}:
* for the repeater footer.
* - {@link setSeparatorTemplate SeparatorTemplate}, {@link setSeparatorRenderer SeparatorRenderer}:
* for content to be displayed between items.
* - {@link setEmptyTemplate EmptyTemplate}, {@link setEmptyRenderer EmptyRenderer}:
* used when data bound to the repeater is empty.
*
* If a content type is defined with both a template and a renderer, the latter takes precedence.
*
* When {@link dataBind()} is being called, TRepeater undergoes the following lifecycles for each row of data:
* - create item based on templates or renderers
* - set the row of data to the item
* - raise {@link onItemCreated OnItemCreated}:
* - add the item as a child control
* - call dataBind() of the item
* - raise {@link onItemDataBound OnItemDataBound}:
*
* TRepeater raises an {@link onItemCommand OnItemCommand} whenever a button control
* within some repeater item raises a OnCommand event. Therefore,
* you can handle all sorts of OnCommand event in a central place by
* writing an event handler for {@link onItemCommand OnItemCommand}.
*
* When a page containing a repeater is post back, the repeater will restore automatically
* all its contents, including items, header, footer and separators.
* However, the data row associated with each item will not be recovered and become null.
* To access the data, use one of the following ways:
* - Use {@link getDataKeys DataKeys} to obtain the data key associated with
* the specified repeater item and use the key to fetch the corresponding data
* from some persistent storage such as DB.
* - Save the whole dataset in viewstate, which will restore the dataset automatically upon postback.
* Be aware though, if the size of your dataset is big, your page size will become big. Some
* complex data may also have serializing problem if saved in viewstate.
*
* @author Qiang Xue
* @package Prado\Web\UI\WebControls
* @since 3.0
*/
class TRepeater extends TDataBoundControl implements \Prado\Web\UI\INamingContainer
{
/**
* Repeater item types
* @deprecated deprecated since version 3.0.4. Use TListItemType constants instead.
*/
const IT_HEADER='Header';
const IT_FOOTER='Footer';
const IT_ITEM='Item';
const IT_SEPARATOR='Separator';
const IT_ALTERNATINGITEM='AlternatingItem';
/**
* @var ITemplate template for repeater items
*/
private $_itemTemplate=null;
/**
* @var ITemplate template for each alternating item
*/
private $_alternatingItemTemplate=null;
/**
* @var ITemplate template for header
*/
private $_headerTemplate=null;
/**
* @var ITemplate template for footer
*/
private $_footerTemplate=null;
/**
* @var ITemplate template used for repeater when no data is bound
*/
private $_emptyTemplate=null;
/**
* @var ITemplate template for separator
*/
private $_separatorTemplate=null;
/**
* @var TRepeaterItemCollection list of repeater items
*/
private $_items=null;
/**
* @var TControl header item
*/
private $_header=null;
/**
* @var TControl footer item
*/
private $_footer=null;
/**
* @return string the class name for repeater items. Defaults to empty, meaning not set.
* @since 3.1.0
*/
public function getItemRenderer()
{
return $this->getViewState('ItemRenderer','');
}
/**
* Sets the item renderer class.
*
* If not empty, the class will be used to instantiate as repeater items.
* This property takes precedence over {@link getItemTemplate ItemTemplate}.
*
* @param string the renderer class name in namespace format.
* @see setItemTemplate
* @since 3.1.0
*/
public function setItemRenderer($value)
{
$this->setViewState('ItemRenderer',$value,'');
}
/**
* @return string the class name for alternative repeater items. Defaults to empty, meaning not set.
* @since 3.1.0
*/
public function getAlternatingItemRenderer()
{
return $this->getViewState('AlternatingItemRenderer','');
}
/**
* Sets the alternative item renderer class.
*
* If not empty, the class will be used to instantiate as alternative repeater items.
* This property takes precedence over {@link getAlternatingItemTemplate AlternatingItemTemplate}.
*
* @param string the renderer class name in namespace format.
* @see setAlternatingItemTemplate
* @since 3.1.0
*/
public function setAlternatingItemRenderer($value)
{
$this->setViewState('AlternatingItemRenderer',$value,'');
}
/**
* @return string the class name for repeater item separators. Defaults to empty, meaning not set.
* @since 3.1.0
*/
public function getSeparatorRenderer()
{
return $this->getViewState('SeparatorRenderer','');
}
/**
* Sets the repeater item separator renderer class.
*
* If not empty, the class will be used to instantiate as repeater item separators.
* This property takes precedence over {@link getSeparatorTemplate SeparatorTemplate}.
*
* @param string the renderer class name in namespace format.
* @see setSeparatorTemplate
* @since 3.1.0
*/
public function setSeparatorRenderer($value)
{
$this->setViewState('SeparatorRenderer',$value,'');
}
/**
* @return string the class name for repeater header item. Defaults to empty, meaning not set.
* @since 3.1.0
*/
public function getHeaderRenderer()
{
return $this->getViewState('HeaderRenderer','');
}
/**
* Sets the repeater header renderer class.
*
* If not empty, the class will be used to instantiate as repeater header item.
* This property takes precedence over {@link getHeaderTemplate HeaderTemplate}.
*
* @param string the renderer class name in namespace format.
* @see setHeaderTemplate
* @since 3.1.0
*/
public function setHeaderRenderer($value)
{
$this->setViewState('HeaderRenderer',$value,'');
}
/**
* @return string the class name for repeater footer item. Defaults to empty, meaning not set.
* @since 3.1.0
*/
public function getFooterRenderer()
{
return $this->getViewState('FooterRenderer','');
}
/**
* Sets the repeater footer renderer class.
*
* If not empty, the class will be used to instantiate as repeater footer item.
* This property takes precedence over {@link getFooterTemplate FooterTemplate}.
*
* @param string the renderer class name in namespace format.
* @see setFooterTemplate
* @since 3.1.0
*/
public function setFooterRenderer($value)
{
$this->setViewState('FooterRenderer',$value,'');
}
/**
* @return string the class name for empty repeater item. Defaults to empty, meaning not set.
* @since 3.1.0
*/
public function getEmptyRenderer()
{
return $this->getViewState('EmptyRenderer','');
}
/**
* Sets the repeater empty renderer class.
*
* The empty renderer is created as the child of the repeater
* if data bound to the repeater is empty.
* This property takes precedence over {@link getEmptyTemplate EmptyTemplate}.
*
* @param string the renderer class name in namespace format.
* @see setEmptyTemplate
* @since 3.1.0
*/
public function setEmptyRenderer($value)
{
$this->setViewState('EmptyRenderer',$value,'');
}
/**
* @return ITemplate the template for repeater items
*/
public function getItemTemplate()
{
return $this->_itemTemplate;
}
/**
* @param ITemplate the template for repeater items
* @throws TInvalidDataTypeException if the input is not an {@link ITemplate} or not null.
*/
public function setItemTemplate($value)
{
if($value instanceof \Prado\Web\UI\ITemplate || $value===null)
$this->_itemTemplate=$value;
else
throw new TInvalidDataTypeException('repeater_template_required','ItemTemplate');
}
/**
* @return ITemplate the alternative template string for the item
*/
public function getAlternatingItemTemplate()
{
return $this->_alternatingItemTemplate;
}
/**
* @param ITemplate the alternative item template
* @throws TInvalidDataTypeException if the input is not an {@link ITemplate} or not null.
*/
public function setAlternatingItemTemplate($value)
{
if($value instanceof \Prado\Web\UI\ITemplate || $value===null)
$this->_alternatingItemTemplate=$value;
else
throw new TInvalidDataTypeException('repeater_template_required','AlternatingItemTemplate');
}
/**
* @return ITemplate the header template
*/
public function getHeaderTemplate()
{
return $this->_headerTemplate;
}
/**
* @param ITemplate the header template
* @throws TInvalidDataTypeException if the input is not an {@link ITemplate} or not null.
*/
public function setHeaderTemplate($value)
{
if($value instanceof \Prado\Web\UI\ITemplate || $value===null)
$this->_headerTemplate=$value;
else
throw new TInvalidDataTypeException('repeater_template_required','HeaderTemplate');
}
/**
* @return ITemplate the footer template
*/
public function getFooterTemplate()
{
return $this->_footerTemplate;
}
/**
* @param ITemplate the footer template
* @throws TInvalidDataTypeException if the input is not an {@link ITemplate} or not null.
*/
public function setFooterTemplate($value)
{
if($value instanceof \Prado\Web\UI\ITemplate || $value===null)
$this->_footerTemplate=$value;
else
throw new TInvalidDataTypeException('repeater_template_required','FooterTemplate');
}
/**
* @return ITemplate the template applied when no data is bound to the repeater
*/
public function getEmptyTemplate()
{
return $this->_emptyTemplate;
}
/**
* @param ITemplate the template applied when no data is bound to the repeater
* @throws TInvalidDataTypeException if the input is not an {@link ITemplate} or not null.
*/
public function setEmptyTemplate($value)
{
if($value instanceof \Prado\Web\UI\ITemplate || $value===null)
$this->_emptyTemplate=$value;
else
throw new TInvalidDataTypeException('repeater_template_required','EmptyTemplate');
}
/**
* @return ITemplate the separator template
*/
public function getSeparatorTemplate()
{
return $this->_separatorTemplate;
}
/**
* @param ITemplate the separator template
* @throws TInvalidDataTypeException if the input is not an {@link ITemplate} or not null.
*/
public function setSeparatorTemplate($value)
{
if($value instanceof \Prado\Web\UI\ITemplate || $value===null)
$this->_separatorTemplate=$value;
else
throw new TInvalidDataTypeException('repeater_template_required','SeparatorTemplate');
}
/**
* @return TControl the header item
*/
public function getHeader()
{
return $this->_header;
}
/**
* @return TControl the footer item
*/
public function getFooter()
{
return $this->_footer;
}
/**
* @return TRepeaterItemCollection list of repeater item controls
*/
public function getItems()
{
if(!$this->_items)
$this->_items=new TRepeaterItemCollection;
return $this->_items;
}
/**
* @return string the field of the data source that provides the keys of the list items.
*/
public function getDataKeyField()
{
return $this->getViewState('DataKeyField','');
}
/**
* @param string the field of the data source that provides the keys of the list items.
*/
public function setDataKeyField($value)
{
$this->setViewState('DataKeyField',$value,'');
}
/**
* @return TList the keys used in the data listing control.
*/
public function getDataKeys()
{
if(($dataKeys=$this->getViewState('DataKeys',null))===null)
{
$dataKeys=new TList;
$this->setViewState('DataKeys',$dataKeys,null);
}
return $dataKeys;
}
/**
* Creates a repeater item.
* This method invokes {@link createItem} to create a new repeater item.
* @param integer zero-based item index.
* @param TListItemType item type
* @return TControl the created item, null if item is not created
*/
private function createItemInternal($itemIndex,$itemType)
{
if(($item=$this->createItem($itemIndex,$itemType))!==null)
{
$param=new TRepeaterItemEventParameter($item);
$this->onItemCreated($param);
$this->getControls()->add($item);
return $item;
}
else
return null;
}
/**
* Creates a repeater item and performs databinding.
* This method invokes {@link createItem} to create a new repeater item.
* @param integer zero-based item index.
* @param TListItemType item type
* @param mixed data to be associated with the item
* @return TControl the created item, null if item is not created
*/
private function createItemWithDataInternal($itemIndex,$itemType,$dataItem)
{
if(($item=$this->createItem($itemIndex,$itemType))!==null)
{
$param=new TRepeaterItemEventParameter($item);
if($item instanceof \Prado\IDataRenderer)
$item->setData($dataItem);
$this->onItemCreated($param);
$this->getControls()->add($item);
$item->dataBind();
$this->onItemDataBound($param);
return $item;
}
else
return null;
}
/**
* Creates a repeater item instance based on the item type and index.
* @param integer zero-based item index
* @param TListItemType item type
* @return TControl created repeater item
*/
protected function createItem($itemIndex,$itemType)
{
$template=null;
$classPath=null;
switch($itemType)
{
case TListItemType::Item :
$classPath=$this->getItemRenderer();
$template=$this->_itemTemplate;
break;
case TListItemType::AlternatingItem :
if(($classPath=$this->getAlternatingItemRenderer())==='' && ($template=$this->_alternatingItemTemplate)===null)
{
$classPath=$this->getItemRenderer();
$template=$this->_itemTemplate;
}
break;
case TListItemType::Header :
$classPath=$this->getHeaderRenderer();
$template=$this->_headerTemplate;
break;
case TListItemType::Footer :
$classPath=$this->getFooterRenderer();
$template=$this->_footerTemplate;
break;
case TListItemType::Separator :
$classPath=$this->getSeparatorRenderer();
$template=$this->_separatorTemplate;
break;
default:
throw new TInvalidDataValueException('repeater_itemtype_unknown',$itemType);
}
if($classPath!=='')
{
$item=Prado::createComponent($classPath);
if($item instanceof IItemDataRenderer)
{
$item->setItemIndex($itemIndex);
$item->setItemType($itemType);
}
}
else if($template!==null)
{
$item=new TRepeaterItem;
$item->setItemIndex($itemIndex);
$item->setItemType($itemType);
$template->instantiateIn($item);
}
else
$item=null;
return $item;
}
/**
* Creates empty repeater content.
*/
protected function createEmptyContent()
{
if(($classPath=$this->getEmptyRenderer())!=='')
$this->getControls()->add(Prado::createComponent($classPath));
else if($this->_emptyTemplate!==null)
$this->_emptyTemplate->instantiateIn($this);
}
/**
* Renders the repeater.
* This method overrides the parent implementation by rendering the body
* content as the whole presentation of the repeater. Outer tag is not rendered.
* @param THtmlWriter writer
*/
public function render($writer)
{
if($this->_items && $this->_items->getCount() || $this->_emptyTemplate!==null || $this->getEmptyRenderer()!=='')
$this->renderContents($writer);
}
/**
* Saves item count in viewstate.
* This method is invoked right before control state is to be saved.
*/
public function saveState()
{
parent::saveState();
if($this->_items)
$this->setViewState('ItemCount',$this->_items->getCount(),0);
else
$this->clearViewState('ItemCount');
}
/**
* Loads item count information from viewstate.
* This method is invoked right after control state is loaded.
*/
public function loadState()
{
parent::loadState();
if(!$this->getIsDataBound())
$this->restoreItemsFromViewState();
$this->clearViewState('ItemCount');
}
/**
* Clears up all items in the repeater.
*/
public function reset()
{
$this->getControls()->clear();
$this->getItems()->clear();
$this->_header=null;
$this->_footer=null;
}
/**
* Creates repeater items based on viewstate information.
*/
protected function restoreItemsFromViewState()
{
$this->reset();
if(($itemCount=$this->getViewState('ItemCount',0))>0)
{
$items=$this->getItems();
$hasSeparator=$this->_separatorTemplate!==null || $this->getSeparatorRenderer()!=='';
$this->_header=$this->createItemInternal(-1,TListItemType::Header);
for($i=0;$i<$itemCount;++$i)
{
if($hasSeparator && $i>0)
$this->createItemInternal($i-1,TListItemType::Separator);
$itemType=$i%2==0?TListItemType::Item : TListItemType::AlternatingItem;
$items->add($this->createItemInternal($i,$itemType,false,null));
}
$this->_footer=$this->createItemInternal(-1,TListItemType::Footer);
}
else
$this->createEmptyContent();
$this->clearChildState();
}
/**
* Performs databinding to populate repeater items from data source.
* This method is invoked by dataBind().
* You may override this function to provide your own way of data population.
* @param Traversable the data
*/
protected function performDataBinding($data)
{
$this->reset();
$keys=$this->getDataKeys();
$keys->clear();
$keyField=$this->getDataKeyField();
$items=$this->getItems();
$itemIndex=0;
$hasSeparator=$this->_separatorTemplate!==null || $this->getSeparatorRenderer()!=='';
foreach($data as $key=>$dataItem)
{
if($keyField!=='')
$keys->add($this->getDataFieldValue($dataItem,$keyField));
else
$keys->add($key);
if($itemIndex===0)
$this->_header=$this->createItemWithDataInternal(-1,TListItemType::Header,null);
if($hasSeparator && $itemIndex>0)
$this->createItemWithDataInternal($itemIndex-1,TListItemType::Separator,null);
$itemType=$itemIndex%2==0?TListItemType::Item : TListItemType::AlternatingItem;
$items->add($this->createItemWithDataInternal($itemIndex,$itemType,$dataItem));
$itemIndex++;
}
if($itemIndex>0)
$this->_footer=$this->createItemWithDataInternal(-1,TListItemType::Footer,null);
else
{
$this->createEmptyContent();
$this->dataBindChildren();
}
$this->setViewState('ItemCount',$itemIndex,0);
}
/**
* This method overrides parent's implementation to handle
* {@link onItemCommand OnItemCommand} event which is bubbled from
* repeater items and their child controls.
* This method should only be used by control developers.
* @param TControl the sender of the event
* @param TEventParameter event parameter
* @return boolean whether the event bubbling should stop here.
*/
public function bubbleEvent($sender,$param)
{
if($param instanceof TRepeaterCommandEventParameter)
{
$this->onItemCommand($param);
return true;
}
else
return false;
}
/**
* Raises OnItemCreated event.
* This method is invoked after a repeater item is created and instantiated with
* template, but before added to the page hierarchy.
* The repeater item control responsible for the event
* can be determined from the event parameter.
* If you override this method, be sure to call parent's implementation
* so that event handlers have chance to respond to the event.
* @param TRepeaterItemEventParameter event parameter
*/
public function onItemCreated($param)
{
$this->raiseEvent('OnItemCreated',$this,$param);
}
/**
* Raises OnItemDataBound event.
* This method is invoked right after an item is data bound.
* The repeater item control responsible for the event
* can be determined from the event parameter.
* If you override this method, be sure to call parent's implementation
* so that event handlers have chance to respond to the event.
* @param TRepeaterItemEventParameter event parameter
*/
public function onItemDataBound($param)
{
$this->raiseEvent('OnItemDataBound',$this,$param);
}
/**
* Raises OnItemCommand event.
* This method is invoked after a button control in
* a template raises OnCommand event.
* The repeater control responsible for the event
* can be determined from the event parameter.
* The event parameter also contains the information about
* the initial sender of the OnCommand event, command name
* and command parameter.
* You may override this method to provide customized event handling.
* Be sure to call parent's implementation so that
* event handlers have chance to respond to the event.
* @param TRepeaterCommandEventParameter event parameter
*/
public function onItemCommand($param)
{
$this->raiseEvent('OnItemCommand',$this,$param);
}
/**
* Returns the value of the data at the specified field.
* If data is an array, TMap or TList, the value will be returned at the index
* of the specified field. If the data is a component with a property named
* as the field name, the property value will be returned.
* Otherwise, an exception will be raised.
* @param mixed data item
* @param mixed field name
* @return mixed data value at the specified field
* @throws TInvalidDataValueException if the data is invalid
*/
protected function getDataFieldValue($data,$field)
{
return TDataFieldAccessor::getDataFieldValue($data,$field);
}
}