Subversion-Projekte lars-tiefland.prado

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
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
&lt;?php
59
class Login extends TPage
60
{
61
}
62
?&gt;
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
&lt;com:TForm&gt;
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
            &lt;com:TLabel ForControl="username" Text="Username:" /&gt;
78
            &lt;com:TTextBox ID="username" MaxLength="20" /&gt;
79
            &lt;com:TRequiredFieldValidator
80
                ControlToValidate="username"
81
                Display="Dynamic"
82
                ErrorMessage="Please provide a username." /&gt;
83
        </div>
84
        <div class="login-button">
85
            &lt;com:TButton Text="Login" /&gt;
86
        </div>
87
&lt;/com:TForm&gt;
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>&lt;authorization&gt;</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>&lt;module&gt;</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
&lt;com:TCustomValidator
281
    ControlToValidate="username"
282
    Display="Dynamic"
283
    OnServerValidate="checkUsername"
284
    ErrorMessage="The username is already taken." /&gt;
285
 
286
...
287
 
288
&lt;com:TButton Text="Login" OnClick="createNewUser" /&gt;
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
&lt;com:TForm&gt;
420
<h1 id="18014">Prado Chat Demo</h1>
421
<div id="messages" class="messages">
422
    &lt;com:TPlaceHolder ID="messageList" /&gt;
423
</div>
424
<div id="users" class="user-list">
425
    &lt;com:TPlaceHolder ID="userList" /&gt;
426
</div>
427
<div class="message-input">
428
    &lt;com:TActiveTextBox ID="userinput"
429
        Columns="40" Rows="2" TextMode="MultiLine" /&gt;
430
    &lt;com:TActiveButton ID="sendButton" CssClass="send-button"
431
        Text="Send" /&gt;
432
</div>
433
&lt;/com:TForm&gt;
434
&lt;com:TJavascriptLogger /&gt;
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
&lt;com:TActiveButton ID="sendButton" CssClass="send-button"
454
	Text="Send" OnClick="processMessage"/&gt;
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
&lt;com:TTimeTriggeredCallback OnCallback="refresh"
721
	Interval="2" StartTimerOnLoad="true" /&gt;
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
&lt;com:TClientScript&gt;
739
Event.observe($("&lt;%= $this->userinput->ClientID %&gt;"), "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("&lt;%= $this->sendButton->UniqueID %&gt;");
745
        Event.stop(ev);
746
    }
747
});
748
&lt;/com:TClientScript&gt;
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>