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