Click here to go back to the previous section: Languages in Extensions
Please note that this documentation is for Vanilla 1
As mentioned in the Order of Operations document, there are two types of generic controls in Vanilla. With controls you can add new user interfaces to Vanilla in the form of new widgets anywhere on the screen, custom data lists, new forms, etc. Let’s start with the most basic type of Control:
Aside from some utility methods, the Control Class (located at library/Framework/Framework.Class.Control.php) is pretty much a straight-up rendering control. In other words, you can extend the control class for your purposes and add a bunch of stuff to render. Then add your Control to the Page object, and you’re done.
In order to better understand the Control class and how it works, let’s take a look at some actual Controls extended from the Control class in Vanilla. The PageEnd control is a good example of an extremely simple Control extended from the Control class. You can take a look at it in library/Framework/Framework.Control.PageEnd.php, and this is what you’ll see:
class PageEnd extends Control { function PageEnd(&$Context) { $this->Name = 'PageEnd'; $this->Control($Context); } function Render() { $this->CallDelegate('PreRender'); include(ThemeFilePath($this->Context->Configuration, 'page_end.php')); $this->CallDelegate('PostRender'); } }
As you can see, that’s pretty simple. This control is instantiated in the appg/init_vanilla.php file, like this:
$PageEnd = $Context->ObjectFactory->CreateControl($Context, 'PageEnd');
As you can see, the ObjectFactory has been used to instantiate the class. It is later added to the page control in specific pages where it is required. For example, it is added to the discussions index in the index.php like this:
$Page->AddRenderControl($PageEnd, $Configuration['CONTROL_POSITION_PAGE_END']);
Now let’s take a look at another, more complex example: The DiscussionGrid Control. You can find the DiscussionGrid control in library/Vanilla/Vanilla.Control.DiscussionGrid.php. The DiscussionGrid is an excellent example of how the Control class can be used to trap fatal errors before a control is rendered by the Page Object.
The idea here is that I loaded all of the discussions from the database in the Constructor of my DiscussionGrid control. All possible errors that could have happened while connecting to the database or parsing data from the database were handled before any of the page was actually rendered. So, by the time the page object is firing it’s render events, there are no more errors waiting to pop up and mess up the page - only text to be rendered.
The DiscussionGrid control is added to the Page object in a similar way to the PageEnd control. Open up index.php and you’ll see that the DiscussionGrid is instantiated by the ObjectFactory...
$DiscussionGrid = $Context->ObjectFactory->CreateControl($Context, 'DiscussionGrid');
... and added to the Page control:
$Page->AddRenderControl($DiscussionGrid, $Configuration['CONTROL_POSITION_BODY_ITEM']);
There are a ton of Controls extended from the Control class in Vanilla that serve a myriad of purposes: The Head Control (library/Framework/Framework.Control.Head.php), Panel Control (library/Framework/Framework.Control.Panel.php), and Foot Control (library/Vanilla/Vanilla.Control.Foot.php) to name a few. You can use the Control class as a base for just about any kind of control you want to add to the page.
The PostBackControl class (also located in the library/Framework/Framework.Class.Control.php file) is actually extended from the Control class. This means that it inherits all of the properties and methods of Control. This class is by far the most common type of Control in Vanilla because it only renders when required. Some examples of PostBackControls are all of the forms that you see when you click on the panel items in your account tab. When you click one of those buttons, all of your account information disappears and you are presented with the appropriate form, but if you watch the url you will notice that the page you are looking at hasn’t actually changed at all. The forms render based on PostBackActions that are defined in the relevant PostBackControl.
Let’s take a look at the properties and methods of the PostBackControl:
| Property | Description |
|---|---|
| PostBackAction | (Public) Inherited from Control, this property is always retrieved from the querystring when the class is constructed. The control can then look at the value retrieved from the querystring and decide what action to take, if any is required. |
| ValidActions | (Private) All classes must define ValidActions, which is an array of PostBackActions which apply to that control. This array should be hard-coded in the constructor for the PostBackControl. |
| IsPostBack | (Public) IsPostBack is a property that is set to true if any of the values in ValidActions matched the PostBackAction value retrieved from the querystring. |
| PostBackValidated | (Private) PostBackValidated is a convenience property that control authors can use as a boolean indicating that, if a form was posted back and database operations were performed on that posted data, it succeeded or failed. |
| PostBackParams | (Private) PostBackParams is an instantiation of the library/Framework/Framework.Class.Parameters.php class which retrieves all form-posted values from the page’s get and post headers. This collection can then be used to render hidden form fields if the PostBackControl’s Render_PostBackForm method is called. It basically allows you to continually post values back to the form and not lose any data. |
| Method | Description |
|---|---|
| Control | Inherited from Control, this method is called from this control’s Constructor method. It allows your control to set up it’s Context object, PostBackAction, and set up Delegation. |
| AddItemToCollection | Inherited from Control. Rarely used in PostBackControls, but may come in handy. |
| InsertItemAt | Inherited from Control. Again, rarely used in PostBackControls, but may come in handy. |
| Render | Inherited and Overridden from Control. The PostBackControl’s Render method first examines the IsPostBack property of itself to see if any rendering needs to take place. If IsPostBack is true, it will then examine it’s PostBackValidated property to determine which rendering method it should actually call: Render_NoPostBack or Render_ValidPostBack. If PostBackValidated is false it will call the former, and if true it will call the latter. |
| Render_Warnings | Always used by PostBackControls to render Warnings gathered by the Context object’s WarningCollector class (defined in library/Framework/Framework.Class.MessageCollector.php) |
| Get_Warnings | This is called by Render_Warnings and it actually returns a string containing xhtml and the any messages present in the WarningCollector class. If, for some reason, you need to get your warnings into a string before you render them, this is what you would call. |
| Constructor | This method should always be called from within a custom PostBackControl’s constructor. This constructor will automagically call the Control method (see the first item in this method list) and it will also determine the value of IsPostBack based on the values you define for ValidActions in your custom PostBackControl. Finally, it will instantiate and fill your PostBackParams collection. |
| Render_NoPostBack | This method will almost always be overridden by your custom PostBackControl and is just here as a placeholder. |
| Render_ValidPostBack | This method will also almost always be overridden by your custom PostBackControl and is just here as a placeholder. |
| Render_PostBackForm | This method calls the Get_PostBackForm and renders (echoes) the returned value. |
| Get_PostBackForm | This method is used to automatically draw your form tag and hidden input parameters collected within the PostBackParams collection. |
| IsValidFormPostBack | Check that a form process a valid Actions with a valid CSRF protection key. It will raised an error if the PostBackAction is if the CSRF protection key in the form and session data are different or empty. |
Does any of that make sense to you yet? Maybe not, but let’s look at some...
A nice straightforward PostBackControl is the PasswordForm control which is located in library/Vanilla/Vanilla.PasswordForm.Control.php. This is the form that you are presented with when you click the “Change Password” button in the panel on the Account tab in Vanilla. First let’s look at how this control is instantiated and added to the account.php page:
if ($Configuration["ALLOW_PASSWORD_CHANGE"]) $PasswordForm = $Context->ObjectFactory->CreateControl($Context, "PasswordForm", $UserManager, $AccountUserID); ... if ($Configuration["ALLOW_PASSWORD_CHANGE"]) $Page->AddRenderControl($PasswordForm, $Configuration["CONTROL_POSITION_BODY_ITEM"]);
As you can see, the PasswordForm control isn’t added to the page unless the ALLOW_PASSWORD_CHANGE configuration variable is set to true. (Aside: This variable was created because I wanted a way to turn off password changing for guest accounts in Vanilla).
The important thing to note here is that (a) The PasswordForm is always created when the page is loaded - even if it is not going to be used or displayed on the page, and (b) while it is extended from the PostBackControl, it’s constructor is customized to include a few extra parameters when it is instantiated. In this case it also takes a $UserManager class and the $AccountUserID variable, which is the id of the user currently being viewed.
So now let’s take a look at the Control itself:
class PasswordForm extends PostBackControl { var $UserManager; var $User; function PasswordForm (&$Context, &$UserManager, $UserID) { $this->Name = 'PasswordForm'; if ($Context->Configuration['ALLOW_PASSWORD_CHANGE']) $this->ValidActions = array('ProcessPassword', 'Password'); $this->Constructor($Context); if ($this->IsPostBack) { $this->UserManager = &$UserManager; $this->User = $this->Context->ObjectFactory->NewContextObject($Context, 'User'); $this->User->GetPropertiesFromForm(); $this->User->UserID = $UserID; if ($this->PostBackAction == 'ProcessPassword' && $this->IsValidFormPostBack()) { if ($this->UserManager->ChangePassword($this->User)) header('location: '.GetUrl($this->Context->Configuration, $this->Context->SelfUrl)); } } $this->CallDelegate('Constructor'); } function Render() { if ($this->IsPostBack) { $this->CallDelegate('PreRender'); include(ThemeFilePath($this->Context->Configuration, 'account_password_form.php')); $this->CallDelegate('PostRender'); } } }
This control has a couple of custom properties: UserManager and User. Any control you make can, of course, have as many or as few custom properties as you require.
The constructor for this control’s first parameter is the Context object. You’ll notice that it is defined with an ampersand in front of the variable, like this &$Context. The ampersand indicates that the Context is being passed into the control by reference. This reduces the amount of memory consumed by the application because the Context object doesn’t need to be copied, and it also guarantees that any changes made to the context object in your control will be reflected in other controls that use the Context object after this control is finished with it. The next two parameters are the UserManager and AccountUserID as mentioned above. These other two parameters are custom to this control and, unlike the Context parameter, are not required as you create your own custom PostBackControls.
The first four actions that are taken in the constructor are absolutely imperative. When you extend the PostBackControl, your custom control must perform these same actions and it must do them in the exact same order. Let’s take a look at what they are and why they are important:
$this->Name = 'PasswordForm';
The Name property is absolutely essential for delegation. This value will be referenced when people attempt to add functions to delegates within your control. Typically this value will be exactly the same as the name of your control.
if ($Context->Configuration['ALLOW_PASSWORD_CHANGE']) $this->ValidActions = array('ProcessPassword', 'Password');
Next the ValidActions are defined. Without is array of strings, there is no way for the control to identify if it should take action of any kind.
$this->Constructor($Context);
Now the PostBackControl’s Constructor method is called. If you remember from above, this is where all of the properties from the PostBackControl and even Control are set up. Most importantly, this is where the PostBackAction is compared to your ValidActions array and the form determines if IsPostBack is true or not.
if ($this->IsPostBack) {
This line is absolutely the most important. Without this line, all PostBackControls would needlessly perform all kinds of actions that they don’t need to. Whenever you create a PostBackControl, you should always make sure that you need to create new objects and/or access the database before you do so, and this line is the way you do it.
So, at this point in the page processing, the PasswordForm has been created and it’s constructor has been called. The PostBackActions from the querystring have been examined, and if the PostBackAction matches either “ProcessPassword” or “Password”, the IsPostBack property is true and the constructor is now going to have to do some work:
$this->UserManager = &$UserManager; $this->User = $this->Context->ObjectFactory->NewContextObject($Context, 'User'); $this->User->GetPropertiesFromForm(); $this->User->UserID = $UserID; if ($this->PostBackAction == 'ProcessPassword' && $this->IsValidFormPostBack()) { if ($this->UserManager->ChangePassword($this->User)) header('location: '.GetUrl($this->Context->Configuration, $this->Context->SelfUrl)); }
In this case, I first create a new User object and call it’s GetPropertiesFromForm method. This method looks at form fields in the page’s post headers for user-related values and assigns them to properties of the user object. You can check out this class in library/People/People.Class.User.php. Next I assign the $AccountUserID value from the page to the User object I’ve just defined. This will allow the object to know that it is changing the password for the appropriate user.
Now we have a fully defined set of properties from the form postback (if there was one). The next line looks for a ProcessPassword PostBackAction, and if it finds one and if the request is valid (it could be a CSRF attack), then it attempts to change the password. The PasswordForm’s template (located in themes/account_password_form.php) assigns this “ProcessPassword” value in a hidden input using the PostBackControl’s PostBackParams collection, like so:
$this->PostBackParams->Set('PostBackAction', 'ProcessPassword'); ... $Required = $this->Context->GetDefinition('Required');
So, if the ProcessPassword value is found in the PostBackAction variable, then we know that the form was submitted and we should try to save it. Otherwise we just skip the ChangePassword method and continue through the constructor. If the CheckPassword method is called and it returns success (indicating that the password was changed successfully), the page is redirected back to the profile. If it doesn’t succeed in saving, the control will finish working through it’s constructor, and eventually the form will be re-displayed, except this time the inputs will be filled in by the properties contained within this PostBackControl’s User object.
This control, unlike many PostBackControls, explicitly overrides the PostBackControl’s Render method and doesn’t call the Render_NoPostBack or Render_ValidPostBack methods.
The only other thing to note about this very simple PostBackControl is that it’s Render method doesn’t do much except call some delegates and then include the account_password_form.php template. In any custom PostBackControl you write, you should not be adding templates to the themes directory. Instead you can keep your templates in your extension’s folder or just echo your render contents directly within your PostBackControl’s Render, Render_NoPostBack, or Render_ValidPostBack methods.
Let’s take a look at an example of a control that doesn’t override the PostBackControl’s Render method and instead takes advantage of the Render_NoPostBack and Render_ValidPostBack methods:
The UpdateCheck control can be located in library/Framework/Framework.Control.UpdateCheck.php and is an administrative control that checks lussumo.com for updates to the Vanilla codebase. You can see this control if you have administrative priviledges in Vanilla by going to the Settings Tab and clicking the “Check for Updates” link in the “Administrative Options” heading of the Panel.
The first thing you’ll notice when you click that link is the url. On my forum, it is:
http://lussumo.com/community/settings/?PostBackAction=UpdateCheck
Immediately you can see how the settings page is going to identify the UpdateCheck control by the PostBackAction value passed in the querystring. Furthermore, all other controls will know not to display themselves because they do not render when “UpdateCheck” is the PostBackAction.
Let’s look at this control’s constructor:
function UpdateCheck(&$Context) { $this->Name = 'UpdateCheck'; $this->ValidActions = array('UpdateCheck', 'ProcessUpdateCheck'); $this->Constructor($Context); if (!$this->Context->Session->User->Permission('PERMISSION_CHECK_FOR_UPDATES')) { $this->IsPostBack = 0; } elseif ($this->IsPostBack && $this->PostBackAction == 'ProcessUpdateCheck') { // Ping lussumo.com for update information $Lines = file($this->Context->Configuration['UPDATE_URL'] .'?Application='.APPLICATION .'&Version='.APPLICATION_VERSION .'&Language='.$this->Context->Configuration['LANGUAGE']); $this->LussumoMessage = implode('\r\n', $Lines); $this->PostBackValidated = 1; } $this->CallDelegate('Constructor'); }
As per usual, you see that the Name property is defined (again, the same as the name of the control itself), the valid actions are defined (UpdateCheck and ProcessUpdateCheck) and you can see how the PostBackAction you saw in the url will match the first ValidAction item. Next the PostBackControl’s Constructor is called.
Next a permission is checked on the user viewing the page. If the user doesn’t have access to this control’s abilities, the IsPostBack is marked back to false.
Now if the IsPostBack is still true and the PostBackAction is “ProcessUpdateCheck” (indicating that the form has already been displayed and a submit button was clicked), then the control pings out to lussumo.com with a request for information about the latest version of Vanilla. This information is retrieved and the PostBackValidated property is set to true. By setting this property to true, the control will now know to call the Render_ValidPostBack method instead of the Render_NoPostBack method. Each of these different methods call different template files: themes/settings_update_check_nopostback.php and themes/settings_update_check_validpostback.php respectively. The “nopostback” template is rendered before the form is submitted, and the “validpostback” template is rendered when the PostBackValidated has been set to true. This way you can have a “before” and “after” result of submitting a form.
That is really it. If you can grasp how PostBackControls work, then you’re well on your way to writing a completely custom application using the Lussumo Framework or just creating custom controls to do some very funky things within Vanilla.
Click here to read the next section on developing new extensions: Delegation