| 1 |
lars |
1 |
<com:TContent ID="body">
|
|
|
2 |
<h1 id="18008">Building an AJAX Chat Application</h1>
|
|
|
3 |
<com:RequiresVersion Version="3.1a" />
|
|
|
4 |
<p id="90081" class="block-content">This tutorial introduces the Prado web application framework's
|
|
|
5 |
<a href="?page=Database.ActiveRecord">ActiveRecord</a>
|
|
|
6 |
and <a href="?page=ActiveControls.Home">Active Controls</a> to build a Chat
|
|
|
7 |
web application. It is assumed that you
|
|
|
8 |
are familiar with PHP and you have access to a web server that is able to serve PHP5 scripts.
|
|
|
9 |
This basic chat application will utilize the following ideas/components in Prado.
|
|
|
10 |
</p>
|
|
|
11 |
<ul id="u1" class="block-content">
|
|
|
12 |
<li>Building a custom User Manager class.</li>
|
|
|
13 |
<li>Authenticating and adding a new user to the database.</li>
|
|
|
14 |
<li>Using ActiveRecord to interact with the database.</li>
|
|
|
15 |
<li>Using Active Controls and callbacks to implement the user interface.</li>
|
|
|
16 |
<li>Separating application logic and application flow.</li>
|
|
|
17 |
</ul>
|
|
|
18 |
|
|
|
19 |
<p id="90082" class="block-content">In this tutorial you will build an AJAX Chat web application that allows
|
|
|
20 |
multiple users to communicate through their web browser.
|
|
|
21 |
The application consists of two pages: a login page
|
|
|
22 |
that asks the user to enter their nickname and the main application chat
|
|
|
23 |
page.
|
|
|
24 |
You can try the application <a href="../chat/index.php">locally</a> or at
|
|
|
25 |
<a href="http://www.pradosoft.com/demos/chat/">Pradosoft.com</a>.
|
|
|
26 |
The main application chat page is shown bellow.
|
|
|
27 |
<img src=<%~ chat1.png %> class="figure" />
|
|
|
28 |
</p>
|
|
|
29 |
|
|
|
30 |
<h1 id="18009">Download, Install and Create a New Application</h1>
|
|
|
31 |
<p id="90083" class="block-content">The download and installation steps are similar to those in
|
|
|
32 |
the <a href="?page=Tutorial.CurrencyConverter#download">Currency converter tutorial</a>.
|
|
|
33 |
To create the application, we run from the command line the following.
|
|
|
34 |
See the <a href="?page=GettingStarted.CommandLine">Command Line Tool</a>
|
|
|
35 |
for more details.
|
|
|
36 |
<com:TTextHighlighter Language="text" CssClass="source block-content" id="code_90027">
|
|
|
37 |
php prado/framework/prado-cli.php -c chat
|
|
|
38 |
</com:TTextHighlighter>
|
|
|
39 |
</p>
|
|
|
40 |
|
|
|
41 |
<p id="90084" class="block-content">The above command creates the necessary directory structure and minimal
|
|
|
42 |
files (including "index.php" and "Home.page") to run a Prado web application.
|
|
|
43 |
Now you can point your browser's URL to the web server to serve up
|
|
|
44 |
the <tt>index.php</tt> script in the <tt>chat</tt> directory.
|
|
|
45 |
You should see the message "Welcome to Prado!"
|
|
|
46 |
</p>
|
|
|
47 |
|
|
|
48 |
<h1 id="18010">Authentication and Authorization</h1>
|
|
|
49 |
<p id="90085" class="block-content">The first task for this application is to ensure that each user
|
|
|
50 |
of the chat application is assigned with a unique (chosen by the user)
|
|
|
51 |
username. To achieve this, we can secure the main chat application
|
|
|
52 |
page to deny access to anonymous users. First, let us create the <tt>Login</tt>
|
|
|
53 |
page with the following code. We save the <tt>Login.php</tt> and <tt>Login.page</tt>
|
|
|
54 |
in the <tt>chat/protected/pages/</tt> directory (there should be a <tt>Home.page</tt>
|
|
|
55 |
file created by the command line tool).
|
|
|
56 |
</p>
|
|
|
57 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90028">
|
|
|
58 |
<?php
|
|
|
59 |
class Login extends TPage
|
|
|
60 |
{
|
|
|
61 |
}
|
|
|
62 |
?>
|
|
|
63 |
</com:TTextHighlighter>
|
|
|
64 |
<com:TTextHighlighter Language="prado" CssClass="source block-content" id="code_90029">
|
|
|
65 |
<!doctype html public "-//W3C//DTD XHTML 1.0 Strict//EN"
|
|
|
66 |
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
|
|
67 |
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
|
|
68 |
<head>
|
|
|
69 |
<title>Prado Chat Demo Login</title>
|
|
|
70 |
</head>
|
|
|
71 |
<body>
|
|
|
72 |
<com:TForm>
|
|
|
73 |
<h1 class="login">Prado Chat Demo Login</h1>
|
|
|
74 |
<fieldset class="login">
|
|
|
75 |
<legend>Please enter your name:</legend>
|
|
|
76 |
<div class="username">
|
|
|
77 |
<com:TLabel ForControl="username" Text="Username:" />
|
|
|
78 |
<com:TTextBox ID="username" MaxLength="20" />
|
|
|
79 |
<com:TRequiredFieldValidator
|
|
|
80 |
ControlToValidate="username"
|
|
|
81 |
Display="Dynamic"
|
|
|
82 |
ErrorMessage="Please provide a username." />
|
|
|
83 |
</div>
|
|
|
84 |
<div class="login-button">
|
|
|
85 |
<com:TButton Text="Login" />
|
|
|
86 |
</div>
|
|
|
87 |
</com:TForm>
|
|
|
88 |
</body>
|
|
|
89 |
</html>
|
|
|
90 |
</com:TTextHighlighter>
|
|
|
91 |
<p id="90086" class="block-content">The login page contains
|
|
|
92 |
a <com:DocLink ClassPath="System.Web.UI.TForm" Text="TForm" />,
|
|
|
93 |
a <com:DocLink ClassPath="System.Web.UI.WebControls.TTextBox" Text="TTextBox" />,
|
|
|
94 |
a <com:DocLink ClassPath="System.Web.UI.WebControls.TRequiredFieldValidator" Text="TRequiredFieldValidator" />
|
|
|
95 |
and a <com:DocLink ClassPath="System.Web.UI.WebControls.TButton" Text="TButton" />. The resulting
|
|
|
96 |
page looks like the following (after applying some a style sheet).
|
|
|
97 |
<img src=<%~ chat2.png %> class="figure" />
|
|
|
98 |
If you click on the <tt>Login</tt> button without entering any
|
|
|
99 |
text in the username textbox, an error message is displayed. This is
|
|
|
100 |
due to the <com:DocLink ClassPath="System.Web.UI.WebControls.TRequiredFieldValidator" Text="TRequiredFieldValidator" />
|
|
|
101 |
requiring the user to enter some text in the textbox before proceeding.
|
|
|
102 |
</p>
|
|
|
103 |
<h2 id="18019">Securing the <tt>Home</tt> page</h2>
|
|
|
104 |
<p id="90087" class="block-content">Now we wish that if the user is trying to access the main application
|
|
|
105 |
page, <tt>Home.page</tt>, before they have logged in, the user is presented with
|
|
|
106 |
the <tt>Login.page</tt> first. We add a <tt>chat/protected/application.xml</tt> configuration
|
|
|
107 |
file to import some classes that we shall use later.
|
|
|
108 |
<com:TTextHighlighter Language="xml" CssClass="source block-content" id="code_90030">
|
|
|
109 |
<?xml version="1.0" encoding="utf-8"?>
|
|
|
110 |
<application id="Chat" Mode="Debug">
|
|
|
111 |
<paths>
|
|
|
112 |
<using namespace="System.Data.*" />
|
|
|
113 |
<using namespace="System.Data.ActiveRecord.*" />
|
|
|
114 |
<using namespace="System.Security.*" />
|
|
|
115 |
<using namespace="System.Web.UI.ActiveControls.*" />
|
|
|
116 |
</paths>
|
|
|
117 |
</application>
|
|
|
118 |
</com:TTextHighlighter>
|
|
|
119 |
Next, we add a <tt>chat/protected/pages/config.xml</tt> configuration file to
|
|
|
120 |
secure the <tt>pages</tt> directory.
|
|
|
121 |
<com:TTextHighlighter Language="xml" CssClass="source block-content" id="code_90031">
|
|
|
122 |
<?xml version="1.0" encoding="utf-8"?>
|
|
|
123 |
<configuration>
|
|
|
124 |
<modules>
|
|
|
125 |
<module id="users" class="TUserManager" />
|
|
|
126 |
<module id="auth" class="TAuthManager" UserManager="users" LoginPage="Login" />
|
|
|
127 |
</modules>
|
|
|
128 |
<authorization>
|
|
|
129 |
<allow pages="Login" users="?" />
|
|
|
130 |
<allow roles="normal" />
|
|
|
131 |
<deny users="*" />
|
|
|
132 |
</authorization>
|
|
|
133 |
</configuration>
|
|
|
134 |
</com:TTextHighlighter>
|
|
|
135 |
We setup the authentication using the default classes as explained in the
|
|
|
136 |
<a href="?page=Advanced.Auth">authentication/authorization quickstart</a>.
|
|
|
137 |
In the authorization definition, we allow anonymous users to access the
|
|
|
138 |
<tt>Login</tt> page (anonymous users is specified by the <tt>?</tt> question mark).
|
|
|
139 |
We allow any users with role equal to "normal" (to be defined later)
|
|
|
140 |
to access all the pages, that is, the <tt>Login</tt> and <tt>Home</tt> pages.
|
|
|
141 |
Lastly, we deny all users without any roles to access any page. The authorization
|
|
|
142 |
rules are executed on first match basis.
|
|
|
143 |
</p>
|
|
|
144 |
|
|
|
145 |
<p id="90088" class="block-content">If you now try to access the <tt>Home</tt> page by pointing your browser
|
|
|
146 |
to the <tt>index.php</tt> you will be redirected to the <tt>Login</tt> page.
|
|
|
147 |
</p>
|
|
|
148 |
|
|
|
149 |
<h1 id="18011">Active Record for <tt>chat_users</tt> table</h1>
|
|
|
150 |
<p id="90089" class="block-content">The <com:DocLink ClassPath="System.Secutity.TUserManager" Text="TUserManager" />
|
|
|
151 |
class only provides a read-only list of users. We need to be able to add or
|
|
|
152 |
login new users dynamically. So we need to create our own user manager class.
|
|
|
153 |
First, we shall setup a database with a <tt>chat_users</tt> table and create an ActiveRecord
|
|
|
154 |
that can work with the <tt>chat_users</tt> table with ease. For the demo, we
|
|
|
155 |
use <tt>sqlite</tt> as our database for ease of distributing the demo. The demo
|
|
|
156 |
can be extended to use other databases such as MySQL or Postgres SQL easily.
|
|
|
157 |
We define the <tt>chat_users</tt> table as follows.
|
|
|
158 |
<com:TTextHighlighter Language="text" CssClass="source block-content" id="code_90032">
|
|
|
159 |
CREATE TABLE chat_users
|
|
|
160 |
(
|
|
|
161 |
username VARCHAR(20) NOT NULL PRIMARY KEY,
|
|
|
162 |
last_activity INTEGER NOT NULL DEFAULT "0"
|
|
|
163 |
);
|
|
|
164 |
</com:TTextHighlighter>
|
|
|
165 |
Next we define the corresponding <tt>ChatUserRecord</tt> class and save it as
|
|
|
166 |
<tt>chat/protected/App_Code/ChatUserRecord.php</tt> (you need to create the
|
|
|
167 |
<tt>App_Code</tt> directory as well). We also save the sqlite database file
|
|
|
168 |
as <tt>App_Code/chat.db</tt>.
|
|
|
169 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90033">
|
|
|
170 |
class ChatUserRecord extends TActiveRecord
|
|
|
171 |
{
|
|
|
172 |
const TABLE='chat_users';
|
|
|
173 |
|
|
|
174 |
public $username;
|
|
|
175 |
public $last_activity;
|
|
|
176 |
|
|
|
177 |
public static function finder($className=__CLASS__)
|
|
|
178 |
{
|
|
|
179 |
return parent::finder($className);
|
|
|
180 |
}
|
|
|
181 |
}
|
|
|
182 |
</com:TTextHighlighter>
|
|
|
183 |
Before using the <tt>ChatUserRecord</tt> class we to configure a default
|
|
|
184 |
database connection for ActiveRecord to function. In the <tt>chat/protected/application.xml</tt>
|
|
|
185 |
we import classes from the <tt>App_Code</tt> directory and add an
|
|
|
186 |
<a href="?page=Database.ActiveRecord">ActiveRecord configuration module</a>.
|
|
|
187 |
<com:TTextHighlighter Language="xml" CssClass="source block-content" id="code_90034">
|
|
|
188 |
<?xml version="1.0" encoding="utf-8"?>
|
|
|
189 |
<application id="Chat" Mode="Debug">
|
|
|
190 |
<paths>
|
|
|
191 |
<using namespace="Application.App_Code.*" />
|
|
|
192 |
<using namespace="System.Data.*" />
|
|
|
193 |
<using namespace="System.Data.ActiveRecord.*" />
|
|
|
194 |
<using namespace="System.Security.*" />
|
|
|
195 |
<using namespace="System.Web.UI.ActiveControls.*" />
|
|
|
196 |
</paths>
|
|
|
197 |
<modules>
|
|
|
198 |
<module class="TActiveRecordConfig" EnableCache="true"
|
|
|
199 |
Database.ConnectionString="sqlite:protected/App_Code/chat.db" />
|
|
|
200 |
</modules>
|
|
|
201 |
</application>
|
|
|
202 |
</com:TTextHighlighter>
|
|
|
203 |
</p>
|
|
|
204 |
|
|
|
205 |
<h2 id="18020">Custom User Manager class</h2>
|
|
|
206 |
<p id="90090" class="block-content">To implement a custom user manager module class we just need
|
|
|
207 |
to extends the <tt>TModule</tt> class and implement the <tt>IUserManager</tt>
|
|
|
208 |
interface. The <tt>getGuestName()</tt>, <tt>getUser()</tt> and <tt>validateUser()</tt>
|
|
|
209 |
methods are required by the <tt>IUserManager</tt> interface.
|
|
|
210 |
We save the custom user manager class as <tt>App_Code/ChatUserManager.php</tt>.
|
|
|
211 |
</p>
|
|
|
212 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90035">
|
|
|
213 |
class ChatUserManager extends TModule implements IUserManager
|
|
|
214 |
{
|
|
|
215 |
public function getGuestName()
|
|
|
216 |
{
|
|
|
217 |
return 'Guest';
|
|
|
218 |
}
|
|
|
219 |
|
|
|
220 |
public function getUser($username=null)
|
|
|
221 |
{
|
|
|
222 |
$user=new TUser($this);
|
|
|
223 |
$user->setIsGuest(true);
|
|
|
224 |
if($username !== null && $this->usernameExists($username))
|
|
|
225 |
{
|
|
|
226 |
$user->setIsGuest(false);
|
|
|
227 |
$user->setName($username);
|
|
|
228 |
$user->setRoles(array('normal'));
|
|
|
229 |
}
|
|
|
230 |
return $user;
|
|
|
231 |
}
|
|
|
232 |
|
|
|
233 |
public function addNewUser($username)
|
|
|
234 |
{
|
|
|
235 |
$user = new ChatUserRecord();
|
|
|
236 |
$user->username = $username;
|
|
|
237 |
$user->save();
|
|
|
238 |
}
|
|
|
239 |
|
|
|
240 |
public function usernameExists($username)
|
|
|
241 |
{
|
|
|
242 |
$finder = ChatUserRecord::finder();
|
|
|
243 |
$record = $finder->findByUsername($username);
|
|
|
244 |
return $record instanceof ChatUserRecord;
|
|
|
245 |
}
|
|
|
246 |
|
|
|
247 |
public function validateUser($username,$password)
|
|
|
248 |
{
|
|
|
249 |
return $this->usernameExists($username);
|
|
|
250 |
}
|
|
|
251 |
}
|
|
|
252 |
</com:TTextHighlighter>
|
|
|
253 |
<p id="90091" class="block-content">
|
|
|
254 |
The <tt>getGuestName()</tt>
|
|
|
255 |
method simply returns the name for a guest user and is not used in our application.
|
|
|
256 |
The <tt>getUser()</tt> method returns a <tt>TUser</tt> object if the username
|
|
|
257 |
exists in the database, the <tt>TUser</tt> object is set with role of "normal"
|
|
|
258 |
that corresponds to the <tt><authorization></tt> rules defined in our
|
|
|
259 |
<tt>config.xml</tt> file. </p>
|
|
|
260 |
|
|
|
261 |
<p id="90092" class="block-content">The <tt>addNewUser()</tt> and <tt>usernameExists()</tt>
|
|
|
262 |
method uses the ActiveRecord corresponding to the <tt>chat_users</tt> table to
|
|
|
263 |
add a new user and to check if a username already exists, respectively.
|
|
|
264 |
</p>
|
|
|
265 |
|
|
|
266 |
<p id="90093" class="block-content">The next thing to do is change the <tt>config.xml</tt> configuration to use
|
|
|
267 |
our new custom user manager class. We simply change the <tt><module></tt>
|
|
|
268 |
configuration with <tt>id="users"</tt>.</p>
|
|
|
269 |
<com:TTextHighlighter Language="xml" CssClass="source block-content" id="code_90036">
|
|
|
270 |
<module id="users" class="ChatUserManager" />
|
|
|
271 |
</com:TTextHighlighter>
|
|
|
272 |
|
|
|
273 |
<h1 id="18012">Authentication</h1>
|
|
|
274 |
<p id="90094" class="block-content">To perform authentication, we just want the user to enter a unique
|
|
|
275 |
username. We add a
|
|
|
276 |
<com:DocLink ClassPath="System.Web.UI.WebControls.TCustomValidator" Text="TCustomValidator" />
|
|
|
277 |
for validate the uniqueness of the username and add an <tt>OnClick</tt> event handler
|
|
|
278 |
for the login button.</p>
|
|
|
279 |
<com:TTextHighlighter Language="prado" CssClass="source block-content" id="code_90037">
|
|
|
280 |
<com:TCustomValidator
|
|
|
281 |
ControlToValidate="username"
|
|
|
282 |
Display="Dynamic"
|
|
|
283 |
OnServerValidate="checkUsername"
|
|
|
284 |
ErrorMessage="The username is already taken." />
|
|
|
285 |
|
|
|
286 |
...
|
|
|
287 |
|
|
|
288 |
<com:TButton Text="Login" OnClick="createNewUser" />
|
|
|
289 |
</com:TTextHighlighter>
|
|
|
290 |
In the <tt>Login.php</tt> file, we add the following 2 methods.
|
|
|
291 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90038">
|
|
|
292 |
function checkUsername($sender, $param)
|
|
|
293 |
{
|
|
|
294 |
$manager = $this->Application->Modules['users'];
|
|
|
295 |
if($manager->usernameExists($this->username->Text))
|
|
|
296 |
$param->IsValid = false;
|
|
|
297 |
}
|
|
|
298 |
|
|
|
299 |
function createNewUser($sender, $param)
|
|
|
300 |
{
|
|
|
301 |
if($this->Page->IsValid)
|
|
|
302 |
{
|
|
|
303 |
$manager = $this->Application->Modules['users'];
|
|
|
304 |
$manager->addNewUser($this->username->Text);
|
|
|
305 |
|
|
|
306 |
//do manual login
|
|
|
307 |
$user = $manager->getUser($this->username->Text);
|
|
|
308 |
$auth = $this->Application->Modules['auth'];
|
|
|
309 |
$auth->updateSessionUser($user);
|
|
|
310 |
$this->Application->User = $user;
|
|
|
311 |
|
|
|
312 |
$url = $this->Service->constructUrl($this->Service->DefaultPage);
|
|
|
313 |
$this->Response->redirect($url);
|
|
|
314 |
}
|
|
|
315 |
}
|
|
|
316 |
</com:TTextHighlighter>
|
|
|
317 |
The <tt>checkUserName()</tt> method uses the <tt>ChatUserManager</tt> class
|
|
|
318 |
(recall that in the <tt>config.xml</tt> configuration we set the
|
|
|
319 |
ID of the custom user manager class as "users") to validate the username
|
|
|
320 |
is not taken.
|
|
|
321 |
</p>
|
|
|
322 |
<p id="90095" class="block-content">
|
|
|
323 |
In the <tt>createNewUser</tt> method, when the validation passes (that is,
|
|
|
324 |
when the user name is not taken) we add a new user. Afterward we perform
|
|
|
325 |
a manual login process:</p>
|
|
|
326 |
<ul id="u2" class="block-content">
|
|
|
327 |
<li>First we obtain a <tt>TUser</tt> instance from
|
|
|
328 |
our custom user manager class using the <tt>$manager->getUser(...)</tt> method.</li>
|
|
|
329 |
<li>Using the <tt>TAuthManager</tt> we set/update the user object in the
|
|
|
330 |
current session data.</li>
|
|
|
331 |
<li>Then we set/update the <tt>Application</tt>'s user instance with our
|
|
|
332 |
new user object.</li>
|
|
|
333 |
</ul>
|
|
|
334 |
</p>
|
|
|
335 |
<p id="finally" class="block-content">
|
|
|
336 |
Finally, we redirect the client to the default <tt>Home</tt> page.
|
|
|
337 |
</p>
|
|
|
338 |
|
|
|
339 |
<h2 id="18021">Default Values for ActiveRecord</h2>
|
|
|
340 |
<p id="90096" class="block-content">If you try to perform a login now, you will receive an error message like
|
|
|
341 |
"<i>Property '<tt>ChatUserRecord::$last_activity</tt>' must not be null as defined
|
|
|
342 |
by column '<tt>last_activity</tt>' in table '<tt>chat_users</tt>'.</i>". This means that the <tt>$last_activity</tt>
|
|
|
343 |
property value was null when we tried to insert a new record. We need to either
|
|
|
344 |
define a default value in the corresponding column in the table and allow null values or set the default
|
|
|
345 |
value in the <tt>ChatUserRecord</tt> class. We shall demonstrate the later by
|
|
|
346 |
altering the <tt>ChatUserRecord</tt> with the addition of a set getter/setter
|
|
|
347 |
methods for the <tt>last_activity</tt> property.
|
|
|
348 |
|
|
|
349 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90039">
|
|
|
350 |
private $_last_activity;
|
|
|
351 |
|
|
|
352 |
public function getLast_Activity()
|
|
|
353 |
{
|
|
|
354 |
if($this->_last_activity === null)
|
|
|
355 |
$this->_last_activity = time();
|
|
|
356 |
return $this->_last_activity;
|
|
|
357 |
}
|
|
|
358 |
|
|
|
359 |
public function setLast_Activity($value)
|
|
|
360 |
{
|
|
|
361 |
$this->_last_activity = $value;
|
|
|
362 |
}
|
|
|
363 |
</com:TTextHighlighter>
|
|
|
364 |
Notice that we renamed <tt>$last_activity</tt> to <tt>$_last_activity</tt> (note
|
|
|
365 |
the underscore after the dollar sign).
|
|
|
366 |
</p>
|
|
|
367 |
|
|
|
368 |
<h1 id="18013">Main Chat Application</h1>
|
|
|
369 |
<p id="90097" class="block-content">Now we are ready to build the main chat application. We use a simple
|
|
|
370 |
layout that consist of one panel holding the chat messages, one panel
|
|
|
371 |
to hold the users list, a textarea for the user to enter the text message
|
|
|
372 |
and a button to send the message.
|
|
|
373 |
<com:TTextHighlighter Language="prado" CssClass="source block-content" id="code_90040">
|
|
|
374 |
<!doctype html public "-//W3C//DTD XHTML 1.0 Strict//EN"
|
|
|
375 |
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
|
|
376 |
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
|
|
377 |
<head>
|
|
|
378 |
<title>Prado Chat Demo</title>
|
|
|
379 |
<style>
|
|
|
380 |
.messages
|
|
|
381 |
{
|
|
|
382 |
width: 500px;
|
|
|
383 |
height: 300px;
|
|
|
384 |
float: left;
|
|
|
385 |
border: 1px solid ButtonFace;
|
|
|
386 |
overflow: auto;
|
|
|
387 |
}
|
|
|
388 |
.user-list
|
|
|
389 |
{
|
|
|
390 |
margin-left: 2px;
|
|
|
391 |
float: left;
|
|
|
392 |
width: 180px;
|
|
|
393 |
height: 300px;
|
|
|
394 |
border: 1px solid ButtonFace;
|
|
|
395 |
overflow: auto;
|
|
|
396 |
font-size: 0.85em;
|
|
|
397 |
}
|
|
|
398 |
.message-input
|
|
|
399 |
{
|
|
|
400 |
float: left;
|
|
|
401 |
}
|
|
|
402 |
|
|
|
403 |
.message-input textarea
|
|
|
404 |
{
|
|
|
405 |
margin-top: 3px;
|
|
|
406 |
padding: 0.4em 0.2em;
|
|
|
407 |
width: 493px;
|
|
|
408 |
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
|
|
|
409 |
font-size: 0.85em;
|
|
|
410 |
height: 40px;
|
|
|
411 |
}
|
|
|
412 |
.send-button
|
|
|
413 |
{
|
|
|
414 |
margin: 0.5em;
|
|
|
415 |
}
|
|
|
416 |
</style>
|
|
|
417 |
</head>
|
|
|
418 |
<body>
|
|
|
419 |
<com:TForm>
|
|
|
420 |
<h1 id="18014">Prado Chat Demo</h1>
|
|
|
421 |
<div id="messages" class="messages">
|
|
|
422 |
<com:TPlaceHolder ID="messageList" />
|
|
|
423 |
</div>
|
|
|
424 |
<div id="users" class="user-list">
|
|
|
425 |
<com:TPlaceHolder ID="userList" />
|
|
|
426 |
</div>
|
|
|
427 |
<div class="message-input">
|
|
|
428 |
<com:TActiveTextBox ID="userinput"
|
|
|
429 |
Columns="40" Rows="2" TextMode="MultiLine" />
|
|
|
430 |
<com:TActiveButton ID="sendButton" CssClass="send-button"
|
|
|
431 |
Text="Send" />
|
|
|
432 |
</div>
|
|
|
433 |
</com:TForm>
|
|
|
434 |
<com:TJavascriptLogger />
|
|
|
435 |
</body>
|
|
|
436 |
</html>
|
|
|
437 |
</com:TTextHighlighter>
|
|
|
438 |
We added two Active Control components: a
|
|
|
439 |
<com:DocLink ClassPath="System.Web.UI.ActiveControls.TActiveTextBox" Text="TActiveTextBox" />
|
|
|
440 |
and a
|
|
|
441 |
<com:DocLink ClassPath="System.Web.UI.ActiveControls.TActiveButton" Text="TActiveButton" />.
|
|
|
442 |
We also added a
|
|
|
443 |
<com:DocLink ClassPath="System.Web.UI.WebControls.TJavascriptLogger" Text="TJavascriptLogger" />
|
|
|
444 |
that will be very useful for understanding how the Active Controls work.
|
|
|
445 |
</p>
|
|
|
446 |
|
|
|
447 |
<h2 id="18022">Exploring the Active Controls</h2>
|
|
|
448 |
<p id="90098" class="block-content">We should have some fun before we proceeding with setting up the chat buffering. We want
|
|
|
449 |
to see how we can update the current page when we receive a message. First, we add
|
|
|
450 |
an <tt>OnClick</tt> event handler for the <tt>Send</tt> button.
|
|
|
451 |
|
|
|
452 |
<com:TTextHighlighter Language="prado" CssClass="source block-content" id="code_90041">
|
|
|
453 |
<com:TActiveButton ID="sendButton" CssClass="send-button"
|
|
|
454 |
Text="Send" OnClick="processMessage"/>
|
|
|
455 |
</com:TTextHighlighter>
|
|
|
456 |
And the corresponding event handler method in the <tt>Home.php</tt> class (we
|
|
|
457 |
need to create this new file too).
|
|
|
458 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90042">
|
|
|
459 |
class Home extends TPage
|
|
|
460 |
{
|
|
|
461 |
function processMessage($sender, $param)
|
|
|
462 |
{
|
|
|
463 |
echo $this->userinput->Text;
|
|
|
464 |
}
|
|
|
465 |
}
|
|
|
466 |
</com:TTextHighlighter>
|
|
|
467 |
If you now type something in the main application textbox and click the send button
|
|
|
468 |
you should see whatever you have typed echoed in the <tt>TJavascriptLogger</tt> console.
|
|
|
469 |
</p>
|
|
|
470 |
|
|
|
471 |
<p id="90099" class="block-content">To append or add some content to the message list panel, we need to use
|
|
|
472 |
some methods in the
|
|
|
473 |
<com:DocLink ClassPath="System.Web.UI.ActiveControls.TCallbackClientScript" Text="TCallbackClientScript" />
|
|
|
474 |
class which is available through the <tt>CallbackClient</tt> property of the
|
|
|
475 |
current <tt>TPage</tt> object. For example, we do can do
|
|
|
476 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90043">
|
|
|
477 |
function processMessage($sender, $param)
|
|
|
478 |
{
|
|
|
479 |
$this->CallbackClient->appendContent("messages", $this->userinput->Text);
|
|
|
480 |
}
|
|
|
481 |
</com:TTextHighlighter>
|
|
|
482 |
This is one way to update some part of the existing page during a callback (AJAX style events)
|
|
|
483 |
and will be the primary way we will use to implement the chat application.
|
|
|
484 |
</p>
|
|
|
485 |
|
|
|
486 |
<h1 id="18015">Active Record for <tt>chat_buffer</tt> table</h1>
|
|
|
487 |
<p id="90100" class="block-content">To send a message to all the connected users we need to buffer or store
|
|
|
488 |
the message for each user. We can use the database to buffer the messages. The
|
|
|
489 |
<tt>chat_buffer</tt> table is defined as follows.
|
|
|
490 |
<com:TTextHighlighter Language="text" CssClass="source block-content" id="code_90044">
|
|
|
491 |
CREATE TABLE chat_buffer
|
|
|
492 |
(
|
|
|
493 |
id INTEGER PRIMARY KEY,
|
|
|
494 |
for_user VARCHAR(20) NOT NULL,
|
|
|
495 |
from_user VARCHAR(20) NOT NULL,
|
|
|
496 |
message TEXT NOT NULL,
|
|
|
497 |
created_on INTEGER NOT NULL DEFAULT "0"
|
|
|
498 |
);
|
|
|
499 |
</com:TTextHighlighter>
|
|
|
500 |
The corresponding <tt>ChatBufferRecord</tt> class is saved as
|
|
|
501 |
<tt>App_Code/ChatBufferRecord.php</tt>.
|
|
|
502 |
|
|
|
503 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90045">
|
|
|
504 |
class ChatBufferRecord extends TActiveRecord
|
|
|
505 |
{
|
|
|
506 |
const TABLE='chat_buffer';
|
|
|
507 |
|
|
|
508 |
public $id;
|
|
|
509 |
public $for_user;
|
|
|
510 |
public $from_user;
|
|
|
511 |
public $message;
|
|
|
512 |
private $_created_on;
|
|
|
513 |
|
|
|
514 |
public function getCreated_On()
|
|
|
515 |
{
|
|
|
516 |
if($this->_created_on === null)
|
|
|
517 |
$this->_created_on = time();
|
|
|
518 |
return $this->_created_on;
|
|
|
519 |
}
|
|
|
520 |
|
|
|
521 |
public function setCreated_On($value)
|
|
|
522 |
{
|
|
|
523 |
$this->_created_on = $value;
|
|
|
524 |
}
|
|
|
525 |
|
|
|
526 |
public static function finder($className=__CLASS__)
|
|
|
527 |
{
|
|
|
528 |
return parent::finder($className);
|
|
|
529 |
}
|
|
|
530 |
}
|
|
|
531 |
</com:TTextHighlighter>
|
|
|
532 |
</p>
|
|
|
533 |
|
|
|
534 |
<h1 id="18016">Chat Application Logic</h1>
|
|
|
535 |
<p id="90101" class="block-content">We finally arrive at the guts of the chat application logic. First, we
|
|
|
536 |
need to save a received message into the chat buffer for <b>all</b> the
|
|
|
537 |
current users. We add this logic in the <tt>ChatBufferRecord</tt> class.
|
|
|
538 |
|
|
|
539 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90046">
|
|
|
540 |
public function saveMessage()
|
|
|
541 |
{
|
|
|
542 |
foreach(ChatUserRecord::finder()->findAll() as $user)
|
|
|
543 |
{
|
|
|
544 |
$message = new self;
|
|
|
545 |
$message->for_user = $user->username;
|
|
|
546 |
$message->from_user = $this->from_user;
|
|
|
547 |
$message->message = $this->message;
|
|
|
548 |
$message->save();
|
|
|
549 |
if($user->username == $this->from_user)
|
|
|
550 |
{
|
|
|
551 |
$user->last_activity = time(); //update the last activity;
|
|
|
552 |
$user->save();
|
|
|
553 |
}
|
|
|
554 |
}
|
|
|
555 |
}
|
|
|
556 |
</com:TTextHighlighter>
|
|
|
557 |
We first find all the current users using the <tt>ChatUserRecord</tt> finder
|
|
|
558 |
methods. Then we duplicate the message and save it into the database. In addition,
|
|
|
559 |
we update the message sender's last activity timestamp. The above piece of code
|
|
|
560 |
demonstrates the simplicity and succinctness of using ActiveRecords for simple database designs.
|
|
|
561 |
</p>
|
|
|
562 |
|
|
|
563 |
<p id="90102" class="block-content">The next piece of the logic is to retrieve the users' messages from the buffer.
|
|
|
564 |
We simply load all the messages for a particular username and format that message
|
|
|
565 |
appropriately (remember to escape the output to prevent Cross-Site Scripting attacks).
|
|
|
566 |
After we load the messages, we delete those loaded messages and any older
|
|
|
567 |
messages that may have been left in the buffer.
|
|
|
568 |
</p>
|
|
|
569 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90047">
|
|
|
570 |
public function getUserMessages($user)
|
|
|
571 |
{
|
|
|
572 |
$content = '';
|
|
|
573 |
foreach($this->findAll('for_user = ?', $user) as $message)
|
|
|
574 |
$content .= $this->formatMessage($message);
|
|
|
575 |
$this->deleteAll('for_user = ? OR created_on < ?',
|
|
|
576 |
$user, time() - 300); //5 min inactivity
|
|
|
577 |
return $content;
|
|
|
578 |
}
|
|
|
579 |
|
|
|
580 |
protected function formatMessage($message)
|
|
|
581 |
{
|
|
|
582 |
$user = htmlspecialchars($message->from_user);
|
|
|
583 |
$content = htmlspecialchars($message->message);
|
|
|
584 |
return "<div class=\"message\"><strong>{$user}:</strong>"
|
|
|
585 |
." <span>{$content}</span></div>";
|
|
|
586 |
}
|
|
|
587 |
</com:TTextHighlighter>
|
|
|
588 |
|
|
|
589 |
To retrieve a list of current users (formatted), we add this logic to the
|
|
|
590 |
<tt>ChatUserRecord</tt> class. We delete any users that may have been inactive
|
|
|
591 |
for awhile.
|
|
|
592 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90048">
|
|
|
593 |
public function getUserList()
|
|
|
594 |
{
|
|
|
595 |
$this->deleteAll('last_activity < ?', time()-300); //5 min inactivity
|
|
|
596 |
$content = '<ul>';
|
|
|
597 |
foreach($this->findAll() as $user)
|
|
|
598 |
$content .= '<li>'.htmlspecialchars($user->username).'</li>';
|
|
|
599 |
$content .= '</ul>';
|
|
|
600 |
return $content;
|
|
|
601 |
}
|
|
|
602 |
</com:TTextHighlighter>
|
|
|
603 |
|
|
|
604 |
<div class="note"><b class="tip">Note:</b>
|
|
|
605 |
For simplicity
|
|
|
606 |
we formatted the messages in these Active Record classes. For large applications,
|
|
|
607 |
these message formatting tasks should be done using Prado components (e.g. using
|
|
|
608 |
a TRepeater in the template or a custom component).
|
|
|
609 |
</div>
|
|
|
610 |
</p>
|
|
|
611 |
|
|
|
612 |
<h1 id="18017">Putting It Together</h1>
|
|
|
613 |
<p id="90103" class="block-content">Now comes to put the application flow together. In the <tt>Home.php</tt> we update
|
|
|
614 |
the <tt>Send</tt> buttons <tt>OnClick</tt> event handler to use the application
|
|
|
615 |
logic we just implemented.
|
|
|
616 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90049">
|
|
|
617 |
function processMessage($sender, $param)
|
|
|
618 |
{
|
|
|
619 |
if(strlen($this->userinput->Text) > 0)
|
|
|
620 |
{
|
|
|
621 |
$record = new ChatBufferRecord();
|
|
|
622 |
$record->message = $this->userinput->Text;
|
|
|
623 |
$record->from_user = $this->Application->User->Name;
|
|
|
624 |
$record->saveMessage();
|
|
|
625 |
|
|
|
626 |
$this->userinput->Text = '';
|
|
|
627 |
$messages = $record->getUserMessages($this->Application->User->Name);
|
|
|
628 |
$this->CallbackClient->appendContent("messages", $messages);
|
|
|
629 |
$this->CallbackClient->focus($this->userinput);
|
|
|
630 |
}
|
|
|
631 |
}
|
|
|
632 |
</com:TTextHighlighter>
|
|
|
633 |
We simply save the message to the chat buffer and then ask for all the messages
|
|
|
634 |
for the current user and update the client side message list using a callback
|
|
|
635 |
response (AJAX style).
|
|
|
636 |
</p>
|
|
|
637 |
|
|
|
638 |
<p id="90104" class="block-content">At this point the application is actually already functional, just not very
|
|
|
639 |
user friendly. If you open two different browsers, you should be able to communicate
|
|
|
640 |
between the two users whenever the <tt>Send</tt> button is clicked.
|
|
|
641 |
</p>
|
|
|
642 |
|
|
|
643 |
<p id="90105" class="block-content">The next part is perhaps the more tricker and fiddly than the other tasks. We
|
|
|
644 |
need to improve the user experience. First, we want a list of current users
|
|
|
645 |
as well. So we add the following method to <tt>Home.php</tt>, we can call
|
|
|
646 |
this method when ever some callback event is raised, e.g. when the <tt>Send</tt>
|
|
|
647 |
button is clicked.
|
|
|
648 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90050">
|
|
|
649 |
protected function refreshUserList()
|
|
|
650 |
{
|
|
|
651 |
$lastUpdate = $this->getViewState('userList','');
|
|
|
652 |
$users = ChatUserRecord::finder()->getUserList();
|
|
|
653 |
if($lastUpdate != $users)
|
|
|
654 |
{
|
|
|
655 |
$this->CallbackClient->update('users', $users);
|
|
|
656 |
$this->setViewstate('userList', $users);
|
|
|
657 |
}
|
|
|
658 |
}
|
|
|
659 |
</com:TTextHighlighter>
|
|
|
660 |
</p>
|
|
|
661 |
|
|
|
662 |
<p id="90106" class="block-content">Actually, we want to periodically update the messages and user list as new
|
|
|
663 |
users join in and new message may arrive from other users. So we need to refresh
|
|
|
664 |
the message list as well.</p>
|
|
|
665 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90051">
|
|
|
666 |
function processMessage($sender, $param)
|
|
|
667 |
{
|
|
|
668 |
...
|
|
|
669 |
$this->refreshUserList();
|
|
|
670 |
$this->refreshMessageList();
|
|
|
671 |
...
|
|
|
672 |
}
|
|
|
673 |
|
|
|
674 |
protected function refreshMessageList()
|
|
|
675 |
{
|
|
|
676 |
//refresh the message list
|
|
|
677 |
$finder = ChatBufferRecord::finder();
|
|
|
678 |
$content = $finder->getUserMessages($this->Application->User->Name);
|
|
|
679 |
if(strlen($content) > 0)
|
|
|
680 |
{
|
|
|
681 |
$anchor = (string)time();
|
|
|
682 |
$content .= "<a href=\"#\" id=\"{$anchor}\"> </a>";
|
|
|
683 |
$this->CallbackClient->appendContent("messages", $content);
|
|
|
684 |
$this->CallbackClient->focus($anchor);
|
|
|
685 |
}
|
|
|
686 |
}
|
|
|
687 |
</com:TTextHighlighter>
|
|
|
688 |
The anchor using <tt>time()</tt> as ID for a focus point is so that when the
|
|
|
689 |
message list on the client side gets very long, the focus method will
|
|
|
690 |
scroll the message list to the latest message (well, it works in most browsers).
|
|
|
691 |
</p>
|
|
|
692 |
|
|
|
693 |
<p id="90107" class="block-content">Next, we need to redirect the user back to the login page if the user has
|
|
|
694 |
been inactive for some time, say about 5 mins, we can add this check to any stage
|
|
|
695 |
of the page life-cycle. Lets add it to the <tt>onLoad()</tt> stage.
|
|
|
696 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90052">
|
|
|
697 |
public function onLoad($param)
|
|
|
698 |
{
|
|
|
699 |
$username = $this->Application->User->Name;
|
|
|
700 |
if(!$this->Application->Modules['users']->usernameExists($username))
|
|
|
701 |
{
|
|
|
702 |
$auth = $this->Application->Modules['auth'];
|
|
|
703 |
$auth->logout();
|
|
|
704 |
|
|
|
705 |
//redirect to login page.
|
|
|
706 |
$this->Response->Redirect($this->Service->ConstructUrl($auth->LoginPage));
|
|
|
707 |
}
|
|
|
708 |
}
|
|
|
709 |
</com:TTextHighlighter>
|
|
|
710 |
</p>
|
|
|
711 |
|
|
|
712 |
<h1 id="18018">Improving User Experience</h1>
|
|
|
713 |
<p id="90108" class="block-content">The last few details are to periodically check for new messages and
|
|
|
714 |
refresh the user list. We can accomplish this by polling the server using a
|
|
|
715 |
<com:DocLink ClassPath="System.Web.UI.ActiveControls.TTimeTriggeredCallback" Text="TTimeTriggeredCallback" />
|
|
|
716 |
control. We add a <tt>TTimeTriggeredCallback</tt> to the <tt>Home.page</tt>
|
|
|
717 |
and call the <tt>refresh</tt> handler method defined in <tt>Home.php</tt>.
|
|
|
718 |
We set the polling interval to 2 seconds.
|
|
|
719 |
<com:TTextHighlighter Language="prado" CssClass="source block-content" id="code_90053">
|
|
|
720 |
<com:TTimeTriggeredCallback OnCallback="refresh"
|
|
|
721 |
Interval="2" StartTimerOnLoad="true" />
|
|
|
722 |
</com:TTextHighlighter>
|
|
|
723 |
<com:TTextHighlighter Language="php" CssClass="source block-content" id="code_90054">
|
|
|
724 |
function refresh($sender, $param)
|
|
|
725 |
{
|
|
|
726 |
$this->refreshUserList();
|
|
|
727 |
$this->refreshMessageList();
|
|
|
728 |
}
|
|
|
729 |
</com:TTextHighlighter>
|
|
|
730 |
</p>
|
|
|
731 |
|
|
|
732 |
<p id="90109" class="block-content">The final piece requires us to use some javascript. We want that when the
|
|
|
733 |
user type some text in the textarea and press the <tt>Enter</tt> key, we want it
|
|
|
734 |
to send the message without clicking on the <tt>Send</tt> button. We add to the
|
|
|
735 |
<tt>Home.page</tt> some javascript.
|
|
|
736 |
|
|
|
737 |
<com:TTextHighlighter Language="javascript" CssClass="source block-content" id="code_90055">
|
|
|
738 |
<com:TClientScript>
|
|
|
739 |
Event.observe($("<%= $this->userinput->ClientID %>"), "keypress", function(ev)
|
|
|
740 |
{
|
|
|
741 |
if(Event.keyCode(ev) == Event.KEY_RETURN)
|
|
|
742 |
{
|
|
|
743 |
if(Event.element(ev).value.length > 0)
|
|
|
744 |
new Prado.Callback("<%= $this->sendButton->UniqueID %>");
|
|
|
745 |
Event.stop(ev);
|
|
|
746 |
}
|
|
|
747 |
});
|
|
|
748 |
</com:TClientScript>
|
|
|
749 |
</com:TTextHighlighter>
|
|
|
750 |
Details regarding the javascript can be explored in the
|
|
|
751 |
<a href="?page=Advanced.Scripts">Introduction to Javascript</a> section of the quickstart.
|
|
|
752 |
</p>
|
|
|
753 |
|
|
|
754 |
<p id="90110" class="block-content">This completes the tutorial on making a basic chat web application using
|
|
|
755 |
the Prado framework. Hope you have enjoyed it.
|
|
|
756 |
</p>
|
|
|
757 |
|
|
|
758 |
<div class="last-modified">$Id: AjaxChat.page 1846 2007-04-07 10:35:16Z wei $</div></com:TContent>
|