|
Delphi ASP.NET Portal Programming - Felix John COLIBRI.
|
- abstract : presentation, architecture and programming of the Delphi Asp
Net Portal. This is a Delphi version of the Microsoft ASP.NET Starter Kit
Web Portal showcase
- key words : ASP.NET - Web Portal - Starter Kit
- software used : Windows XP Personal, Cassini Web Server, Delphi 2006,
Asp.Net v1.1.4322 or Asp.Net v2.0.50727, Interbase 6 or Interbase 7.5
- hardware used : Pentium 2.800Mhz, 512 M memory, 25 G hard disc
- scope : Windows developers - Delphi 8 to 2006, Turbo Delphi - Interbase 6
or 7.5, Asp.Net 1 or 2
- level : Web developer - Asp.Net developer
- plan :
1 - The Delphi Asp.Net Web Portal
1.1 - Purpose
The goal of a Portal is to offer a framework which can be used to build Web
pages having the same look an feel and which can be customized using .HTML
pages included in the Portal.
We are going to present the Asp.Net Starter Kit
portal, in a Delphi version.
Here is a snapshot of the basic Starter Kit page:
1.2 - The Asp.Net Starter Kit
The Asp.Net Portal comes bundled with a tutorial
module, as well as many .HTML files explaining how to use the portal, from an
administrator point of view. There are also .HMTL pages of the source code,
which can be displayed by directly looking at the .CS or .ASPX/ASCX files
anyway.
This paper will explain how we handled the project.
1.3 - What was changed
The basic "Asp.Net Starter Kit" files are
- in C# language
- for ASP.Net version 1
- the database is Sql Server, with all SQL scripts included, and the binary
file
- the web server is IIS (Internet Information Server)
We basically started off with the Delphi translation made by
Pascal Chapuis, which used
- Delphi 8 or Delphi 2005
- either Firebird, or Sql Server. For Firebird, the FBK backup file is
provided, for Sql Server the full Sql scripts are included
- the access components are the Firebird Data Provider, or the Sql Data
Provider
- the web server is IIS or Cassini
This version was transformed in the following way
- we are using Delphi 2006
- the Database is Interbase 6, or 7.5, with the SQL scripts as well as the
binary file
- the access component set is the BDP (Borland Data Provider)
- the Web Server is CASSINI
- Asp.Net is version 1 or 2
1.4 - The strategy
Here is a summary of our modifications
- we renamed many of the files and identifiers
- we created some additional folders
- we removed the Mobile part
- we commented out the security and Asp.Net cache part
1.5 - The File Organization
We will use the following folders:
And:
- _data: contains the Interbase database file, and the SQL scripts
- _exe: contains the intermediate compilation files (.DCUIL) and our logs
(MUST be in the same folder as the .BDSPROJ)
- 1_helpers: the configuration units, the security handling, and some global
helper units (litteral strings etc)
- 2_db_components: most of the units making a connection to the database (with
the exception of U_C_CONFIGURATION)
- 3_elements: all components and pages other than the main a_asp_portal.ASPX
- the common files: the portal banner, the Module title, the Module
ancestor CLASS
- 31_user_components:
- all ASCX Module components
- 311_edit_user: the ASPX Module edition pages
- 32_admin_components
- the ASCX Modules for administering the portal (changing the portal
configuration)
- 321_edit_admin the ASPX pages for portal administration
- bin: Asp.Net JIT compiled code. DO NOT change anything there
- Data: the files used by the "About the Portal" tab (presentation and
tutorial about administering the portal)
- images: the .GIF used by the different .ASPX and .ASCX files
- portal_images: our own logo, placed in the banner
Note that there is also a hidden "__history" folders created and used by
Delphi, as well as hidden files with ~nnn~ extensions, all handled by Delphi
1.6 - The detail of the project
We will now present
- the programming architecture
- the database
- the configuration files
- the pages used by the users
- the administration of the portal
2 - The Overall Web Portal Architecture
As explained in the Delphi Asp.Net Portal architecture paper, the user sees the
Portal as several different pages. Each pages includes
- a banner
- several tabs, like in a NoteBook, one of them being selected
- several modules
Such pages are called, in Portal parlance, "views".
From a programming viewpoint, the views are assembled using the following
elements:
- the main frame is the a_asp_portal.aspx template, with
- an instance of the a_c_desktop_portal_banner.ascx Asp.Net component
- an .HTML Table with three cells, called panes, where the modules will be
stuffed
Here is the organization:
- the a_c_desktop_portal_banner.ascx Asp.Net component contains
- the site icon
- the site title
- links to the home page and the index
- more important, a DataList which will be used for the tabs
The banner looks like this:
- each Module is an .ASCX Asp.Net component which has
- some kind of header. It can be a simple Label, but in most case it is an
a_c_desktop_module_title.ascx Asp.Net component with a Label and an
Hiperlink (for calling a Module edit page)
- some content control (a DataGrid, a DataList, or some TextBoxes)
Here is the a_c_quicklinks.ascx Module (no module title) and the
a_c_html_module.ascx Module (with an a_c_desktop_module_title.ascx module
title):
Since all Modules must manage some identification parameters common to all
Modules, an abstract c_portal_module_control ancestor class has been
created with attributes for ModuleId, PortalConfiguration etc.
So all Module CLASSes (like TWebUserControlContacts, for instance)
descend from this common CLASS.
The HyperLink control placed on the Module title is used do call an edit
page associated with the module. For instance, clicking on the "Edit"
hyperlink of an a_c_announcements.ascx will request and display the the
a_edit_announcements.aspx page (which contains TextBoxes, Validators,
Buttons etc). The Edit page still has a banner (to keep the same look), but
without any tabs.
This can be represented like this:
A complete view, with initialized tabs and some modules, looks like this:
In addition to the usual Asp.Net code files (.PAS,.ASPX, .ASCX), the Portal
also uses configuration and content data files:
- the structure files:
- WEB.CONFIG
- a ASPNETPORTAL.CSS Cascading Style Sheet for the font and graphic
properties
- all the images (the arrows, the Asp.Net Logo etc)
- x_asp_portal.xml which contains the structure and properties of each view
(basically what Module is contained in each view, and in which Pane it
is displayed) and module definitions (the file name of each Module)
- the content files:
- the Interbase database, whith the content of the Modules (the .HTML
text of the a_c_html_module.ascx Module, the list of discussions from
the a_c_discussion.ascx Module etc)
- an .XML/XSD sample SALES.XML file to show how to include .XML in the
a_c_xml.ascx Module
- image (.GIF) wich are displayed in some Modules, mainly in the
a_c_html_module.ascx .HTML content
This can be represented like this:
We will now present in more detail
- the Interbase Database
- the configuration file
- the User pages
- the Administration pages
3 - The Delphi Web Portal Database
3.1 - The Database Schema
The database schema is the following:
Note that
- only 9 of the 16 Modules have database content. For the other Modules
- a_c_xml.ascx uses an .XML file
- a_c_image.ascx uses .GIFs
- the other modules manage in memory data
- the portal_useroles is an N-N link table
3.2 - Database Creation
We have provided the Sql script which enables the Interbase database creation.
In addition, we have included the .ZIP of the complete Interbase 6 and
Interbase 7.5 database.
3.3 - Ado.Net and the ConnectionString
We will use Ado.Net to transfer data to and from the Database.
Ado.Net requires a ConnectionString, which contains the detail of the driver,
the .Net assembly, the file location etc.
This connection string is set in the Web.Config global file:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!-- application specific settings -->
<appSettings>
<add key="ConnectionString" value="assembly=Borland.Data.Interbase,Version=2.5.0.0,
...ooo... username=sysdba;password=masterkey"/>
<add key="ConnectionOptions" value="waitonlocks=False;commitretain=False;
...ooo... " />
</appSettings>
...ooo...
|
|
So fetching the connection string will be performed using:
var my_connection_string: String;
my_xxx_connection: xxxpConnection;
my_connection_string:= ConfigurationSettings.AppSettings.Get('ConnectionString');
my_xxx_connection: xxxpConnection.Create(my_connection_string);
|
3.4 - The data table access classes
To fetch or modify the content of the tables, separate CLASSes have been
defined containing the main method for handling the access. For instance, for
handling contacts, the U_C_CONTACTS_DB contains the following CLASS:
Unit u_c_contact_db;
interface
uses system.Configuration, system.Data
, Borland.Data.Provider
, Borland.Data.Common
;
type ContactsDB=
class
public
constructor Create;
function GetContacts(moduleId: Integer): DataSet;
function GetSingleContact(itemId: Integer): BdpDataReader;
function AddContact(moduleId: Integer; itemId: Integer;
userName: Widestring; name: Widestring; role:
Widestring; email: Widestring; contact1:
Widestring; contact2: Widestring): Integer;
procedure DeleteContact(itemID: Integer);
procedure UpdateContact(moduleId: Integer; itemId:
Integer; userName: Widestring; name: Widestring;
role: Widestring; email: Widestring; contact1:
Widestring; contact2: Widestring);
end; // ContactsDB
//
|
3.5 - Access to the Database
To access those tables, we chose to use the BDP. We had not much choice anyway,
since this is the most obvious way to access Interbase from Delphi 2006.
In the basic product, nearly all requests use stored procedures. So our
Interbase database contains those stored procedures.
Here is the Get_Events stored procedure:
CREATE PROCEDURE PORTAL_GETEVENTS (p_module_id INTEGER)
RETURNS
(
pv_item_id INTEGER,
pv_created_by_user VARCHAR(100),
pv_created_date TIMESTAMP,
pv_title VARCHAR(150),
pv_where_when VARCHAR(150),
pv_description VARCHAR(2000),
pv_expire_date TIMESTAMP
)
AS
BEGIN
FOR
SELECT
itemid,
title,
createdbyuser,
wherewhen,
createddate,
title,
expiredate,
description
FROM Portal_Events
WHERE
(ModuleID = :p_module_id)
AND
(ExpireDate > Current_timestamp)
INTO
:pv_item_id,
:pv_title,
:pv_created_by_user,
:pv_where_when,
:pv_created_date,
:pv_title,
:pv_expire_date,
:pv_description
DO
SUSPEND;
END
|
Using the Sql Adapter components, this procedure could be called with:
function EventsDB.f_c_get_events(p_module_id: Integer): DataSet;
var l_c_sql_connection: SqlConnection;
l_c_sql_transaction: SqlTransaction;
l_c_sql_parameter1: SqlParameter;
l_c_sql_adapter: SqlDataAdapter;
l_c_dataset: DataSet;
begin
l_c_sql_connection:= SqlConnection.Create(k_configuration_string);
l_c_sql_connection.Open;
l_c_sql_transaction:= l_c_sql_connection.BeginTransaction;
l_c_sql_adapter:= SqlDataAdapter.Create('Portal_GetEvents', l_c_sql_connection);
l_c_sql_adapter.SelectCommand.Transaction:= l_c_sql_transaction;
l_c_sql_adapter.SelectCommand.CommandType:= CommandType.StoredProcedure;
l_c_sql_parameter1:= SqlParameter.Create('@ModuleID', SqlDbType.Integer, 4);
l_c_sql_parameter1.Value:= system.object(p_module_id);
l_c_sql_adapter.SelectCommand.Parameters.Add(l_c_sql_parameter1);
l_c_dataset:= DataSet.Create;
l_c_sql_adapter.Fill(l_c_dataset);
result:= l_c_dataset;
l_c_sql_transaction.Commit;
l_c_sql_transaction.Dispose;
l_c_sql_connection.Close;
end; // f_c_get_events
|
With the BPD, it seems that we have to initialize ALL parameters (not only the
INPUT parameters). Adding a parameters is performed with 3 lines (which
certainly could be concatenated). In addition starting the request always uses
the same calls. So we created the following helper functions:
- to start the connection:
const k_connection_string= 'xxx...';
var g_open_bdp_connection_count: Integer= 0;
procedure open_bdp_connection(p_c_bdp_connection: BdpConnection);
begin
p_c_bdp_connection.Open();
Inc(g_open_bdp_connection_count);
end; // open_bdp_connection
procedure open_portal_bdp_connection(var pv_c_bdp_connection: BdpConnection);
begin
pv_c_bdp_connection:= BdpConnection.Create(k_connection_string);
open_bdp_connection(pv_c_bdp_connection);
end; // open_portal_bdp_connection
procedure open_portal_bdp_connection_transaction(
var pv_c_bdp_connection: BdpConnection;
var pv_c_bdp_transaction: BdpTransaction);
begin
open_portal_bdp_connection(pv_c_bdp_connection);
if g_open_bdp_connection_count<= 0
then pv_c_bdp_transaction:= pv_c_bdp_connection.Begintransaction
else pv_c_bdp_transaction:= Nil;
end; // open_portal_bdp_connection_transaction
|
- to initialize the Stored Procedure parameters:
function f_c_add_input_parameter(p_c_bdp_command: BdpCommand;
p_name: String; p_type, p_subtype: BdpType; p_size: Integer;
p_value: System.Object): BdpParameter;
begin
Result:= BdpParameter.Create(p_name, p_type, p_size);
Result.Direction:= ParameterDirection.Input;
Result.Value:= p_value;
p_c_bdp_command.Parameters.Add(Result);
end; // add_input_parameter
function f_c_add_output_parameter(p_c_bdp_command: BdpCommand;
p_name: String; p_type, p_subtype: BdpType;
p_size, p_max_precision: Integer): BdpParameter;
begin
Result:= BdpParameter.Create(p_name, p_type, p_size);
Result.MaxPrecision:= p_max_precision;
Result.Direction:= ParameterDirection.Output;
p_c_bdp_command.Parameters.Add(Result);
end; // add_output_parameter
|
And our Get_Events stored procedure is called with:
function EventsDB.GetEvents(p_module_id: Integer): DataSet;
var l_c_bdp_connection: BdpConnection;
l_c_bdp_transaction: BdpTransaction;
l_c_bdp_dataadapter: BdpDataAdapter;
l_c_dataset: DataSet;
begin
open_portal_bdp_connection_transaction(l_c_bdp_connection, l_c_bdp_transaction);
l_c_bdp_dataadapter:= BdpDataAdapter.Create('Portal_GetEvents', l_c_bdp_connection);
l_c_bdp_dataadapter.SelectCommand.Transaction:= l_c_bdp_transaction;
l_c_bdp_dataadapter.SelectCommand.CommandType:= CommandType.StoredProcedure;
f_c_add_input_parameter(l_c_bdp_dataadapter.SelectCommand, 'MODULEID',
BdpType.Int32, BdpType.Int32, 4, System.Object(p_module_id));
f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand, 'ITEMID',
BdpType.Int32, BdpType.Int32, 4, 4);
f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand, 'CREATEDBYUSER',
BdpType.String, BdpType.String, 100, 100);
f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand, 'CREATEDDATE',
BdpType.DateTime, BdpType.DateTime, 16, 16);
f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand, 'TITLE',
BdpType.String, BdpType.String, 150, 150);
f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand, 'WHEREWHEN',
BdpType.String, BdpType.String, 150, 150);
f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand, 'DESCRIPTION',
BdpType.String, BdpType.String, 2000, 2000);
f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand, 'EXPIREDATE',
BdpType.DateTime, BdpType.DateTime, 16, 16);
l_c_dataset:= DataSet.Create;
l_c_bdp_dataadapter.Fill(l_c_dataset);
result:= l_c_dataset;
l_c_bdp_transaction.Commit;
l_c_bdp_transaction.Dispose;
close_bdp_connection(l_c_bdp_connection);
end; // get_with_stored_procedure
|
In fact the code in the .ZIP uses routines with optional loging functionalities
(with journal inputs for database connection, connection count, results etc),
which helped us debug the database access.
Using those debugging functions, we also found out that the BDP Stored
Procedures did not work as expected. At least we could not make them run
whenever the procedure returns a DataSet (deleting, adding, updating or
retrieveing single lines worked, but FOR SELECT did not work). So for those
cases, we replaced the stored procedure call with a simple SQL Request, which
was used to fill the target DataSet. For instance:
function EventsDB.f_c_get_events_2(p_module_id: Integer): DataSet;
const k_request=
' SELECT ItemID, Title, CreatedByUser, WhereWhen, '
+ CreatedDate, Title, ExpireDate, Description'
+ ' FROM Portal_Events'
+ ' WHERE (ModuleID = ?)'; // AND (ExpireDate > Current_timestamp)';
var l_c_bdp_connection: BdpConnection;
l_c_bdp_transaction: BdpTransaction;
l_c_bdp_dataadapter: BdpDataAdapter;
l_c_dataset: DataSet;
begin
open_portal_bdp_connection_transaction(l_c_bdp_connection, l_c_bdp_transaction);
l_c_bdp_dataadapter:= BdpDataAdapter.Create(k_request, l_c_bdp_connection);
f_c_add_input_parameter(l_c_bdp_dataadapter.SelectCommand, 'MODULEID',
BdpType.Int32, BdpType.Int32, 4, System.Object(moduleId));
l_c_dataset:= DataSet.Create;
l_c_bdp_dataadapter.Fill(l_c_dataset);
Result:= l_c_dataset;
l_c_bdp_transaction.Commit;
l_c_bdp_transaction.Dispose;
close_bdp_connection(l_c_bdp_connection);
end; // f_get_with_request_2
|
4 - The Delphi .Net Portal Configuration files
4.1 - Web.Config
This file contains the following information:
So the main information are
- the Interbase connection string
- the authentication mode (Forms or Windows)
4.2 - The Portal .XML Configuration File
4.2.1 - X_PORTAL_CONFIG.XML
Here is a view of the X_PORTAL_CONFIG.XML file (partial display):
Basically, this file tells us two things
- how Modules are incorporated in the Views
- the parameters of each Module
The link between the two is the "ModuleDefId"
The basic structure of this .XML can be represented by this free style BNF-like
definition:
config.xml= header <site_configuration ...> .
header= <?XML ...> .
site_configuration= <SITECONFIGURATION global tabs module_definitions ...> .
global= <GLOBAL ...> .
tabs= { <TAB { module } ...> } .
module= <MODULE [ settings ] ...> .
settings = <SETTINGS { setting } ...> .
setting = <SETTING ...> .
module_definitions= { <MODULEDEFINITION ...> } .
|
where
- <GLOBAL > contains the main title and a global edition parameter
- <TAB > describes one view: it corresponds to one of the banner "tab" (like
the tabs of a notebook), and when this tab is selected, the modules
specified within the <TAB> tag will be displayed
- <MODULE > describes each module of the view: which module, at which
position etc
- <SETTINGS > for some modules, there are additional parameters, which
are presented in this tag
- <MODULEDEFINITION > describes each available modules (a module for
discussions, one for links, one for .HTML text etc)
If we display the same information removing all .XML attribute keys, we get:
1 Home All Users; 1
1 QuickLinks Admins; 8 LeftPane 0 1 false
2 Welcome to the Portal Admins; 5 ContentPane 0 1 true
3 News and Features Admins; 1 ContentPane 0 2 true
4 Upcoming Events Admins; 4 ContentPane 0 3 true
5 This Weeks Special Admins; 5 RightPane 0 1 false
6 Top Movers Admins; 9 RightPane 0 2 false
2 Employee Info All Users; 3
7 Spy Diary Admins; 5 LeftPane 0 1 false
8 HR Benefits Admins; 1 ContentPane 0 1 true
9 Employee Contact Inform. Admins; 2 ContentPane 0 2 true
10 New Employee Documentation Admins; 10 ContentPane 0 3 false
3 Product Info All Users; 5
11 Spy Diary Admins; 5 LeftPane 0 1 false
12 Competition: TradeCraft Admins; 1 ContentPane 0 1 true
13 Competition: Surveillance Admins; 1 ContentPane 0 2 true
14 Competition: Protection Admins; 1 ContentPane 0 3 true
15 Night Vision Goggles Admins; 6 RightPane 0 1 false
16 Competitors to Watch Admins; 8 RightPane 0 2 false
4 Discussions All Users; 7
17 Spy Diary Admins; 5 LeftPane 0 1 false
18 TradeCraft Techniques and Admins; 3 ContentPane 0 1 false
19 Recipes From the Field Admins; 3 ContentPane 0 2 false
20 GoodReads Admins; 10 ContentPane 0 3 false
5 About the Portal All Users; 9
21 Quick Links Admins; 8 LeftPane 0 1 false
22 About the Portal Starter Admins; 5 ContentPane 0 1 true
23 Portal Tabs Admins; 5 ContentPane 0 2 false
24 Portal Modules Admins 5 ContentPane 0 3 false
25 Managing the Portal Admins; 5 ContentPane 0 4 false
26 Managing Portal Layout Admins; 5 ContentPane 0 5 false
27 Managing User Security Admins; 5 ContentPane 0 6 false
6 Admin Admins; 11
28 Module Definitions Admins; 11 RightPane 0 1 false
29 Site Settings Admins; 14 ContentPane 0 1 false
30 Tabs Admins; 13 ContentPane 0 2 false
31 Security Roles Admins; 12 ContentPane 0 3 false
32 Manage Users Admins; 15 ContentPane 0 4 false
Announcements 5_module_components/a_c_announcements.ascx 1
Contacts 5_module_components/a_c_contacts.ascx 2
Discussion 5_module_components/a_c_discussion.ascx 3
Events 5_module_components/a_c_events.ascx 4
Html Document 5_module_components/a_c_html_module.ascx 5
Image 5_module_components/a_c_image_module.ascx 6
Links 5_module_components/a_c_links.ascx 7
QuickLinks 5_module_components/a_c_quick_links.ascx 8
XML/XSL 5_module_components/a_c_xml_module.ascx 9
Documents 5_module_components/a_c_document.ascx 10
Module Types (Admin) 6_admin_components/a_c_module_definitions.ascx 11
Roles (Admin) 6_admin_components/a_c_roles.ascx 12
Tabs (Admin) 6_admin_components/a_c_tabs.ascx 13
Site Settings (Admin) 6_admin_components/a_c_site_settings.ascx 14
Manage Users (Admin) 6_admin_components/a_c_users.ascx 15
|
4.2.2 - U_C_PORTAL_CONFIGURATION.PAS
On the code side, this .XML file is managed by the CLASSes contained in
U_C_PORTAL_CONFIGURATION.PAS. This file was automatically generated using
Babel_Code (a .CS to .PAS utility), from the original CONFIGURATION.CS file.
This is a huge file (40 K for the .CS) and the Delphi counterpart has a 700
line INTERFACE.
This INTERFACE contains a main c_site_configuration CLASS modeling the
whole .XML file, with many subclasses for each part of the .XML structure:
- GlobalRow for <Global PortalId="0" ...>
- TabRow for <Tab TabId="1" TabName="Home" ...>
- ModuleRow for <Module ModuleId="1" ModuleTitle="QuickLinks" ...>
and with intermediate CLASSes for the structure (lists of tabs, lists of
modulerow), which are called table_xxx, since they will be DataTable
descendents. For the modules, for instance, we have:
ModuleDataTable= class(DataTable, System.Collections.IEnumerable)
...
|
The base c_site_configuration CLASS is essentially used by the
U_C_CONFIGURATION's CLASSes (and also by U_C_SECURITY and
U_MODULE_DEFINITIONS)
4.2.3 - U_C_CONFIGURATION.PAS
The configuration is then managed by several CLASSES contained in
U_C_CONFIGURATION.PAS
The CLASSes of this unit also manage the configuration of the portal, but are
not so tightly coupled with the .XML file. The information is retrieve from the
.XML using the c_site_configuration CLASS, but it is massaged in several
ways which are more easy to use in the different parts of the code.
The CLASSes found in this unit are:
- c_module_info: allows to sort the Modules by ModuleOrder
- c_module_item: encapsulation of the Module attributes. Mainly used by the
administration pages
- c_configuration: creates and modifies the basic c_site_configuration
CLASS
- c_tab_info= class: represents the Tab array
- c_tab_strip_details: represent a single tab
- c_tab_item: the main tab properties
- c_portal_info: the main class used to get the configuration parameters
Note that
- we replaced the original "_setting" denomination with "_info", since
<SETTINGS> is used in the .XML file to specify some MODULE sub part
4.2.4 - Loading and Using the Configuration
The information contained in the .XML is loaded during the
TGlobal.Application_BeginRequest (GLOBALS.PAS) event. This is one of the the
very first event of each Asp.Net request, and it is used here to fetch the .XML
and load it into an in-memory DataSet. A reference to this Dataset is included
in the HTTP Context, which is available from nearly everywhere in Asp.Net.
Here is the DataSet creation:
var l_tab_index, l_tab_id: integer;
l_c_portal_info: c_portal_info;
if assigned(Request.Params['TabIndex'])
then l_tab_index:= Int32.Parse(Request.Params['TabIndex'])
else l_tab_index:= 0;
if assigned(Request.Params['TabId'])
then l_tab_id:= Int32.Parse(Request.Params['TabId'])
else l_tab_id:= 1;
l_c_portal_info:= c_portal_info.Create(l_tab_index, l_tab_id);
Context.Items.Add('portal_info_key', l_c_portal_info);
HttpContext.Current.Items.Add('site_configuration_key',
u_c_configuration.c_configuration.f_c_site_configuration());
|
and:
- for the first request, there are no TabIndex, TabId parameters, so the
default will be used (TabIndex=0, TabId=1, which is the main view)
- whenever the USER clicks on a Tab, the parameters of this tab will be
appended to the request, and the relevant configuration will be retrieved
In both cases, the configuration is now in the HTTP Context, which travels thru
all the layers of the Asp.Net HTTP Pipeline, and will be retrieved using this
context:
- the 'portal_info_key' will retrieve the c_portal_info CLASS
- the 'site_configuration_key' will retrieve the c_site_configuration CLASS
(mainly in U_C_CONFIGURATION.PAS, and U_C_SECURITY.PAS)
For instance, to set the portal name in the banner (A_C_PORTAL_BANNER.PAS):
procedure TDesktopPortalBanner.Page_Load(sender: System.Object ;
e: System.EventArgs);
var l_c_portal_info: c_portal_info;
begin
l_c_portal_info:=
c_portal_info(HttpContext.Current.Items['portal_info_key']);
siteName.Text:= l_c_portal_info.PortalName;
|
5 - The User Files
We present here the elements that the normal user (not an administrator) will
see. They are:
- the Modules with their optional title
- the Module Edit pages
- the banner with the tabs
5.1 - The Modules
The Modules are Asp.Net components, which are created and placed on the View's
Panes.
The module is made of a title and a content.
Here is the A_C_CONTACTS.ASCX in the Delphi IDE (the top third of the snapshot
is the Delphi IDE part. Only the central white rectangle is the Module
control):
and the same when loaded on the default page (the purple color comes from our
CenterPane coloring):
In the present case, the module contains
- a title custom component, which is an instance of the TDesktopModuleTitle
Asp.Net component
- the content of the Module (a DataGrid in this case)
This organization is not mandatory, but frequent. In fact, the Portal contains
different Module types, to present how to handle different kind of
informations: .HTML content, .XML content, .GIF images etc.
5.1.1 - The Module component ancestor
All Module components (user and administration) descend from a basic Asp.Net
component which is used to handle the common attributes:
unit u_c_desktop_controls;
interface
uses ... ;
type c_portal_module_control=
class(UserControl)
private
_moduleConfiguration: c_module_info;
_isEditable: integer;
_portalId: integer;
_settings: Hashtable;
function GetModuleId: integer;
function GetIsEditable: boolean;
function GetSettings: Hashtable;
published
property ModuleId: integer read GetModuleId;
property PortalId: integer read _PortalId write _PortalId;
property IsEditable: boolean read GetIsEditable;
property ModuleConfiguration:
c_module_info read _moduleConfiguration
write _moduleConfiguration;
property Settings: Hashtable read GetSettings;
// ...
end; // c_portal_module_control
====
unit a_c_contacts;
interface
uses ...
, u_c_desktop_controls;
type TWebUserControlContacts=
class(c_portal_module_control)
strict private
procedure InitializeComponent;
strict private
procedure Page_Load(sender: System.Object ;
e: System.EventArgs);
strict protected
myDataGrid: System.Web.UI.WebControls.DataGrid;
procedure OnInit(e: System.EventArgs); override;
end; // TWebUserControlContacts
|
5.1.2 - The Module Title
Most Module component use an instance of the a_c_desktop_module_title.ascx
Module title Asp.Net component.
Here is a snapshot of this component displayed in the Delphi IDE:
This component contains
- the ModuleTitle Label
- the EditButton Hyperlink, which will be initialized to load the Module
Edit .Aspx page
5.1.3 - Contacts.ascx
Here is the Contacts.Ascx template file
- before the selected part is the .ASPx header
- the selected part is the Module Title component (with its EditText title
and EditUrl link for calling the Module edition a_edit_contacts.aspx page)
- after the selected part is the DataGrid
On the action side, the a_c_contacts.TWebUserControlContacts.Page_Load event
will fetch the events and fill the DataGrid :
procedure TWebUserControlContacts.Page_Load(sender: System.Object ;
e: System.EventArgs);
var l_c_contact_db: ContactsDB;
l_c_dataset: DataSet;
begin
l_c_contact_db:= ContactsDB.Create();
l_c_dataset:= l_c_contact_db.GetContacts(ModuleId);
if l_c_dataset.Tables.Count> 0
then begin
myDataGrid.DataSource:= l_c_contact_db.GetContacts(ModuleId);
myDataGrid.DataBind();
end;
end; // Page_Load
|
5.2 - The Portal Banner
Here is a snapshot of the a_c_desktop_portal_banner.ascx component in the Ide
(with our debugging colors):

and the corresponding .ASCX template is:
<%@ Control Language="c#" AutoEventWireup="false"
CodeBehind="a_c_desktop_portal_banner.pas"
Inherits="a_c_desktop_portal_banner.TDesktopPortalBanner"%>
<table class="HeadBg" cellspacing="0" width="100%" border="0"
bgcolor="#ff99ff">
<tbody>
<tr valign="top">
<td style="WIDTH: 68px; HEIGHT: 50px">
<p align="center">
<img style="WIDTH: 45px; HEIGHT: 40px" hspace="0"
src="k_logo_png" align="middle" vspace="2" height="45">
<font size="1">
<a class="sitelink" href="k_http://www.felix-colibri.com/">
k_F_Colibri
</a>
</font>
</p>
</td>
<td style="WIDTH: 336px; HEIGHT: 50px">
& nbsp ;
<asp:Label id="siteName" width="310px" height="32px" runat="server"
cssclass="SiteTitle" font-size="20pt">
k_Delphi_Asp.Net_Portal
</asp:Label>
</td>
<td style="WIDTH: 464px; HEIGHT: 50px">
<p align="left">
<asp:Label id="WelcomeMessage" width="342px" height="19"
runat="server" forecolor="#eeeeee" backcolor="#C0FFFF">
</asp:Label>
<a class="SiteLink"href="c:\programs\us\web\asp_net\
delphi_asp_portal\prog\a_asp_portal.aspx">
k_Index
</a>
</p>
</td>
</tr>
</tbody>
</table>
<asp:DataList id="tabs" runat="server" cssclass="OtherTabsBg"
itemstyle-borderwidth="1" selecteditemstyle-cssclass="TabBg"
itemstyle-height="25" enableviewstate="false"
repeatdirection="horizontal" bordercolor="White" backcolor="#C0C0FF">
<SelectedItemStyle cssclass="TabBg">
</SelectedItemStyle>
<SelectedItemTemplate>
& nbsp ;
<span class="SelectedTab">
<%# ((u_c_configuration.c_tab_strip_details) Container.DataItem)
.TabName %>
</span>
& nbsp ;
</SelectedItemTemplate>
<ItemStyle height="25px" borderwidth="1px">
</ItemStyle>
<ItemTemplate>
& nbsp ;
<a href='<%=u_c_portal_helpers.TGlobalHelpers
.GetApplicationPath2(Request) %>/a_asp_portal.aspx
?tabindex=<%# Container.ItemIndex %>
&tabid=<%# ((u_c_configuration.c_tab_strip_details)
Container.DataItem).TabId %>' class="OtherTabs">
<%# ((u_c_configuration.c_tab_strip_details)
Container.DataItem).TabName %>
</a>
& nbsp ;
</ItemTemplate>
</asp:DataList>
|
|
where
- at the start is the header
- then the .HTML Table for the logo, the home URL etc
- at the end the DataList for the Tab array
And here is the Page_Load event which fills the Tab array:
procedure TDesktopPortalBanner.Page_Load(sender: System.Object ;
e: System.EventArgs);
var l_c_portal_info: c_portal_info;
l_c_tab_strip_detail: c_tab_strip_details;
l_c_authorized_tab_array: ArrayList;
l_tab_index: integer;
begin
l_c_portal_info:= c_portal_info(HttpContext.Current.Items['portal_info_key']);
// -- set the site name
siteName.Text:= l_c_portal_info.PortalName;
// --- If user logged in, customize welcome message
if Request.IsAuthenticated
then
begin
// --- if authentication mode is Cookie, provide a logoff link
if (Context.User.Identity.AuthenticationType= 'Forms')
then LogoffLink:= TGlobalHelpers.GetApplicationPath2(Request)+
k_logoff_module_aspx;
// --- initialize welcome, and the logoff link
WelcomeMessage.Text:= (k_welcome_message.Replace('%1s', Context.
User.Identity.Name))+ k_logoff_message.Replace('%1s',
LogoffLink);
end;
// -- set the active tab index
TabIndex:= 1;
if ShowTabs
then TabIndex:= l_c_portal_info.m_c_active_tab_info.TabIndex
else
begin
// --- toggle visibility
tabs.Visible:= FALSE;
exit;
end;
// -- Build list of tabs to be shown to user
l_c_authorized_tab_array:= ArrayList.Create();
// -- fill the array and set the selected index
for l_tab_index:= 0 to l_c_portal_info.m_c_desktop_tab_array.Count- 1 do
begin
write_to_asp_net_log('select_tab ? '+ l_tab_index.ToString);
l_c_tab_strip_detail:= c_tab_strip_details(l_c_portal_info.m_c_desktop_tab_array[l_tab_index]);
// -- add to the array if security is ok
if c_portal_security.IsInRoles(l_c_tab_strip_detail.AuthorizedRoles)
then l_c_authorized_tab_array.Add(l_c_tab_strip_detail);
// -- set the selected tab
if l_tab_index= tabIndex
then tabs.SelectedIndex:= l_tab_index;
end; // for l_tab_index
// -- render the Page tab array with authorized tabs
tabs.DataSource:= l_c_authorized_tab_array;
tabs.DataBind();
end; // Page_Load
|
5.3 - The Main page
The main page a_asp_portal.aspx looks like this:
and the initialization of the Modules in the Panes takes place during the
OnInit event (the loading from cache has been removed in our display):
procedure t_asp_portal_form.OnInit(e: EventArgs);
var l_c_portal_info: c_portal_info;
l_c_module_info: c_module_info;
l_c_portal_module_control: c_portal_module_control;
l_c_parent_control: Control;
l_module_index: integer;
begin
InitializeComponent;
inherited OnInit(e);
// -- Obtain c_portal_info from Current Context
l_c_portal_info:= c_portal_info(HttpContext.Current.Items['portal_info_key']);
// -- Ensure that the visiting user has access to the current page
if not c_portal_security.IsInRoles(l_c_portal_info.m_c_active_tab_info.AuthorizedRoles)
then Response.Redirect(k_access_denied_module_aspx);
// -- Dynamically inject a signin login module into the top left-hand corner
// -- of the home page if the client is not yet authenticated
if not Request.IsAuthenticated and (l_c_portal_info.m_c_active_tab_info.TabIndex= 0)
then
begin
Page.FindControl('LeftPane').Controls.Add(Page.LoadControl(k_signin_module_ascx));
Page.FindControl('LeftPane').Visible:= true;
end;
// -- Dynamically Populate the Left, Center and Right pane sections of the portal page
// -- with the modules defined in x_portal_cfg
if l_c_portal_info.m_c_active_tab_info.Modules.Count> 0
then
// -- Loop through each entry in the configuration system for this tab
for l_module_index:= 0 to l_c_portal_info.m_c_active_tab_info.Modules.Count- 1 do
begin
// -- in which parent pane should this module be positioned ?
l_c_module_info:=
c_module_info(l_c_portal_info.m_c_active_tab_info.Modules[l_module_index]);
l_c_parent_control:= Page.FindControl(l_c_module_info.PaneName);
// -- create the user control instance and dynamically
// -- inject it into the page.
l_c_portal_module_control:=
c_portal_module_control(Page.LoadControl(l_c_module_info.DesktopSrc));
l_c_portal_module_control.PortalId:= l_c_portal_info.PortalId;
l_c_portal_module_control.ModuleConfiguration:= l_c_module_info;
// -- add the module to its parent
l_c_parent_control.Controls.Add(l_c_portal_module_control);
// -- Dynamically inject separator break between portal modules
l_c_parent_control.Controls.Add(LiteralControl.Create('<br>'));
l_c_parent_control.Visible:= true;
end; // for l_module_index
end; // OnInit
|
5.4 - The Global.Pas File
The Global.BeginRequest, which has already been partially presented above, and
here is a more complete version (the language initialization removed):
procedure TGlobal.Application_BeginRequest(sender: System.Object ;
e: EventArgs);
var l_tab_index, l_tab_id: integer;
l_c_portal_info: c_portal_info;
begin
// -- default values
l_tab_index:= 0;
l_tab_id:= 1;
// -- Get l_tab_index from querystring
if (assigned(Request.Params['TabIndex']))
then l_tab_index:= Int32.Parse(Request.Params['TabIndex']);
// -- Get l_tab_id from querystring
if (assigned(Request.Params['TabId']))
then l_tab_id:= Int32.Parse(Request.Params['TabId']);
l_c_portal_info:= c_portal_info.Create(l_tab_index, l_tab_id);
Context.Items.Add('portal_info_key', l_c_portal_info);
// -- Retrieve and add the configuration DataSet to the current Context
HttpContext.Current.Items.Add('site_configuration_key',
u_c_configuration.c_configuration.f_c_site_configuration());
end; // Application_BeginRequest
|
5.5 - The basic cycle
5.5.1 - The User calls the first time
The basic scenario is the following:
|
the Web Server waits for incoming requests:
|
|
the User calls the default a_asp_portal.aspx file using his Browser
|
|
the Asp.Net Server gets the request and then
- calls the Globals.BeginRequest event to load the configuration and
include it in the Http.Context
- the a_asp_portal.aspx.Page_Load event is called. Since there is no
ModuleId parameter, the default value of 1, corresponding to the first
tab is used. The configuration for this View is grabbed from the
Context, which tells which Modules should be loaded
- each Module is built in turn, with its title and content filled
The corresponding page is sent back the user
|
Here is what the User will get:
In an UML Sequence diagram, this would look like this:
Granted, this is not a bona-fide UML sequence diagram. Usually:
- they contain Objets at the top of the diagram (and not file names)
- Grady Booch included neither any object-container, like we used for modules,
nor any multiplicity (modules again)
- when an .ASPX contains an Asp.Net custom control, there is no visible method
call to instanciate the component class (at the developer level). This is
performed inside Asp.Net
- and, or course, there are no fancy colors !
We could have presented the standard events called upon each Asp.Net Request:
BeginRequest(), AuthenticateRequest(), AuthorizeRequest(), ... all the way
up to PreSendRequest(). But this would have been too detailed, given the
number of components involved here.
So we have presented this diagram instead, hoping that it somehow still conveys
the processing involved at the Web Server level.
5.5.2 - The User selects another Tab
When the User has received his .HTML page, he can click on a tab or on any
hyperlink.
Selecting any Tab, this will trigger the "OnClick" event of this item. This
handling is located in the a_c_desktop_portal_banner.ascx template. The text of
the .ASCX has been presented above, but the real processing has been drowned in
the formating flood. Here is a more focused version:
and:
These snippets display the TEMPLATE code, which is located on the Server. When
the Server receives a request, it uses the template and the .PAS code to build
the true .HTML page that is sent over to the User. In our case, after
receiving the default page, we saved it on disc, and here is a partial listing
of this default view:
So when the User clicks on a tab, it is the hilighted link that is clicked,
and this link contains exactly the .HTML request for the page with the View
parameters.
The scenario looks like this
|
the User clicks a Tab, which sends the request to the Server
|
|
the Server builds the page and returns the .HTML to the User:
|
And here is what the user will see after clicking the "Product Info" tab:
6 - The Administration Files
Those Modules are used to let users with Administrator rights modify the
x_portal_cfg.xml file, which in fact reorganizes the presentation of the Views.
We will not present those Modules in this paper
7 - What is missing - Lessons learned
7.1 - What is missing in this downloadable version
You can download the .ZIP of
the project. In those files, we removed the following parts
- all Mobile files were removed. We do not use a mobile so we could not test
those parts. The mobile parameters are however still present in the
configuration files and CLASSes
- we commented out the security part. When we started to use the Portal, we
could not access to anything, and we had to use a Data Explorer to
understand the password and registering system. In addition, having to fight
with passwords during all trials tests was a pain in the neck. So we
commented out the security checks
- we removed the security (logging, roles etc) and the Asp.Net caching
In addition, our purpose being the presentation of the overall architecture, we
did not test all features. For instance:
- we did not check the User editing
- we did not check the administration Modules
In the production version of the portal, the security and caching are
reinstalled.
7.2 - The Renaming and Prefixing
In order to be sure that we understood the interactions between all the parts,
we performed, as ususal, a massive renaming of files and variables.
Lets take an example: what is Text ? Text is:
- the name of a file Type (the ascii buffered FILE)
- the name of a tWinComponent PROPERTY (the Win32 WindowString equivalent),
which becomes Caption or Text, and is available even in tForms, even if
it is not surfaced in the Object Inspector
But since Text is a predefined word (and not a KEYword), you can name any
identifier of your choice Text: a CONST, a TYPE, a VAR, and also
parameters, procedures, functions, meth |