3 state checkbox for headerrenderer in datagrid

This post has been moved to http://srinichekuri.com/2011/05/20/3-state-checkbox-for-headerrenderer-in-datagrid/

**********************************************************************

Today I made by first contribution to Adobe Cook by posting about this topic. The idea on this topic started by showing my earlier post on gmail header to my colleague who also happens to be the owner of post in adobe cookbook that talks about 3State checkbox for TreeRenderer. He pointed out that the checkbox in the header can be threeState. Well thats a common problem but suprisingly this was not addressed in the online opensource community for Datagrid. I saw an opportunity and coded it.

Note:
There is a bug in my colleague’s post that it doesn’t take care of the initial state, I have taken care of that bug in my code.My code itself is inspired by my colleagues post.

The checkbox in headerRenderer will have three states:

  • All the rows in the datagrid are selected (‘select’ State).
  • None of the rows in the datagrid are selected(‘unselected’ State)
  • One or more rows (but not all) are selected(‘undecided’ State).

ScreenShots:
Change of state from ‘unselect’ state to ‘select’ state:

Change of state from ‘select’ state to ‘undecided’ state:

Code:

Here is the code for Main.mxml:

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
				xmlns:s="library://ns.adobe.com/flex/spark"
				xmlns:mx="library://ns.adobe.com/flex/mx"
				creationComplete="creationCompleteHandler(event)">

	<fx:Script>
		<![CDATA[
			import mx.collections.ArrayCollection;
			import mx.controls.Alert;
			import mx.events.FlexEvent;

			private var _dataProvider:ArrayCollection;
			[Bindable]
			public function set dataProvider(value:Object):void{
				_dataProvider=value as ArrayCollection
			}
			public function get dataProvider():Object{
				return _dataProvider;
			}

			protected function creationCompleteHandler(event:FlexEvent):void
			{
				dataProvider = new ArrayCollection([
					{checked:true, name:'Jim Smith'},
					{checked:false, name:'Yancy Williams'}]);
			}

		]]>
	</fx:Script>

	<mx:DataGrid id="myDataGrid" left="10" top="10" dataProvider="@{dataProvider}">
		<mx:columns>
			<mx:DataGridColumn textAlign="center" dataField="checked"
									   width="90"
									   rendererIsEditor="true"
									   sortable="false"
									   itemRenderer="renderer.CheckboxItemRenderer"
									   headerRenderer="renderer.ThreeStateCheckBoxHeaderRenderer"
									   />
			<mx:DataGridColumn headerText="Name" dataField="name" width="110"/>
		</mx:columns>
	</mx:DataGrid>

</s:Application>

Code for headerRenderer

package renderer
{
	import flash.events.MouseEvent;

	import mx.collections.ArrayCollection;
	import mx.controls.DataGrid;
	import mx.controls.Image;
	import mx.controls.dataGridClasses.DataGridListData;
	import mx.controls.dataGridClasses.MXDataGridItemRenderer;

	import spark.components.CheckBox;

	/**
	 * HeaderRender with a Three State Checkbox.
	 * <p>Functionality:<br>
	 * 	  <li>Selecting the checkbox will select all the rows in the datagrid</li>
	 * 	  <li>Unselecting the checkbox will unselect all the rows in the datagrid</li>
	 *
	 * <p>The checkbox can be three states at any point:<br>
	 *    <li>select: This would mean that all the rows are selected</li>
	 * 	  <li>unselect: This would mean that none of the rows are selected</li>
	 *    <li>undecided: This would mean that one or more (but not all) of the rows are selected</li>
	 *
	 * @author Srinivas Chekuri
	 *
	 */
	public class ThreeStateCheckBoxHeaderRenderer extends MXDataGridItemRenderer
	{
		protected var myCheckBox:CheckBox;
		protected var myImage:Image;
		private var imageWidth:Number 	= 9.5;
		private var imageHeight:Number 	= 9.5;
		private var inner:String 	= "assets/inner.png";

		private const SELECT_STATE:String="select";
		private const UNSELECT_STATE:String="unselect";
		private const UNDECIDED_STATE:String="undecided";

		private var STATE:String = UNSELECT_STATE;

		/**
		 * Constuctor
		 *
		 */
		public function ThreeStateCheckBoxHeaderRenderer()
		{
			super();
		}

		/**
		 * overides the function <code>createChildren</code> in the component lifecyle. This instantiates
		 * <code>myCheckBox</code> and <code>myImage</code>.
		 *
		 */
		override protected function createChildren():void{
			super.createChildren();
			myCheckBox = new CheckBox();
			myCheckBox.setStyle("horizontalCenter", "0");
			myCheckBox.setStyle("verticalCenter", "0");
			myCheckBox.addEventListener( MouseEvent.CLICK, checkBoxClickHandler );
			addElement(myCheckBox);
			myImage = new Image();
			myImage.source = inner;
			myImage.addEventListener( MouseEvent.CLICK, imageClickHandler );
			myImage.visible=false;
			addElement(myImage);
		}

		/**
		 * overides the function <code>updateDisplayList</code> in the component lifecyle. This sets the
		 * display settings of <code>myCheckBox</code> and <code>myImage</code>. This also updates the state
		 * according to the selections made in the datagrid.
		 *
		 * @param unscaledWidth
		 * @param unscaledHeight
		 *
		 */
		override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void{
			super.updateDisplayList(unscaledWidth,unscaledHeight);

			myImage.x=myCheckBox.x+1.5;
			myImage.y=myCheckBox.y+5.5;

			myImage.width=imageWidth;
			myImage.height=imageHeight;

			if(areAllSelected()){
				STATE=SELECT_STATE;
			}else if(isAnyColumnSelected()){
				STATE=UNDECIDED_STATE;
			}else if(!isAnyColumnSelected()){
				STATE=UNSELECT_STATE;
			}
			checkState();
		}

		/**
		 * Makes adjustments to the <code>visible</code> property of <code>myImage</code> and
		 * <code>selected</code> of <code>myCheckBox</code> according to the state.
		 *
		 * @private
		 *
		 */
		private function checkState():void{
			if(STATE==SELECT_STATE){
				myImage.visible=false;
				myCheckBox.selected=true;
			}else if(STATE==UNSELECT_STATE){
				myImage.visible=false;
				myCheckBox.selected=false;
			}else{
				myImage.visible=true;
				myCheckBox.selected=false;
			}
		}

		/**
		 * Handler method for <code>MouseEvent.CLICK</code> event on <code>myImage</code>.
		 *
		 * @param event
		 *
		 */
		protected function imageClickHandler(event:MouseEvent):void{
			STATE=SELECT_STATE;
			checkState();
			selectAll();
		}

		/**
		 * Handler method for <code>MouseEvent.CLICK</code> event on <code>myCheckBox</code>.
		 *
		 * @param event
		 *
		 */
		protected function checkBoxClickHandler(event:MouseEvent):void{
			selectAll();
		}

		/**
		 * Selects all the rows in the datagrid.
		 *
		 */
		private function selectAll():void{
			var ac:ArrayCollection = DataGrid(DataGridListData(listData).owner).dataProvider as ArrayCollection;
			for each (var o:Object in ac){
				o.checked=myCheckBox.selected;
			}
			DataGrid(DataGridListData(listData).owner).dataProvider = ac;
		}

		/**
		 * Checks if all the rows are selected in the datagrid.
		 *
		 * @return Boolean: returns <code>true</code> if all rows are selected else returns
		 * <code>false</code>.
		 *
		 */
		private function areAllSelected():Boolean{
			var ac:ArrayCollection = DataGrid(DataGridListData(listData).owner).dataProvider as ArrayCollection;
			var b:Boolean=true;
			for each (var o:Object in ac){
				if(!o.checked){
					b=false;
					break;
				}
			}
			return b;
		}

		/**
		 * Checks to see if any one of the rows are selected.
		 *
		 * @return Boolean: return <code>true</code> if any one the rows are selected.
		 *
		 */
		private function isAnyColumnSelected():Boolean{
			var ac:ArrayCollection = DataGrid(DataGridListData(listData).owner).dataProvider as ArrayCollection;
			var b:Boolean=false;
			for each (var o:Object in ac){
				if(o.checked){
					b=true;
					break;
				}
			}
			return b;
		}
	}
}

Code for itemRenderer:

<!--?xml version="1.0" encoding="utf-8"?-->

		<![CDATA[
			import mx.collections.ArrayCollection;
			import mx.controls.DataGrid;
			import mx.controls.dataGridClasses.DataGridListData;
			import mx.events.FlexEvent;

			/**
			 * Handler function for <code>MouseEvent.CLICK</code> on checkbox.
			 *
			 * @param event MouseEvent
			 *
			 */
			public function clickHandler(event:MouseEvent):void{
				var o:Object = DataGrid(DataGridListData(listData).owner).selectedItem;
				o.checked=cb.selected;
				ArrayCollection(DataGrid(DataGridListData(listData).owner).dataProvider).setItemAt(o,DataGrid(DataGridListData(listData).owner).selectedIndex);
			}

			/**
			 * Overides <code>data</code> parameter to set the <code>selected</code> parameter of the
			 * checkbox.
			 *
			 * @param value Object
			 */
			override public function set data(value:Object):void{
				if(value!=null){
					super.data=value;
					cb.selected=value.checked;
				}
			}
		]]>

You can download the code from my post in Adobe cook book.