return undefined;


Creating truly reusable renderers with ClassFactory

Posted in AS3, Flex by Ben Clinkinbeard on the November 14th, 2006

Update: I have come up with better solutions alternate approaches since writing this post. For a more compact and self-contained approach check out this post as well.

In my last post, I outlined some ways to ensure that your item renderers behave predictably. In that article, however, I admitted that I had yet to figure out the best way to create renderers that were completely decoupled from the data they represented and therefore wholly reusable across projects. As of yesterday afternoon, I believe that has changed. I have extended my previous example to demonstrate, and the new version can be seen here. Right-click for source.

My current project requires a DataGrid with the commonly seen select/unselect all functionality. The design depicts this simply as a column that is a centered CheckBox as both the itemRenderer and headerRenderer. When you click the CheckBox in the header, it toggles the state of all the items to match its own. I first addressed the item renderers. I managed to create a component that was completely decoupled (hooray!), predictable, and easy to use. It implemented the IDropInListItemRenderer interface, which allowed it to be tied to the appropriate property in its data item through the familiar dataField attribute of DataGridColumn. When I moved on to implement the headerRenderer, however, I ran into a bit of a problem. This turned out to be a blessing in disguise though, as it led me to the real star of the show here: ClassFactory. ClassFactory essentially just allows you to specify a class (that implements IFactory), specify some properties for it, and then assign it as the itemRenderer or headerRenderer for your column.

The headerRenderer is probably the simpler of the two, so I'll go into more detail on that first. The problems I initially had with my headerRenderer were related to the fact that headerRenderers seem to be recreated, or at least reinitialized, every time there is an event in the DataGrid in which they live. (If anyone can more accurately explain how they behave please post in the comments.) As a result, you must bind their state to a value that is held outside of said DataGrid. In my case, I called this variable allSelected. You also need to set up the ClassFactory variable that you will assign as the headerRenderer. Here is what that code looks like:

Actionscript:
  1. [Bindable] private var checkBoxHeaderRenderer:ClassFactory;
  2. ...
  3. checkBoxHeaderRenderer = new ClassFactory(GenericCheckBoxHeaderRenderer);
  4. checkBoxHeaderRenderer.properties = {externalObject: this, externalPropertyName: "allSelected"};

This tells Flex to use the GenericCheckBoxHeaderRenderer class as the renderer, and to set 2 properties on each instance it creates. It should be apparent that I am giving each instance a reference to the allSelected property mentioned above. Here is what our GenericCheckBoxHeaderRenderer class looks like.

Actionscript:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="100%" horizontalAlign="center" preinitialize="init()" implements="mx.core.IFactory">
  3.     <mx:Script>
  4.         <![CDATA[
  5.             import mx.binding.utils.BindingUtils;
  6.                        
  7.             // will be set by "properties" property of ClassFactory
  8.             // allows us to bind to an external value while remaining decoupled
  9.             public var externalObject:*;
  10.             public var externalPropertyName:String;
  11.            
  12.             // local property that will be bound to external value held in externalObject[externalPropertyName]
  13.             // CheckBox's selected property in turn bound to this
  14.             [Bindable] public var isSelected:Boolean;
  15.            
  16.             // method required by IFactory
  17.             public function newInstance():*
  18.             {
  19.                 return new GenericCheckBoxHeaderRenderer();
  20.             }
  21.            
  22.             // set up binding of local property to value specified by ClassFactory::properties
  23.             private function init():void
  24.             {
  25.                 BindingUtils.bindProperty(this, "isSelected", externalObject, externalPropertyName);
  26.             }
  27.                        
  28.             // local click handler that dispatches the event
  29.             // so it can be handled in a  more appropriate place (such as the document holding the DataGrid)
  30.             private function onClick(event:MouseEvent):void
  31.             {
  32.                 var evt:SimpleHeaderClickEvent = new SimpleHeaderClickEvent(cb);
  33.                 dispatchEvent(evt);
  34.             }
  35.         ]]>
  36.     </mx:Script>
  37.     <mx:CheckBox id="cb" click="onClick(event)" selected="{isSelected}" width="15"/>
  38. </mx:HBox>

One thing to note in that class is that I have created a local variable that is bound to the external value (allSelected) that is passed in via the properties property, and my CheckBox then binds to that local value.

The itemRenderer version of this code is very similar, the main difference being that it is tied to a specific property on the data object that is passed to it in the list rather than a single, external value. As a result, we only need to pass it one value, that being the name of the property to which we want to tie the renderer. The ClassFactory variable is set up in the same fashion:

Actionscript:
  1. [Bindable] private var checkBoxItemRenderer:ClassFactory;
  2. ...
  3. checkBoxItemRenderer = new ClassFactory(GenericCheckBoxItemRenderer);
  4. checkBoxItemRenderer.properties = {dataField: "isKnown"};

and the GenericCheckBoxItemRenderer class is also similar:

Actionscript:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="100%" horizontalAlign="center" implements="mx.core.IFactory">
  3.     <mx:Script>
  4.         <![CDATA[
  5.             import mx.controls.dataGridClasses.DataGridColumn;
  6.            
  7.             // name of property on our VO we're concerned with
  8.             public var dataField:String;
  9.            
  10.             // method required by IFactory
  11.             public function newInstance():*
  12.             {
  13.                 return new GenericCheckBoxItemRenderer();
  14.             }
  15.            
  16.             // this override is essential for preventing random (un)checking when your DataGrid is scrolled
  17.             override public function set data(value:Object):void
  18.             {
  19.                 // not sure of the details on when or why, but sometimes this method is passed a null value
  20.                 if(value != null)
  21.                 {
  22.                     super.data = value;
  23.                     // the parent DataGridColumn is passed as value before the real data arrives
  24.                     if(!(value is DataGridColumn))
  25.                     {
  26.                         // set itemRenderer's state based on the value held in the property specified by _dataField
  27.                         cb.selected = value[dataField];
  28.                     }
  29.                 }
  30.             }
  31.            
  32.             // local click handler that dispatches the event
  33.             // so it can be handled in a  more appropriate place (such as the document holding the DataGrid)
  34.             private function onClick(e:MouseEvent):void
  35.             {
  36.                 // attach our VO and _dataField onto the event so that it can be manipulated in the handler
  37.                 // wherever that handler may be
  38.                 var evt:SimpleItemClickEvent = new SimpleItemClickEvent(super.data, dataField);
  39.                 dispatchEvent(evt);
  40.             }
  41.         ]]>
  42.     </mx:Script>
  43.     <mx:CheckBox id="cb" click="onClick(event)" width="15"/>
  44. </mx:HBox>

Here you can see that the override method no longer relies on any specific property (and therefore datatype), instead using the property specified by dataField on whatever type of object is passed to the itemRenderer by the dataProvider.

And that's my system. If you look at the source code you can see that we still handle the events in the parent file of our DataGrid, but note that these event handlers are the only place where we are tightly coupled to specific properties and/or data types, which is exactly what we want. We have moved all type references out of our renderers, making them completely reusable across varying parts of a project or in different projects altogether. It would likely be very easy to genericize the renderers even further to allow specifying what type of control should be used (CheckBox, ComboBox, etc), which property of said control you're interested in and on and on all through the properties property, but I decided not to go that far. There is something to be said for retaining semantic meaning in your class/component names and code and not extrapolating to oblivion.

Sorry these posts are so long. As it turns out, conciseness is not a trait I possess. Hopefully someone will find this information useful though, and feel free to post any questions or other thoughts in the comments. Enjoy!

37 Responses to 'Creating truly reusable renderers with ClassFactory'

Subscribe to comments with RSS or TrackBack to 'Creating truly reusable renderers with ClassFactory'.

  1. Sam Shrefler said,

    on November 21st, 2006 at 11:32 am

    Ben:

    Great article! I have run into a problem using your code in an example. For some reason, the last row's checkboxes sometimes seem to display being Checked and enabled=false. I haven't been able to figure out why....still troubleshooting.

    Thanks for the great article though!

    Sam

  2. Ben said,

    on November 21st, 2006 at 12:31 pm

    Thanks Sam, I'm glad you enjoyed it. Not sure why your checkboxes would be getting diabled, there definitely isn't any code in my sample that addresses the enabled proprty. Maybe something in my previous article could help? :)

    Ben

    PS - I don't have time to read them right now but your past articles look useful; I've added your site in my Bloglines account.

  3. Dan said,

    on November 21st, 2006 at 1:17 pm

    Very cool! I've been looking for a good solution to this problem. Thank you for helping folks like me. I've used your code against a datagrid form generated by the ColdFusion wizard and it works well when editing pre-existing data. However, when a new form is called, selecting the itemRenderer checkbox causes all other cells to become uneditable. The checkbox is still enabled, everything else is not. The headerRenderer works as advertised and does not cause any issues that I have seen.
    Maybe because I'm using the actual dataprovider for the grid instead of the model? I dunno.

  4. Ben said,

    on November 22nd, 2006 at 12:22 am

    Hi Dan, glad you got something out of the post. I've never touched CF in my life, so I can't really help you there. Also not totally sure I understand your last sentence.

  5. Darren Bishop said,

    on November 22nd, 2006 at 6:39 am

    Hi Ben,

    I just found your updated / new article after commenting on the old one, so apologies for what I have repeated.

    I will be looking into the Class/IFactory mechanism.

    Cheers, Darren

  6. Ben said,

    on November 22nd, 2006 at 8:53 pm

    No worries Darren, you provided some good information that I know a lot of people haven't quite grasped yet. I think IDropInListItemRenderers are probably an adequate solution for a lot of scenarios, but once I discovered ClassFactory it just seemed more appropriate for what I was trying to accomplish.

    One other thing to keep in mind is that rowIndex is simply the row in the DataGrid, and is not tied to the dataProvider in any way. As for Aaron's scenario, while I was developing the example for this post I discovered that making the class that is being used in the dataProvider bindable makes it super simple to accomplish. Then you just have to modify the second data field in the click handler and the second field's renderer should update itself.

    Thanks,
    Ben

  7. Ben Densmore said,

    on November 24th, 2006 at 5:30 pm

    Hi Ben,
    I just happened to stumble across your site while doing some searching on how to use an itemRenderer. I'm still struggling a bit on using them. Would I handle a checkbox itemRenderer in the same manner if I am selecting multiple rows and using a "click" event on a button to remove the rows selected?

    Thanks,
    Ben

  8. Ben said,

    on November 24th, 2006 at 9:19 pm

    Hi Ben,

    I am actually doing just that in the project I'm currently working on. As the articles show, the CheckBox itemRenderer changes a property in the dataProvider. I am assuming you mean a button located outside of the DataGrid. When you click that button you'll just need to iterate over the dataProvider collection and use removeItem or something similar to get rid of items with the property value you've set. Something like this:

    Actionscript:
    1. var ac:ArrayCollection = model.myDgDataProvider;
    2. for each(var item:PersonVO in ac)
    3. {
    4.     if(item.isSelected)
    5.     {
    6.         ac.removeItem(item);
    7.     }
    8. }
    9. ac.refresh();

    Hope that helps, let me know if it doesn't.

    Ben

  9. Ben Densmore said,

    on November 25th, 2006 at 1:13 am

    So then do I still need an onClick event handler to update the dataProvider and let it know that the state has changed on each check box? I guess I'm still lost on how these work.

    I have a DataGrid which is populated with an ArrayCollection which was actually converted from a Query Result to the ArrayCollection. The DataGrid has a bunch of fields, one of them being a checkbox that is a custom component and inserted into the DataGridColumn via itemRenderer.

    I then have a "delete" button that sits outside of the DataGrid, when clicked a "deleteFormItems()" function is called. will my itemRenderer component have all ready updated the dataProvider to tell it which check boxes were checked so I can then loop over the dataProvider as you show in the example above?

    I'm trying not to sound dumb but I really want to understand what I'm doing here and not just copy your example.

    Thanks,
    Ben

  10. Ben said,

    on November 25th, 2006 at 2:59 pm

    I think you've pretty much got it, Ben. You asked "do I still need an onClick event handler to update the dataProvider and let it know that the state has changed on each check box?" and "will my itemRenderer component have all ready updated the dataProvider to tell it which check boxes were checked". The answer to both is yes. Your itemRenderer is simply a visual representation of a specific property on a specific item in your dataProvider. In fact, all cells in a DataGrid are itemRenderers, its just that the default "rendering behavior" is to display the value as text.

    The main example from this article changes the isKnown property of items in the handler of the checkbox. If we wanted to delete the checked items when we click another button, we could define a deleteKnownItems() method that looks exactly like the example in my last comment, except it would look at the isKnown property instead of isSelected.

    So to reiterate, you would be updating (a single item in) your dataProvider each time you click on a checkbox, and then in your button click handler you would just be looping over your dataProvider and modifying it (deleting items) whenever you find an item that meets the criteria you're interested in. Make sense?

  11. Ben Densmore said,

    on November 26th, 2006 at 10:44 pm

    Ben,
    Thank you very much for clearing that up for me. I think I understand it better. I just need to implement it into my App to really grasp it.

    I appreciate you taking the time to explain this more to those of us that are a bit slower in absorbing new concepts.

    Ben

  12. Ben said,

    on November 27th, 2006 at 9:45 am

    My pleasure, Ben. It wasn't so long ago that this was all foreign to me as well. Glad I could help!

  13. Ben Densmore said,

    on November 28th, 2006 at 11:49 am

    Hi Ben,
    If I'm not using a value object how can I pass a property into the ClassFactory? I have an ArrayCollection but I'm not populating a Value Object as you do. I'm still struggling a bit to get this to work with my situation because it's not identical to your example.

    Thanks,
    Ben

  14. Ben Densmore said,

    on November 28th, 2006 at 12:19 pm

    Nevermind, I got it working. I just set a Bindable variable to default the state to false and then update the state of that variable.

    Thanks for all you patience and help,
    Ben

  15. Asgar said,

    on December 6th, 2006 at 1:47 am

    Ben,

    Its a very useful article. I am trying custom components which can be used across multiple apps and this came very handy. Still struggling a bit on the way we retrieve the data from ItemRenderers. Can you point me any link/code which has this ? My ItemRenderer has a Checkbox and a Label, indicating that the label was selected, and to make it more complex, the DataGrid Columns are generated dynamically in my case :(

    Regards,
    Asgar.

  16. Ben said,

    on December 6th, 2006 at 11:19 pm

    Hi Asgar,

    I am not quite sure I know what you mean about retrieving data from the ItemRenderers. Your IRs should be updating a model object somewhere and then that is where other things retrieve data from. If you mean that you have a label in one column that displays the state of a checkbox in another column, you would just want to put logic in the label IR's data method override that checks the same property as the one the CB toggles.

    Let me know if you have more specific questions or if I totally misunderstood what you were asking.

    Thanks.

  17. learner of Flex said,

    on December 14th, 2006 at 4:10 am

    Hi everyone,

    Its a very useful article. But if a DataGrid has a lot of DataGridColumn and each them have a CheckBox headerRenderer for control a column of CheckBox select/unselect.
    how to get dataField in headerRenderer to indicate which column is user click ?

    PS:My English learns of not good and I suppose your are not understand!

  18. Ben said,

    on December 14th, 2006 at 9:05 am

    You're right, I don't quite understand what you mean :) The click handler for the header should run the through the collection being used as the dataProvider and set whichever property it is associated with to the proper value. The collection is then refreshed and the itemRenderers update automatically.

    HTH.

  19. learner of Flex said said,

    on December 17th, 2006 at 10:46 pm

    Hi Ben,

    I know what your meaning is and I can select/unselect a column successfully,but in your code, the dataField is not automatically.
    "checkBoxItemRenderer = new ClassFactory(GenericCheckBoxItemRenderer);
    checkBoxItemRenderer.properties = {dataField: "isKnown"};"
    What method resolve this problem that get dataField when user click a CheckBox?

  20. Ben said,

    on December 18th, 2006 at 12:22 am

    I'm not sure exactly what you mean, but I will take a shot. The dataField property in the properties object is not the same thing as the dataField attribute of DataGridColumn. I suppose I could've named it differently to avoid confusion, but it is serving the same purpose. If you can clarify your question I will do my best to answer it.

    Thanks.

  21. PeteProgram said,

    on January 3rd, 2007 at 10:10 am

    Hello Ben

    I'm not realy shure whether I got your problem wrong, but instead of storing the selected variable outside the datagrid, you also could take a static variable maybe inside your renderer-class. You may store the state of the CheckBox here and use it every it becomes initialized...

    Cheers Pete

  22. Ben said,

    on January 4th, 2007 at 1:00 am

    Hi Pete,

    You could do that, but it would make your renderer class much less generic and reusable. You would need to create a separate static var for each place it is used, which could get ugly. I also think storing the value outside keeps your renderer class truer to its intended purpose of rendering, and prevents having to mix logic into a class that is really just a view component.

  23. Scott said,

    on January 9th, 2007 at 5:18 pm

    Ben, thanks for the example, I'm doing my best to pick up on this. Is there an easy way in the onItemClick function that I could change other column values with the event?

    What I have is two numeric columns and I would like to set one to the value of the other if my checkbox column is selected, and set it to zero if it is not. Thanks for any help.

  24. Ben said,

    on January 9th, 2007 at 9:17 pm

    Hi Scott, you can definitely do that. The key to understanding this subject (in my opinion) is to never think about updating columns or the datagrid or similar things. Those items are nothing but a visual representation of the underlying data, which is the only thing that you should manually need to update. So assuming your dataProvider is a collection of instances of a class marked [Bindable], all you need to do is update the appropriate properties and your DataGrid columns should update accordingly.

    if(cb.selected)
    {
    e.vo.someProperty = e.vo.someOtherProperty;
    }
    else
    {
    e.vo.someProperty = 0;
    }

  25. Me said,

    on July 17th, 2007 at 1:06 pm

    You need to review using containers in grids.
    Alex Harui has written some blog articles that will clear some things up.

    I have been hit hard by using containers in the grids. This is not a recommended method for working with grids.

    http://blogs.adobe.com/aharui/2007/03/thinking_about_item_renderers_1.html

  26. Ben said,

    on July 17th, 2007 at 10:02 pm

    Hi "Me",

    I'm familiar with Alex's article (Ben in the comments there is me). Not sure what you mean by "hit hard" but I've not had any issues using the above approach with 10 - 15 visible rows. I do agree that Alex's approach (and pretty much everything else he says) should be required reading for Flex developers though.

    Ben

  27. Swathi said,

    on August 2nd, 2007 at 11:12 am

    Hi,
    In my application I use the same check box renderer in more than one column. These check boxes are added dynamically. When ever I check the check box true, the the other checkboxes in same row are getting checked. Can anyone of you tell the solution for this problem. I dont have this problem while scrolling since I have overrriden the set data function.

    Thanks in advance.

  28. Morgan said,

    on September 4th, 2007 at 5:41 am

    Hi Ben,

    Great example!

    However, when used with the new FLEX 3 SDK, it breaks. Clicking the header containing the checkbox will throw the all to familiar: "Error #1009: Cannot access a property or method of a null object reference".

    It seems that Datagrid.as (3909) cant find the renderer when trying to initiate a new DataGridEvent.ITEM_EDIT_BEGINNING.

    Any solution to this?

    //Morgan

  29. Ben said,

    on September 6th, 2007 at 11:10 pm

    Hi Morgan,

    Sorry, but I don't have time to investigate right now. I haven't really jumped on the Flex 3 wagon yet as I've been pretty buried at work and with a soon-to-be-announced side project. If you find a solution please let me know, sorry I can't offer more help.

    Thanks,
    Ben

  30. Amy said,

    on October 12th, 2007 at 11:48 am

    It seems redundant to have such similar renderers for header and item. Is it possible to make one renderer that could do double duty?

  31. Ben said,

    on October 12th, 2007 at 6:21 pm

    It might be possible but I haven't spent any time trying to accomplish that. As I mentioned in the article, the creation/update process seems to be very different for headerRenderers. Specifically, they are updated much more often than itemRenderers.


  32. on November 16th, 2007 at 4:26 pm

    [...] As it turns out, truly reusable CheckBox renderers are much simpler to create than I previously reported. This post will provide and discuss what I think are the last CheckBox renderers you will ever need. [...]

  33. Tom Gilmour said,

    on February 1st, 2008 at 8:54 pm

    Hallelujah, that's exactly what I needed. Actually I just needed how to set the properties of the ClassFactory object so I could reuse my custom itemRenderer in multiple columns. But still, you've just opened up my world. Thanks!

  34. Mohammad said,

    on October 24th, 2008 at 5:34 am

    Great Article.
    I'm new to Flex3 and I have a question for you and I hope u own the answer ..
    I want to add a datagrid as ItemRenderer to another main Datagrid ( in other words, nested datagrids).

    how should the ItemRenderer Class looks and how to set the dataprovider to the of the sub-Datagrid .

    Thanks in advance ..


  35. on October 24th, 2008 at 3:04 pm

    Hi Mohammad,

    While this is technically possible I would strongly recommend against it. You will likely run into performance issues very quickly and I also don't think it would make for a good user experience to try and decipher nested grids. Perhaps look into the AdvancedDataGrid which allows you to selectively expand and collapse rows.

    HTH,
    Ben

  36. Mohammad said,

    on October 27th, 2008 at 10:19 am

    Thanks Ben
    But for my case I have to use the nested datagrid; there are some design issues block me for making a use of the advancedatagrid .

    if you can tell me how should the ItemRenderer Class looks like, this will be great and appreciated.

    May thanks in advance.

  37. Mohammad said,

    on October 29th, 2008 at 6:45 am

    I've done it.

    Thanks anyway ;)

Leave a Reply