Component creation tutorial
From PRADO Wiki
Contents |
Introduction
Yea! That's it. I finally made it. My (not first) custom component! It was a mess...
Well, the situation is not that bad. PRADO v3 brought component creation to new level and now you can create pretty complex components in a breeze. As long as you don't have to deal with one of two greater PRADO beasts: events and content-in-content (I don't really know how this is called, so please fix me ;-)). This is where it gets dirty...
Let's head on.
Our goal
First things first. What do we want? We need a component, that should wrap it's body with some HTML and then repeat it a few times. Whee! That is not so hard.. At least sounds not.
In our typical PHP code this would look something like this:
<?php foreach ($values as $data) { print "{$data[0]}<br />\n"; print $general_contents; print "{$data[1]}<br />\n"; } ?>
This is very simple example, but you get the idea. In PRADO it would look something like this:
<com:TRepeater> <prop:ItemTemplate> <%# $this->DataItem[0] %><br /> Contents go here... <%# $this->DataItem[1] %><br /> </prop:ItemTemplate> </com:TRepeater>
This is abstract case. In reality, I was writing a control for my network-management application. Essentialy, each network has physical users and these users live or work in houses with flats. So each house can have n number of users which each can have different number of PCs. Here we are going to write a component which wraps around body content with 'holders' which output a lot of div's and few useful facts about house and it's inhabitors.
As PRADO gives you a lot of power it also gives you a lot of flexibility. How do you reuse that piece of code? You create a component, of course! This is where the scene is highlighted and action begins...
(Somehow) Twisted world of PRADO components
So as I said first things should go first. How do we create new component? PRADO QuickStart says that there are two ways: template composition and createChildControls() overriding. We are going to pick first one.
This is little piece of nifty template code which i called MTHouses.tpl:
<com:TRepeater ID="Houses"> <prop:ItemTemplate> <div class="Holder"> <div class="Header"> <h1><%# $this->DataItem['name'] %></h1> <div class="Stats"> <ul> <li>Users: <%# $this->DataItem['users'] %></li> <li>PCs: <%# $this->DataItem['pcs'] %></li> </ul> </div> </div> <div class="Content"> <!-- Some content should go here. --> </div> </div> </prop:ItemTemplate> </com:TRepeater>
So far so good. Let's take a look to our PHP code:
<?php class MTHouses extends TTemplateControl { public function OnInit($param) { parent::OnInit($param); if (! $this->Page->IsPostBack) { $db = getDB(); $DS = array(); // [Database mangling starts here] $res = $db->getAll( 'SELECT `id`,`name` from `houses` ORDER BY `name` ASC' ); foreach ($res as $row) $DS[ $row['id'] ] = array_merge( $row, array( 'users' => 0, 'pcs' => 0 ) ); $res = $db->getAll( 'SELECT `house_id` as `id`,COUNT(`house_id`) as `users` ' . 'FROM `users_physical` ' . 'WHERE `house_id` IN (!) ' . 'GROUP BY `house_id`', array( implode(',', array_keys($DS)) ) ); foreach ($res as $row) $DS[ $row['id'] ]['users'] = $row['users']; // [And it ends here... You can skip this part if you want] $Houses = $this->Houses; $Houses->setDataSource($DS); $Houses->dataBind(); } } } ?>
So what happens here? Basically we're just getting our data from database (I use PEAR_DB for that) and set datasource for TRepeater Houses.
Let's try to use it. We add <com:MTHouses /> to our .page and the houses are represented. Everything works. Hooray for me!
Uh, wait. I forgot one thing. Previously I commented one place where I said that I'll put body content into. Ok, let's try to do that.
Putting components body content there where it belongs
First I took wrong approach and just written content into component. Then I pressed 'Reload' button and prayed for dark gods of my. They did not help. I saw the result, but it was, er..., not what I have expected. What has PRADO did was that it has put body content into Controls first and my .tpl content into Controls second. Then it rendered it. Thas was not the answer.
I faced myself looking at the wall. So I went to the forum and in usual crybaby manner written a new post. Thankfully jake helped me.
So I found the way. I have inserted my body content between <prop:StaticTemplate> tags. As I later got found of I simply was lucky. As you see PRADO automatically parses component properties which end in template as TTemplate controls. Otherwise I might just have found myself struggling against other brain-twister how to make components appear instead of plain string.. So I glued some code to MTHouses.php:
public function getStaticTemplate() { return $this->getViewState('StaticTemplate'); } public function setStaticTemplate($value) { $this->setViewState( 'StaticTemplate', TPropertyValue::ensureObject($value) ); }
I'm not sure why I chose ViewState to save StaticTemplate but it works and I'm totally happy. Ok... How to turn this template into something that our browser can render? (... here I open a choclate bar and start chewing it... damn, all writers must be fat! ...) After some brainstorming I had brilliant Idea to instantiate that template into some component into some another content. We need few fixes to template. Let's change
<div class="Content">
<!-- Some content should go here. -->
</div>
to
<com:TPanel ID="Content" CssClass="Content" />
Ok, so somehow we need to call ->instantiateIn() method. I decided to do it when TRepeater creates it's items, because I found no other way to reach our Content TPanel. So here goes other fix to the .tpl:
<com:TRepeater ID="Houses">
to
<com:TRepeater ID="Houses" OnItemCreated="Parent.HouseCreated">
Our component is refered as Parent in template. Then in PHP code we should define our event handler:
public function HouseCreated($sender, $param) { $this->StaticTemplate->instantiateIn( $param->item->Content ); }
Save all files, reload, voila! It works! Hehe, I must be genius (or Qiang is...).
As we go deeper and deeper...
Yarrgh! We be pirates. Oops, I forgot this is PRADO wiki and not some Mark Oliver story ;-)
Anyway, back to work. Everyone is happy when we render static StaticTemplate (these two fit, huh?). But what should we do if we want to change StaticTemplate by the house ID we are getting?
This part made me crazy. Finally, somehow, I figured it out. Let's take a look at our .page:
<com:MTHouses ID="Houses" OnItemCreated="Page.HouseCreated"> <prop:StaticTemplate> <table> <tr> <th>Name</th> <th>Surname</th> <th>Flat</th> <th>Phone</th> </tr> <com:TRepeater ID="PhysicalUsers"> <prop:ItemTemplate> <tr> <td><com:TTextBox Text=<%# $this->DataItem['name'] %> /></td> <td><com:TTextBox Text=<%# $this->DataItem['surname'] %> /></td> <td><com:TTextBox Text=<%# $this->DataItem['flat'] %> /></td> <td><com:TTextBox Text=<%# $this->DataItem['phone'] %> /></td> <td class="remove"><img src="images/remove_16.png" /></td> </tr> </prop:ItemTemplate> </com:TRepeater> </table> </prop:StaticTemplate> </com:MTHouses>
And in PHP .page side:
public function HouseCreated($sender, $param) { // Some dummy code we could see it works. print 'Wheeeee!<br />'; }
This is pretty common case if we look at MTHouses like to TRepeater and StaticTemplate like ItemTemplate. But the fact is, that it is not. MTHouses does not have event, called OnItemCreated. At least now...
The question is, when the event should be invoked? I found it out, that it would be best if it was invoked right after component inner TRepeater OnItemCreated event. Party on!
Adding (rerouting) custom events to custom components
Ok. I have something, that I want to say. I just made all this page up.
Did that scare ya? If yes - don't be afraid, I'm just kidding. If not - well, congratuliations. You have nerves of steel.
Fun is over, boys. So... where were we. Oh, rerouting events. Right!
The first bummer was when I tried to set event handler for MTHouses.OnItemCreated. PRADO told me that my component does not have such event. Quick look at quickstart tutorial gave me idea, that i should put this into my components class file:
public function OnItemCreated($param) { $this->raiseEvent('OnItemCreated', $this, $param); }
What does it do? Qiang's explanation below:
This defines an event called ItemCreated. Note, to make the event happen (raised), you will have to call this method in your code somewhere (this is called triggering the event). If any event handler is attached to this event, at the place where your event is triggered, the event handler will be invoked by PRADO (through $this->raiseEvent).
Ok, Reload. The page loaded, though nothing happened. Whee did not appear. I feel sad :-(. Oh, right, we forgot to actually call this method. Another piece of code (I'm almost out of glue...). Lets add this into end of public function HouseCreated:
$this->OnItemCreated($param);
Reload. It works! Whees everywhere! Yay! Balloons and happy faces. I think I saw a clown somewhere... Oh, this is no clown, that's my boss. GTG... Be back in year or two.
So. What's now? Now we need somehow populate StaticTemplate's TRepeater. Let's change our fictional .page event handler:
public function HouseCreated($sender, $param) { $DS = getDB()->getAll( 'SELECT * FROM `users_physical` WHERE `house_id`=?', array($param->item->DataItem['id']) ); $PhysicalUsers = $param->item->Content->PhysicalUsers; $PhysicalUsers->setDataSource($DS); $PhysicalUsers->dataBind(); }
The DataItem is passed by parameter from components TRepeater and we can access everything that is in it's ItemTemplate. There is one side-effect of our trick with instantiating StaticTemplate inside Content TPanel: it has became it's parent. So instead of accessing it like $param->item->PhysicalUsers we have to take longer way and access it like this: $param->item->Content->PhysicalUsers.
Reload, everything works, everyone is happy. I hope so ;-)...
Final code listings
Home.page:
<com:MTHouses ID="Houses" OnItemCreated="Page.HouseCreated"> <prop:StaticTemplate> <table> <tr> <th>Name</th> <th>Surname</th> <th>Flat</th> <th>Phone</th> </tr> <com:TRepeater ID="PhysicalUsers"> <prop:ItemTemplate> <tr> <td><com:TTextBox Text=<%# $this->DataItem['name'] %> /></td> <td><com:TTextBox Text=<%# $this->DataItem['surname'] %> /></td> <td><com:TTextBox Text=<%# $this->DataItem['flat'] %> /></td> <td><com:TTextBox Text=<%# $this->DataItem['phone'] %> /></td> <td class="remove"><img src="images/remove_16.png" /></td> </tr> </prop:ItemTemplate> </com:TRepeater> </table> </prop:StaticTemplate> </com:MTHouses>
Home.php:
<?php class Home extends TPage { public function HouseCreated($sender, $param) { $DS = getDB()->getAll( 'SELECT * FROM `users_physical` WHERE `house_id`=?', array($param->item->DataItem['id']) ); $PhysicalUsers = $param->item->Content->PhysicalUsers; $PhysicalUsers->setDataSource($DS); $PhysicalUsers->dataBind(); } } ?>
MTHouses.tpl:
<com:TRepeater ID="Houses" OnItemCreated="Parent.HouseCreated"> <prop:ItemTemplate> <div class="Holder"> <div class="Header"> <h1><%# $this->DataItem['name'] %></h1> <div class="Stats"> <ul> <li>Users: <%# $this->DataItem['users'] %></li> <li>PCs: <%# $this->DataItem['pcs'] %></li> </ul> </div> </div> <com:TPanel ID="Content" CssClass="Content" /> </div> </prop:ItemTemplate> </com:TRepeater>
MTHouses.php:
<?php class MTHouses extends TTemplateControl { public function OnInit($param) { parent::OnInit($param); if (! $this->Page->IsPostBack) { $db = getDB(); $DS = array(); $res = $db->getAll( 'SELECT `id`,`name` from `houses` ORDER BY `name` ASC' ); foreach ($res as $row) $DS[ $row['id'] ] = array_merge( $row, array( 'users' => 0, 'pcs' => 0 ) ); $res = $db->getAll( 'SELECT `house_id` as `id`,COUNT(`house_id`) as `users` ' . 'FROM `users_physical` ' . 'WHERE `house_id` IN (!) ' . 'GROUP BY `house_id`', array( implode(',', array_keys($DS)) ) ); foreach ($res as $row) $DS[ $row['id'] ]['users'] = $row['users']; $Houses = $this->Houses; $Houses->setDataSource($DS); $Houses->dataBind(); } } public function HouseCreated($sender, $param) { $this->StaticTemplate->instantiateIn( $param->item->Content ); $this->OnItemCreated($param); } public function OnItemCreated($param) { $this->raiseEvent('OnItemCreated', $this, $param); } public function getStaticTemplate() { return $this->getViewState('StaticTemplate'); } public function setStaticTemplate($value) { $this->setViewState( 'StaticTemplate', TPropertyValue::ensureObject($value) ); } } ?>
Conclusion
...or where the pirates have hidden their bounty...
Of course this is article from PRADO user and not from one of PRADO creators but I hope I'm accurate. Also I just could have extended TRepeater in my component and overriden ItemTemplate instead of this mumbo-jumbo with TTemplateControl but that would be a lot of rewriting if I needed something outside of my TRepeater and still in that component. Anyway, I hope it will help you a lot.
Sincerely, Artūras Šlajus.
(... and they lived happilly ever after ...)

