menu
  index  ==>  papers  ==>  web  ==>  asp_net  ==>  delphi_asp_net_portal_programming   

Delphi ASP.NET Portal Programming - Felix John COLIBRI.


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:

image



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:

image

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:

    image

  • 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:

    image

  • 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):

    image

    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:

    image

A complete view, with initialized tabs and some modules, looks like this:

image



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:

image



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:

image

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_stringString;
    my_xxx_connectionxxxpConnection;
 
  my_connection_string:= ConfigurationSettings.AppSettings.Get('ConnectionString');
  my_xxx_connectionxxxpConnection.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.Configurationsystem.Data
        , Borland.Data.Provider
        , Borland.Data.Common
        ;

    type ContactsDB=
        class
           public
             constructor Create;

             function GetContacts(moduleIdInteger): DataSet;
             function GetSingleContact(itemIdInteger): BdpDataReader;

             function AddContact(moduleIdIntegeritemIdInteger;
                   userNameWidestringnameWidestringrole:
                   WidestringemailWidestringcontact1:
                   Widestringcontact2Widestring): Integer;
             procedure DeleteContact(itemIDInteger);
             procedure UpdateContact(moduleIdIntegeritemId:
                   IntegeruserNameWidestringnameWidestring;
                   roleWidestringemailWidestringcontact1:
                   Widestringcontact2Widestring);
         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_idInteger): DataSet;
  var l_c_sql_connectionSqlConnection;
      l_c_sql_transactionSqlTransaction;
      l_c_sql_parameter1SqlParameter;
      l_c_sql_adapterSqlDataAdapter;
      l_c_datasetDataSet;
  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_countInteger= 0;

    procedure open_bdp_connection(p_c_bdp_connectionBdpConnection);
      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_connectionBdpConnection);
      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_connectionBdpConnection;
        var pv_c_bdp_transactionBdpTransaction);
      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_commandBdpCommand;
        p_nameStringp_typep_subtypeBdpTypep_sizeInteger;
        p_valueSystem.Object): BdpParameter;
      begin
        Result:= BdpParameter.Create(p_namep_typep_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_commandBdpCommand;
      p_nameStringp_typep_subtypeBdpType;
      p_sizep_max_precisionInteger): BdpParameter;
    begin
      Result:= BdpParameter.Create(p_namep_typep_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_idInteger): DataSet;
  var l_c_bdp_connectionBdpConnection;
      l_c_bdp_transactionBdpTransaction;
      l_c_bdp_dataadapterBdpDataAdapter;
      l_c_datasetDataSet;
  begin
    open_portal_bdp_connection_transaction(l_c_bdp_connectionl_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.Int32BdpType.Int32, 4, System.Object(p_module_id));

    f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand'ITEMID',
        BdpType.Int32BdpType.Int32, 4, 4);
    f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand'CREATEDBYUSER',
        BdpType.StringBdpType.String, 100, 100);
    f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand'CREATEDDATE',
        BdpType.DateTimeBdpType.DateTime, 16, 16);
    f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand'TITLE',
        BdpType.StringBdpType.String, 150, 150);
    f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand'WHEREWHEN',
        BdpType.StringBdpType.String, 150, 150);
    f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand'DESCRIPTION',
        BdpType.StringBdpType.String, 2000, 2000);
    f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand'EXPIREDATE',
        BdpType.DateTimeBdpType.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_idInteger): DataSet;
  const k_request=
        ' SELECT ItemID, Title, CreatedByUser, WhereWhen, '
      +       CreatedDateTitleExpireDateDescription'       + '   FROM Portal_Events'       + '   WHERE (ModuleID = ?)'; //  AND (ExpireDate > Current_timestamp)';
  var l_c_bdp_connectionBdpConnection;
      l_c_bdp_transactionBdpTransaction;
      l_c_bdp_dataadapterBdpDataAdapter;
      l_c_datasetDataSet;
  begin
    open_portal_bdp_connection_transaction(l_c_bdp_connectionl_c_bdp_transaction);

    l_c_bdp_dataadapter:= BdpDataAdapter.Create(k_requestl_c_bdp_connection);
    f_c_add_input_parameter(l_c_bdp_dataadapter.SelectCommand'MODULEID',
        BdpType.Int32BdpType.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:

image

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):

image

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.xmlheader <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:

ModuleDataTableclass(DataTableSystem.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_indexl_tab_idinteger;
    l_c_portal_infoc_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_indexl_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(senderSystem.Object ;
    eSystem.EventArgs);
  var l_c_portal_infoc_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):

image

and the same when loaded on the default page (the purple color comes from our CenterPane coloring):

image

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
                 _moduleConfigurationc_module_info;
                 _isEditableinteger;
                 _portalIdinteger;
                 _settingsHashtable;

                 function GetModuleIdinteger;
                 function GetIsEditableboolean;
                 function GetSettingsHashtable;
               published
                 property ModuleIdinteger read GetModuleId;
                 property PortalIdinteger read _PortalId write _PortalId;
                 property IsEditableboolean read GetIsEditable;
                 property ModuleConfiguration:
                     c_module_info read _moduleConfiguration
                     write _moduleConfiguration;
                 property SettingsHashtable 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(senderSystem.Object ;
                      eSystem.EventArgs);
                strict protected
                  myDataGridSystem.Web.UI.WebControls.DataGrid;
                  procedure OnInit(eSystem.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:

image

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

image

  • 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(senderSystem.Object ;
    eSystem.EventArgs);
  var l_c_contact_dbContactsDB;
      l_c_datasetDataSet;
  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(senderSystem.Object ;
    eSystem.EventArgs);
  var l_c_portal_infoc_portal_info;
      l_c_tab_strip_detailc_tab_strip_details;
      l_c_authorized_tab_arrayArrayList;
      l_tab_indexinteger;
  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_indextabIndex
        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:

image

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(eEventArgs);
  var l_c_portal_infoc_portal_info;
      l_c_module_infoc_module_info;
      l_c_portal_module_controlc_portal_module_control;
      l_c_parent_controlControl;
      l_module_indexinteger;
  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(senderSystem.Object ;
    eEventArgs);
  var l_tab_indexl_tab_idinteger;
      l_c_portal_infoc_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_indexl_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:

image

   the User calls the default a_asp_portal.aspx file using his Browser

image

   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

image



Here is what the User will get:

image



In an UML Sequence diagram, this would look like this:

image

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:

image

and:

  • the selected tab title is drawn using:

    image

  • the non-selected tab titles are drawn using:

    image

  • these lines build the

        "p_delphi_asp_net_portal/a_aspx_portal.aspx?tabindex=123&tabid=456"

    request:

    image



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:

image

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

image

   the Server builds the page and returns the .HTML to the User:

image



And here is what the user will see after clicking the "Product Info" tab:

image




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