|
WebSnapNow that I've introduced the core elements of developing web server applications with Delphi, let's move to a more complex architecture available since Delphi 6: WebSnap. There are two good reasons I didn't jump into this topic at the beginning of this chapter. First, WebSnap builds on the foundation offered by WebBroker, so you cannot learn how to use the new features if you don't understand the underlying ones. For example, a WebSnap application is technically a CGI program, or an ISAPI or Apache module. Second, because WebSnap is included only in the Enterprise Studio version of Delphi, not all Delphi programmers have the chance to use it. WebSnap has a few definite advantages over WebBroker, such as allowing for multiple web modules each corresponding to a page, integrating server-side scripting, XSL, and the Internet Express technology (these last two elements will be covered in Chapter 22, "Using XML Technologies"). Moreover, many ready-to-use components are available for handling common tasks, such as user logins, session management, and so on. Instead of listing all the features of WebSnap, though, I've decided to cover them in a sequence of simple and focused applications. I built these applications using the Web App Debugger, for testing purposes, but you can easily deploy them using one of the other available technologies. When you're developing a WebSnap application, the starting point is a dialog box you can invoke either in the WebSnap page of the New Items dialog box (File ® New ® Other) or using the Internet toolbar in the IDE (which is not visible by default). The resulting dialog box, shown in Figure 20.6, allows you to choose the type of application (as in a WebBroker application) and to customize the initial application components (you'll be able to add more later). The bottom portion of the dialog determines the behavior of the first page (usually the default or home page of the program). A similar dialog box is displayed for subsequent pages. Figure 20.6: The options offered by the New Web-Snap Application dialog box include the type of server and a button that lets you select the core appli-cation components. If you choose the defaults and type in a name for the home page, the dialog box will create a project and open a TWebAppPageModule. This module contains the components you've chosen, by default:
You can view the HTML file in the Delphi editor thanks to that directive (with reasonably good syntax highlighting) by selecting the corresponding lower tab. The editor also has pages for a WebSnap module, including by default an HTML Result page where you can see the HTML generated after evaluating the scripts, and a Preview page hosting what a user will see in a browser. The Delphi 7 editor for a WebSnap module includes a much more powerful HTML editor than Delphi 6 had; it includes better syntax highlighting and code completion. If you prefer to edit your web application's HTML with a more sophisticated editor, you can set up your choice in the Internet page of the Environment Options dialog box. Click the Edit button for the HTML extension, and you can choose an external editor from the editor's shortcut menu or a specific button on Delphi's Internet toolbar. The standard HTML template used by WebSnap adds to any page of the program its title and the application title, using script lines such as these: <h1><%= Application.Title %></h1> <h2><%= Page.Title %></h2> We'll get back to the scripting in a while; we'll begin developing the WSnap1 example in the next section by creating a program with multiple pages. But first, I'll finish this overview by showing you the source code for a sample web page module: type Thome = class(TWebAppPageModule) ... end; function home: Thome; implementation {$R *.dfm} {*.html} uses WebReq, WebCntxt, WebFact, Variants; function home: Thome; begin Result := Thome(WebContext.FindModuleClass(Thome)); end; initialization if WebRequestHandler <> nil then WebRequestHandler.AddWebModuleFactory(TWebAppPageModuleFactory.Create( Thome, TWebPageInfo.Create([wpPublished {, wpLoginRequired}], '.html'), caCache)); end. The module uses a global function instead of a form's typical global object to support page caching. This application also has extra code in the initialization section (particularly registration code) to let the application know the role of the page and its behavior.
Managing Multiple PagesThe first notable difference between WebSnap and WebBroker is that instead of having a single data module with multiple actions eventually connected to producer components, WebSnap has multiple data modules, each corresponding to an action and having a producer component with an HTML file attached to it. You can add multiple actions to a page/module, but the idea is that you structure applications around pages rather than actions. As is the case with actions, the name of the page is indicated in the request path. As an example, I added two more pages to the WebSnap application (which was built with default settings). For the first page, in the New WebSnap Page Module dialog (see Figure 20.7) choose a standard page producer and name it date. For the second, choose a DataSetPageProducer and name it country. After saving the files, you can begin testing the application. Thanks to some of the scripting I'll discuss later, each page lists all the available pages (unless you've unchecked the Published check box in the New WebSnap Page Module dialog). The pages will be rather empty, but at least you have the structure in place. To complete the home page, you'll edit its linked HTML file directly. For the date page, use the same approach as a WebBroker application. Add some custom tags to the HTML text, like the following: <p>The time at this site is <#time>.</p> I also added code to the producer component's OnTag event handler to replace this tag with the current time. For the third page (the country page), modify the HTML to include tags for the various fields of the country table, as in: <h3>Country: <#name></h3> Then attach the ClientDataSet to the page producer: object DataSetPageProducer: TDataSetPageProducer DataSet = cdsCountry end object cdsCountry: TClientDataSet FileName = 'C:\Program Files\Common Files\Borland Shared\Data\country.cds' end To open this dataset when the page is first created and reset it to the first record in further invocations, you handle the OnBeforeDispatchPage event of the web page module, adding this code to it: cdsCountry.Open; cdsCountry.First; The fact that a WebSnap page can be very similar to a portion of a WebBroker application (basically, an action tied to a producer) is important, if you want to port existing WebBroker code to this new architecture. You can even port your existing DataSetTableProducer components to the new architecture. Technically, you can generate a new page, remove its producer component, replace it with a DataSetTableProducer, and hook this component to the PageProducer property of the web page module. In practice, this approach would cut out the HTML file of the page and its scripts. In the WSnap1 program, I used a better technique. I added a custom tag (<#htmltable>) to the HTML file and used the OnTag event of the page producer to add to the HTML the result of the data set table: if TagString = 'htmltable' then ReplaceText := DataSetTableProducer1.Content; Server-Side ScriptsHaving multiple pages in a server-side program—each associated with a different page module—changes the way you write a program. Having the server-side scripts at hand offers an even more powerful approach. For example, the standard scripts of the WSnap1 example account for the application and page titles, and for the index of the pages. This index is generated by an enumerator, the technique used to scan a list from within WebSnap script code. Let's look at it: <table cellspacing="0" cellpadding="0"><td> <% e = new Enumerator(Pages) s = '' c = 0 for (; !e.atEnd(); e.moveNext()) { if (e.item().Published) { if (c > 0) s += ' | ' if (Page.Name != e.item().Name) s += '<a href="' + e.item().HREF + '">' + e.item().Title + '</a>' else s += e.item().Title c++ } } if (c>1) Response.Write(s) %> </td></table> Inside the single cell of this table (which, oddly enough, has no rows), the script outputs a string with the Response.Write command. This string is built with a for loop over an enumerator of the application's pages, stored in the Pages global entity. The title of each page is added to the string only if the page is published. Each title uses a hyperlink with the exclusion of the current page. Having this code in a script, instead of hard-coded into a Delphi component, allows you to pass it to a good web designer, who can turn it into something a more visually appealing.
As a sample of what you can do with scripting, I added to the WSnap2 example (an extension of the WSnap1 example) a demoscript page. The page's script can generate a full table of multiplied values with the following scripting code (see Figure 20.8 for its output): <table border=1 cellspacing=0> <tr> <th> </th> <% for (j=1;j<=5;j++) { %> <th>Column <%=j %></th> <% } %> </tr> <% for (i=1;i<=5;i++) { %> <tr> <td>Line <%=i %></td> <% for (j=1;j<=5;j++) { %> <td>Value= <%=i*j %></td> <% } %> </tr> <% } %> </table> Figure 20.8: The WSnap2 example features a plain script and a custom menu stored in an include file. In this script, the <%= symbol replaces the longer Response.Write command. Another important feature of server-side scripting is the inclusion of pages within other pages. For example, if you plan to modify the menu, you can include the related HTML and script in a single file, instead of changing it and maintaining it in multiple pages. File inclusion is generally done with a statement like this: <!-- #include file="menu.html" --> In Listing 20.1, you can find the complete source code of the include file for the menu, which is referenced by all of the project's other HTML files. Figure 20.9 shows an example of this menu, which is displayed across the top of the page using the table-generation script mentioned earlier.
Listing 20.1: The menu.html File Included in Each Page of the WSnap2 Example
<html> <head> <title><%= Page.Title %></title> </head> <body> <h2><%= Application.Title %></h2> <table cellspacing="0" cellpadding="2" border="1" bgcolor="#c0c0c0"> <tr> <% e = new Enumerator(Pages) for (; !e.atEnd(); e.moveNext()) { if (e.item().Published) { if (Page.Name != e.item().Name) Response.Write ('<td><a href="' + e.item().HREF + '">' + e.item().Title + '</a></td>') else Response.Write ('<td>' + e.item().Title + '</td>') } } %> </tr> </table> <hr> <h1><%= Page.Title %></h1> <p> This script for the menu uses the Pages list and the Page and Application global scripting objects. WebSnap makes available a few other global objects, including EndUser and Session objects (in case you add the corresponding adapters to the application), the Modules object, and the Producer object, which allows access to the Producer component of the web page module. The script also has available the Response and Request objects of the web module. AdaptersIn addition to these global objects, within a script you can access all the adapters available in the corresponding web page module. (Adapters in other modules, including shared web data modules, must be referenced by prefixing their name with the Modules object and the corresponding module.) Adapters allow you to pass information from your compiled Delphi code to the interpreted script, providing a scriptable interface to your Delphi application. Adapters contain fields that represent data and host actions that represent commands. The server-side scripts can access these values and issue these commands, passing specific parameters to them. Adapter FieldsFor simple customizations, you can add new fields to specific adapters. For instance, in the WSnap2 example, I added a custom field to the application adapter. After selecting this component, you can either open its Fields editor (accessible via its shortcut menu) or work in the Object TreeView. After adding a new field (called AppHitCount in the example), you can assign a value to it in its OnGetValue event. Because you want to count the hits (or requests) on any page of the web application, you can also handle the OnBeforePageDispatch event of the global PageDispatcher component to increase the value of a local field, HitCount. Here is the code for the two methods: procedure Thome.PageDispatcherBeforeDispatchPage(Sender: TObject; const PageName: String; var Handled: Boolean); begin Inc (HitCount); end; procedure Thome.AppHitCountGetValue(Sender: TObject; var Value: Variant); begin Value := HitCount; end; Of course, you could use the page name to also count hits on each page (and you could add support for persistency, because the count is reset every time you run a new instance of the application). Now that you've added a custom field to an existing adapter (corresponding to the Application script object), you can access it from within any script, like this: <p>Application hits since last activation: <%= Application.AppHitCount.Value %></p> Adapter ComponentsIn the same way, you can add custom adapters to specific pages. If you need to pass along a few fields, use the generic Adapter component. Other custom adapters (besides the global ApplicationAdapter you've already used) include these:
Using the AdapterPageProducerMost of these components are used in conjunction with an AdapterPageProducer component. The AdapterPageProducer can generate portions of script after you visually design the desired result. As an example, I've added to the WSnap2 application the inout page, which has an adapter with two fields, one standard and one Boolean: object Adapter1: TAdapter OnBeforeExecuteAction = Adapter1BeforeExecuteAction object TAdapterActions object AddPlus: TAdapterAction OnExecute = AddPlusExecute end object Post: TAdapterAction OnExecute = PostExecute end end object TAdapterFields object Text: TAdapterField OnGetValue = TextGetValue end object Auto: TAdapterBooleanField OnGetValue = AutoGetValue end end end The adapter also has a couple of actions that post the current user input and add a plus sign (+) to the text. The same plus sign is added when the Auto field is enabled. Developing the user interface for this form and the related scripting would take some time using plain HTML. But the AdapterPageProducer component (used in this page) has an integrated HTML designer, which Borland calls Web Surface Designer. Using this tool, you can visually add a form to the HTML page and add an AdapterFieldGroup to it. Connect this field group to the adapter to automatically display editors for the two fields. Then you can add an AdapterCommandGroup and connect it to the AdapterFieldGroup, to provide buttons for all of the adapter's actions. You can see an example of this designer in Figure 20.9. To be more precise, the fields and buttons are automatically displayed if the AddDefaultFields and AddDefaultCommands properties of the field group and command group are set. The effect of these visual operations to build this form are summarized in the following DFM snippet: object AdapterPageProducer: TAdapterPageProducer object AdapterForm1: TAdapterForm object AdapterFieldGroup1: TAdapterFieldGroup Adapter = Adapter1 object FldText: TAdapterDisplayField FieldName = 'Text' end object FldAuto: TAdapterDisplayField FieldName = 'Auto' end end object AdapterCommandGroup1: TAdapterCommandGroup DisplayComponent = AdapterFieldGroup1 object CmdPost: TAdapterActionButton ActionName = 'Post' end object CmdAddPlus: TAdapterActionButton ActionName = 'AddPlus' end end end end Now that you have an HTML page with some scripts to move data back and forth and issue commands, let's look at the source code required to make this example work. First, you must add to the class two local fields to store the adapter fields and manipulate them, and you need to implement the OnGetValue event for both, returning the field values. When each button is clicked, you must retrieve the text passed by the user, which is not automatically copied into the corresponding adapter field. You can obtain this effect by looking at the ActionValue property of these fields, which is set only if something was entered (for this reason, when nothing is entered you set the Boolean field to False). To avoid repeating this code for both actions, place it in the OnBeforeExecuteAction event of the web page module: procedure Tinout.Adapter1BeforeExecuteAction(Sender, Action: TObject; Params: TStrings; var Handled: Boolean); begin if Assigned (Text.ActionValue) then fText := Text.ActionValue.Values [0]; fAuto := Assigned (Auto.ActionValue); end; Notice that each action can have multiple values (in case components allow multiple selections); but this is not the case, so you can grab the first element. Finally, here is the code for the OnExecute events of the two actions: procedure Tinout.AddPlusExecute(Sender: TObject; Params: TStrings); begin fText := fText + '+'; end; procedure Tinout.PostExecute(Sender: TObject; Params: TStrings); begin if fAuto then AddPlusExecute (Self, nil); end; As an alternative, adapter fields have a public EchoActionFieldValue property you can set to get the value entered by the user and place it in the resulting form. This technique is typically used in case of errors, to let the user change the input starting with the values already entered.
Scripts Rather Than Code?Even this example of the combined use of an adapter and an adapter page producer, with its visual designer, shows the power of this architecture. However, this approach also has a drawback: By letting the components generate the script (in the HTML, you have only the <#SERVERSCRIPT> tag), you save a lot of development time; but you end up mixing the script with the code, so changes to the user interface will require updating the program. The division of responsibilities between the Delphi application developer and the HTML/script designer is lost. And, ironically, you end up having to run a script to accomplish something the Delphi program could have done right away, possibly much faster. In my opinion, WebSnap is a powerful architecture and a huge step forward from WebBroker, but it must be used with care to avoid misusing some of these technologies just because they are simple and powerful. For example, it might be worth using the AdapterPageProducer designer to generate the first version of a page, and then grabbing the generated script and copying it to a plain Page-Producer's HTML, so that a web designer can modify the script with a specific tool. For nontrivial applications, I prefer the possibilities offered by XML and XSL, which are available within this architecture even if they don't have a central role. You'll find more on this topic in Chapter 22. Locating FilesWhen you have written an application like the one just described, you must deploy it as a CGI or dynamic library. Instead of placing the templates and include files in the same folder as the executable file, you can devote a subfolder or custom folder to host all the files. The LocateFileService component handles this task. The component is not intuitive to use. Instead of having you specify a target folder as a property, the system fires one of this component's events any time it has to locate a file. (This approach is much more powerful.) There are three events: OnFindIncludeFile, OnFindStream, and OnFindTemplateFile. The first and last events return the name of the file to use in a var parameter. The OnFindStream event allows you to provide a stream directly, using one you already have in memory or that you've created on the fly, extracted from a database, obtained via an HTTP connection, or gotten any other way you can think of. In the simplest case of the OnFindIncludeFile event, you can write code like the following: procedure TPageProducerPage2.LocateFileService1FindIncludeFile( ASender: TObject; AComponent: TComponent; const AFileName: String; var AFoundFile: String; var AHandled: Boolean); begin AFoundFile := DefaultFolder + AFileName; AHandled := True; end; |
|
Copyright © 2004-2024 "Delphi Sources" by BrokenByte Software. Delphi Programming Guide |
|