ASP.NET Web Forms and Databases *

By: Bob Swart

Abstract: In this session, learn how to build, debug, and deploy ASP.NET Web Form applications that work with databases using Borland Data Provider for ADO.NET. Specific topics include using the asp:DataGrid, Borland database Web controls, input validators, login, and security.

Bob Swart (aka Dr.Bob - www.drbob42.com) is an independent consultant, trainer, author & Webmaster for Bob Swart Training & Consultancy (eBob42) using Delphi (for .NET), C#Builder, Kylix, and C++Builder, and has spoken at Delphi and Borland Developer Conferences since 1993. Bob is a trainer who has presented all over the world (USA, BeNeLux, Germany, Italy, Singapore and for UK-BUG in London, Manchester, Reading and Edinburgh). He has written his own training material, including the Delphi 8 for .NET Essentials and Delphi 8 for .NET ASP.NET courseware material licensed by Borland. Bob is co-author of the Revolutionary Guide to Delphi 2, Delphi 4 Unleashed, C++Builder 4 Unleashed, C++Builder 5 Developer's Guide, Kylix Developer's Guide, Delphi 6 Developer's Guide, and the upcoming C++Builder 6 Developer's Guide, as well as author for a number of computer magazines, including The Delphi Magazine, Delphi Developer, UK-BUG Developer's Magazine, SDGN Magazine and Blaise, as well as the TechRepublic (CNET), DevX, Borland-IBM DB2 Web portal and the Borland Developer Network Web site, and his own Dr.Bob's Delphi Clinic at http://www.drbob42.com. Together with Marco Cantu, Bob received the Spirit of Delphi award in 1999 from Borland at BorCon.

b.swart@chello.nl

Bob Swart (aka Dr.Bob) Delphi 8 for .NET and ASP.NET Web Forms
In this session, we learn how to build, debug and deploy ASP.NET Web Form applications that work with databases using BDP for ADO.NET. Specific topics include using the asp:DataGrid, Borland DB Web controls, input validators, login, and security.


New ASP.NET Application
Start Delphi 8 for .NET, and create a new ASP.NET Web Application. Call it project WebForm, so the URL to test it on your local development machine will be http://localhost/WebForm. Note that I've specified Internet Information Server (IIS) as web server option, but you can also use Cassini to test the web form application.

When you click on OK, the new project is created with an empty ASP.NET web form. Save the project, and make sure the form is saved in file Default.aspx, so it will be shown automatically when visitors go to the virtual directory.

ASP.NET and Login
In order to add login and authentication support, we need to edit the web.config file and mark the Default.aspx page as a protected resource for which authorization is required. This can be done as follows:

  <location path="Default.aspx">
    <system.web>
      <authorization>
        <deny users="?"/>
      </authorization>
    </system.web>
  </location>
This will make sure that no anonymous users (i.e. visitors who are not logged in) can view the page. In order to specify the user "Bob" with password "Swart", you need to add another section to the global <system.web> node in the web.config file, with the following contents:
  <system.web>
    ...
    <authentication mode="Forms">
      <forms loginUrl="login.aspx">
        <credentials>
          <user name="Bob" password="Swart" />
        </credentials>
      </forms>
    </authentication>
  </system.web>
Note that there already is an <authentication> node, with mode set to Windows, so you need to replace that node with the new section.
In order to secure the password, you can apply SHA1 encryption, which results in the following final modification of the new <location> section in web.config:
  <system.web>
    ...
    <authentication mode="Forms">
      <forms loginUrl="login.aspx">
        <credentials passwordFormat="SHA1">
          <user name="Bob" password="B0F0B58D128159F8EAFEB02FF53BCCAF0198C232" />
        </credentials>
      </forms>
    </authentication>
  </system.web>
Note that all other pages are not secured, so the authorization and authentication nodes are only relevant to the Default.aspx page. Before you can run the WebFormX ASP.NET application, you now first need to build a login page (in file login.aspx, as specified in the loginUrl attribute).
Do File | New - Other, and create a new ASP.NET Page, save this new page in login.aspx. Insert a HTML table with 3 rows, 2 columns, and the border size set to 0. Place the text Username: and Password in the first two cells in the left column, and place two TextBox controls in the first two cells in the right column, called tbUsername and tbPassword. You may want to set the TextMode property of the tbPassword control to Password to make sure no plain text us shown.
Finally, drop a Button called btnLogin on the form, and a Label called lbLogin (with the Text cleared). You can use this lbLogin label to display login error messages.

Set the Text property of btnLogin to Login, add the System.Web.Security unit to the uses clause, and write the following code in the Click event handler:

  procedure TWebForm1.btnLogin_Click(sender: System.Object; e: System.EventArgs);
  begin
    if FormsAuthentication.Authenticate(tbUsername.Text,tbPassword.Text) then
      FormsAuthentication.RedirectFromLoginPage(tbUsername.Text, False)
    else
      lbLogin.Text := 'Login incorrect!'
  end;
You can now compile and run the WebFormX ASP.NET application again. When start the project, you will not directly be taken to the Default.aspx page, but instead to the login.aspx page. The URL in the browser will be set to:
  http://localhost/WebFormX/login.aspx?ReturnUrl=%2fWebFormX%2fDefault.aspx
After a successful login, you will be redirected to the value of ReturnUrl as specified on this URL.

Borland Data Providers
The next thing we need to do is add the connection to the database. I will use the Borland Data Provider for .NET in this example, which allows us to connect to InterBase, DB2, Oracle, MS Access, or SQL Server/MSDE (depending on the edition of Delphi 8 for .NET that you have). We'll use InterBase for this example, so from the Data Explorer (in the upper-right corner), open up the InterBase connection node, and select the IBConn1 node. Right-click on this node in order to configure it. This results in the Connections Editor. Here, you need to specify the path to the InterBase database as value of the Database property. Note that just the path is not enough, since you also need to prefix it with the name of the machine where the InterBase database is located (relative to the ASP.NET web server application that is, which means we can use localhost here). Without the name of the machine (or IP-address), the connection can still be made at design-time, but will fail at runtime.
Apart from the Database property, you also need to specify the UserName and Password values, which are sysdba and masterkey for the Employee.gdb database.

Before you close this dialog, it's a good idea to click on the Test button, you at least you know if you didn't make any mistake with these property values.
Once the connection works, you can close the dialog. Then, you can open up the IBConn1 node, which shows subnodes for the Tables, Views and (stored) Procedures in the InterBase database. For our example, we need a table, so open up the Table node. This will show the eleven tables in the InterBase database, including the EMPLOYEE table. Now, click on the EMPLOYEE table and drag it to your ASP.NET web form and drop it there. This will create two new components in the non-visual area of the HTML Designer: BdpConnection1 and BdpDataAdapter1.

Configure Data Adapter
Select the BdpDataAdapter1 component and click on the "Configure Data Adapter" verb at the bottom of the Object Inspector. This will start the Data Adapter Configuration dialog. This dialog can be used to examine or modify the configuration of the BdpDataAdapter. In this example, the BdpDataAdapter was automatically created since we dragged the EMPLOYEE table to the designer area. So it shouldn't come as a surprise that the BdpDataAdapter is connected to the BdpConnection component (which was also automatically created) and has four predefined SQL Command properties with ready to use SQL statements for SELECT, UPDATE, INSERT and DELETE.

You can still change the SQL Commands here, if you wish. Once you're happy, you need to go to the last tab (DataSet) to specify that the output of the preconfigured SELECT query should be placed in a new dataset called dataSet1. Then, click on OK to close this dialog, which will result in a third component in the non-visual area of the designer, namely dataSet1.
Now, set the Active property of the BdpDataAdapter1 component to True to make sure we get live data at design-time (which is a great help to design and test the page without having to guess how the final page will look like in the end).

Designing the Web Form
With the dataset in place, it's time to build the visual part of the data entry form. We can do this with a regular asp:DataGrid control, which can display more than one record at a time.
Initially, the ASP.NET DataGrid will show three columns and five rows (not counting the header) with "abc" values. But we can bind the DataGrid to the dataSet1 component using the DataSource property. And apart from pointing DataSource to dataSet1, you also need to point the DataMember property to EMPLOYEE (the name of the internal table in the DataSet). The DataMember is used when there are more than one DataTable inside the DataSet to pick from.
Alternately, you can set only the DataSource property to DataTable1 - the component name of the DataTable in DataSet1 (containing the data from the EMPLOYEE table). Assigning DataSource to DataTable1 means you don't have to assign anything to the DataMember property, of course. Both ways will result in the names of the fields being shown as headers of the DataGrid (but without the actual data).
Before you can see the actual output in the DataGrid, you must make sure that the data from the DataSet's EMPLOYEE DataTable member is actually bound to the DataGrid. It doesn't matter that the BdpDataAdapter is active and the EMPLOYEE table is available in the DataSet component: the DataGrid must be bound to the data every time we make a significant change to the data (like sorting on a different column or refreshing the contents from the server).
This needs to be done at runtime, and the best place is in the ASP.NET Page_Load event handler. Double-click on the web form which will bring you to the code editor for the Page_Load event. In here, you need to write the following code:

  procedure TWebForm1.Page_Load(sender: System.Object; e: System.EventArgs);
  begin
    if not IsPostBack then
      dataGrid1.DataBind
  end;
Note that we use the IsPostBack property in order to make sure the DataGrid is only bound the first time we get into the Page_Load event (and not unnecessarily all subsequent times). This results in better performance, but also means we have to explicitly call dataGrid1.DataBind; when we change something inside the data table.
We can now compile and run the application from the IDE. The result can be seen below:

C:Inetpubwwwroot now contains a (virtual) directory called WebForm with the project files. This directory also has a Bin subdirectory where the WebForm.dll is generated. From a browser, we can also start this ASP.NET Web Forms application with the direct URL http://localhost/WebForm/WebForm1.aspx. (or WebForm2.aspx - depending on whether you started with a new empty form, or continued with the existing web form). In both cases, you'll see all records (which can be quite a lot) without the option to sort the records or display the results in pages of certain sizes.

Customising the DataGrid
You can customise the DataGrid, and add paging and sorting capabilities. If you select the DataGrid in the HTML Designer, you'll notice the Auto Format... and Property Builder... verbs in the Object Inspector. Click on Auto Format to set the look of the DataGrid, for example to Professional 1.
Now, start the Property Builder to customise the DataGrid even further. We start with the General page, where you can specify the data key field as well as the "Allow sorting" option.
On the Columns page, we can specify which columns to show. By default, the option "Create columns automatically at run time" is set. If you want to overrule this, you need to uncheck this option, and then manually drag columns from the available columns list to the selected columns list (tip: use the "All Fields" option to get them all at once).
For each column, you can specify a number of option, like the header text, the footer text, the header image (if you want), the sort expression (we'll get back to that in a moment), whether or not this field should be shown in the DataGrid using the visible check, and an option to specify if this field should be read-only or not.

Sorting the DataGrid
In order to allow the sorting to work, you have to specify the SortExpression, and write some code for the SortCommand event. For each column, you can set the sort expression, typically to the field name, which will turn the header of the column into a hyperlink, and if you click on the hyperlink then the SortCommand event handler is executed, passing the specific value of the Sort Expression as argument. Since this is the actual fieldname, all we have to do to sort the DataGrid is to perform the SQL query again, but this time add an "ORDER BY" clause, followed by the fieldname which is passed as Sort Expression.
The Delphi 8 for .NET source code for the SortCommand event handler of the DataGrid is as follows:

  procedure TWebForm1.DataGrid1_SortCommand(source: System.Object;
    e: System.Web.UI.WebControls.DataGridSortCommandEventArgs);
  begin
    Session['OrderBy'] := e.SortExpression;
    bdpDataAdapter1.Active := False; // close
    bdpSelectCommand1.CommandText := 'SELECT EMP_NO, FIRST_NAME, LAST_NAME, ' +
     'PHONE_EXT, HIRE_DATE, DEPT_NO, JOB_CODE, '+
     'JOB_GRADE, JOB_COUNTRY, SALARY FROM EMPLOYEE ORDER BY ' +
      String(Session['OrderBy']);
    bdpDataAdapter1.Active := True; // re-open
    dataGrid1.DataBind // refresh data
  end;
Note that we store the SortExpression in the ASP.NET Session variable. This means that it's available when we get back to the web form, and we can use this information ensure that the same "order by" ordering is used if we ever have to re-issue the SELECT query again (for example when we want to jump to another page).

Paging the DataGrid
Speaking of going to the next page: you can specify the number of maximum records that the DataGrid should show on a specific page (remember the screenshot that showed all records in the grid - not a big deal for this EMPLOYEE table from the InterBase database, but a potential problem when using a bigger table in the real-world, of course). The dialog specifies a page size of 10 rows. This means that you can get a number of pages - each with 10 rows - and your application has to respond to the right event when the user wants to navigate from one page to another (using the Next and Previous buttons, as specified in the screenshot below, or alternately the individual page numbers).
When the user clicks on one of the navigation buttons, then the PageIndexChanged event of the DataGrid is fired. You can specify that the DataGrid has to start at a specific page using the CurrentPageIndex property. Unfortunately, this property has to be set before the data is bound to the DataGrid, otherwise the grid already contains the data. So, we have to bind the data again, meaning that we have to de-activate the BdpDataAdapter component, assign the right value to the CommandText property again (ordering the table on the current selected column), then re-activate the BdpDataAdapter and finally bind the data in the DataGrid, as can be seen in the following source code:

  procedure TWebForm1.DataGrid1_PageIndexChanged(source: System.Object;
    e: System.Web.UI.WebControls.DataGridPageChangedEventArgs);
  begin
    DataGrid1.CurrentPageIndex := e.NewPageIndex; // go to new page
    bdpDataAdapter1.Active := False; // close
    bdpSelectCommand1.CommandText := 'SELECT EMP_NO, FIRST_NAME, LAST_NAME, '+
     'PHONE_EXT, HIRE_DATE, DEPT_NO, JOB_CODE, '+
     'JOB_GRADE, JOB_COUNTRY, SALARY FROM EMPLOYEE';
    if Session['OrderBy'] <> nil then
      bdpSelectCommand1.CommandText := bdpSelectCommand1.CommandText + ' ORDER BY ' +
        String(Session['OrderBy']);
    bdpDataAdapter1.Active := True; // re-open
    dataGrid1.DataBind // refresh data
  end;
You can now close the DataGrid Property Builder dialog again. At design-time, the DataGrid will now show hyperlinks in the column headers, and a Next and Previous button in the footer of the DataGrid.
The result of compiling and running the ASP.NET application from the Delphi 8 for .NET IDE can be seen below (when clicked on the second column to sort the DataGrid by the FIRST_NAME field).

We can now view the data, browse through the pages, and sort the columns of the DataGrid on any field of the table.
It's also possible to edit the contents of the asp:DataGrid, although that will require some additional code to write. I'd like to refer to my article on the IBM DB2 web portal entitled Using Delphi for .NET to work with DB2 database tables in ASP.NET Web pages for more code details. In order to avoid having to write this code, you can use the Borland DB Web Controls.

Borland DB Web Controls
In order to use the Borland DB Web Controls, open the DB Web category in the Tool Palette. First double-click on the DBWebDataSource component so it will also be placed in the non-visual area of the HTML Designer. Point the DataSource property of DBWebDataSource to dataSet1 - the .NET dataset that contains the results of the SELECT query on the EMPLOYEE table. The DBWeb Data Source component will act as a "gateway" between the visual DB Web controls and the .NET dataset.
The other DB Web controls include a DBWebCalendar, DBWebCheckBox, DBWebDropDownList, DBWebGrid, DBWebImage, DBWebLabel, DBWebLabeledTextBox, DBWebListBox, DBWebMemo, DBWebNavigator, DBWebRadioButtonList, and DBWebTextBox.

Let's start with the two most powerful controls: the DBWebGrid in combination with the DBWebNavigator. Double-click on these two controls in the Tool Palette to place them on the HTML Designer. Select the DBWebNavigator, point its DataSource property to DBWebDataSource1, and its TableName property to EMPLOYEE.
Now, select the DBWebGrid, point its DataSource property to DBWebDataSource1 as well, and its TableName property to EMPLOYEE. This should give you instant live data at design-time:

The DBWebDataGrid contains built-in support for paging, editing, deleting and updating. Although to be honest, for the last feature we need to write one line of code to actually send the update from the DBWebDataGrid via the BdpDataAdapter and the BdpConnection component to the actual underlying EMPLOYEE table from the InterBase database. That line of code needs to be written in the OnApplyChangeRequest event handler of the DBWebDataSource component, as follows:

  procedure TWebForm1.DBWebDataSource1_OnApplyChangesRequest(sender: System.Object;
    e: Borland.Data.Web.WebControlEventArgs);
  begin
    BdpDataAdapter1.AutoUpdate(dataSet1, 'EMPLOYEE', BdpUpdateMode.All)
  end;
You can now compile and run the application from the Delphi 8 for .NET IDE. Note that there are two ways to run the application: with or without the debugger, both available in the Run menu.

Deployment
To deploy the ASP.NET Web Form application, take a closer look at your project directory, which is actually the virtual directory itself. You need to deploy the assembly located in the bin subdirectory - that's WebForm.dll in our case - as well as the Default.aspx and login.aspx files and optionally the web.config and global.asax files. We also need to deploy the assembly that contains the DB Web controls; Borland.Data.Web.dll. And since we're using the Borland Data Provider, we also need to deploy Borland.Data.Provider.dll, Borland.Data.Common.dll, and the InterBase specific Borland.Data.InterBase.dll as well as the InterBase client gds32.dll (visible as Vendor client property in the Connections dialog). All these files can be placed in the same bin subdirectory of the virtual directory on the web server, or you can register the assemblies in the GAC (Global Assembly Cache) on the web server, but this usually requires manual assistance from your ISP.

Other Controls
As I've showed earlier, the DBWeb category of the Tool Palette contains more than just the DBWebGrid control. And not everybody wants to show more than one record at the same time. Especially when offering a data-entry screen to the endusers.
So, click on the DBWebGrid and remove it from the ASP.NET Web Form. Instead, add four DBWebTextBox controls, a DBWebLabel and a DBWebCalendar control. The following table should give a quick and clear overview of which component types you need to drop, in which order, how to call them, and where to connect their properties to.

    New ComponentPropertyValue
    DBWebLabelNameDBWLEMPNO
    DBDataSourceDBWebDataSource1
    TableNameEMPLOYEE
    ColumnNameEMP_NO
    DBWebTextBoxNameDBWTBFIRST_NAME
    DBDataSourceDBWebDataSource1
    TableNameEMPLOYEE
    ColumnNameFIRST_NAME
    DBWebTextBoxNameDBWTBPHONE_EXT
    DBDataSourceDBWebDataSource1
    TableNameEMPLOYEE
    ColumnNamePHONE_EXT
    DBWebCalendarNameDBWCHIRE_DATE
    DBDataSourceDBWebDataSource1
    TableNameEMPLOYEE
    ColumnNameHIRE_DATE
    DBWebTextBoxNameDBWTBJOB_GRADE
    DBDataSourceDBWebDataSource1
    TableNameEMPLOYEE
    ColumnNameJOB_GRADE
    DBWebTextBoxNameDBWTBSALARY
    DBDataSourceDBWebDataSource1
    TableNameEMPLOYEE
    ColumnNameSALARY
This is not a complete coverage of the EMPLOYEE table, but it will be enough to demonstrate some of the validation capabilities of ASP.NET. The DBWebLabel is read-only, so that control doesn't need validation. The DBWebCalendar control can only be used to select a date, so apart from the fact that you can still select a date that is logically wrong, you cannot enter a date that is syntactically wrong.
More interesting are the four DBWebTextBox controls can be associated with one of the ASP.NET Validation controls to perform input validation.

Input Validation
There are two different kinds of validation we can add to the ASP.NET project: server-side validation and client-side validation. If the enduser enters data in the DBWebTextbox controls and hits the next button on the navigator, then this is equal to clicking on the submit button. The data will be sent to the web server, and we can now perform server-side data validation on the data that the enduser just entered (or modified). If the data is invalid, we can respond in kind.
The alternative is that a little section of script code is executed when the enduser clicks on the navigator button. This scripting code can make a first attempt at checking the validity of the data that is entered in the browser. This is called client-side validation, and the obvious benefit is that is doesn't need the round-trip to the server in order to validate your data.

Validating using ASP.NET
Using ASP.NET, we can actually use validation server controls that generate client-side script. This means that the controls run at the server, but the script is executed at the client side in the browser.
ASP.NET contains six built-in validator controls, namely the RequiredFieldValidator, CompareValidator, RangeValidator, RegularExpressionValidator, CustomValidator and ValidationSummary. They can be found in the "Web Controls" category of the Tool Palette.

RequiredFieldValidator
Let's start with the RequiredFieldValidator. This control will enforce the fact that the enduser has to specify a value for the given field. To demonstrate its use, drag one from the Web Controls category on the ASP.NET web form, and place it right next to the DBWTBFIRST_NAME control to enforce the fact that the FIRST_NAME may not be left empty. Placing the RequiredFieldValidtor next to the DBWTBFIRST_NAME control is not enough, of course, you also have to point the ControlToValidate property of the RequiredFieldValidator to the DBWTBFIRST_NAME. Apart from that, you may want to change the ErrorMessage property, which by default only says "RequiredFieldValidator". At runtime, the validator is invisible, but as soon as the DBWTBFIRST_NAME is empty and the cursor leaves the edit control, then the validator is triggered and the error message shown in the browser. Apart from that, the JavaScript code will ensure that you cannot use the DBWebNavigator to move to another page without solving the validation problem.

CompareValidator
Another ASP.NET validation control is the CompareValidator. Where the RequiredFieldValidator could only verify whether or not a field is empty, the CompareValidator can compare the contents of a field against a value or data type. And especially the last one is very handy when it comes to web input, since you want to validate the currency or other numerical input fields before sending them to the server. The CompareValidator has two properties that you can use to point to the DBWebTextBox to validate: either ControlToCompare or ControlToValidate. The first one compares the value to a given value, the second one compares the content to a given type. We'll use the latter now. You can use the CompareValidator to validate the type of input for the PHONE_EXT field, by setting type to Integer (the phone number consists of numbers only), and the Operator property to DateTypeCheck.
Apart from using the CompareValidator to check if the input is of a specific type, we can also use the CompareValidator to compare a field value against a specified value, using the operator Equal, NotEqual, GreaterThan, GreaterThanEqual, LessThan, or LessThanEqual.

RangeValidator
So far, we've been able to check if a field is empty or not, and if a field contains a value that is within a specified type (or matches a specified value). Using the RangeValidator, we can take it one step further, and check if a field value is within a specified range. As an example, we can verify that the SALARY isn't negative or higher than a specified maximum value. Or that the JOB_GRADE is at least a certain value. To build these examples, drop a RangeValidator control next to the DBWTBJOB_GRADE control, set the ControlToValidate property to DBWTBJOB_GRADE, the type to Integer, the MinValue to 4, and the MaxValue to 12. Make sure to set the ErrorMessage property, for example to "Education level must be between 4 and 12".

RegularExpressionValidator
Another validator control - that I will not demonstrate here - is the RegularExpressionValidator. This validator control works just like the CompareValidator or the RangeValidator, but uses a regular expression to validate the value from the input field. This can be used for complex values like zip or postal codes.

ValidationSummary
The ValidationSummary control is a special validation control. Not really a validation control, in fact, but more a control that lists the summary of all validation violations in your page. We can place it on the web form, and it will automatically display all errors in a (bulleted) list, which can be useful at times.
The summary can be shown in the ValidationSummary control itself, or using a messagebox (or both), based on the value of the ShowSummary and ShowMessageBox properties of the ValidationSummary control.

Validation in Action
With all the validation controls in place, compile and run the application and clear some fields or enter invalid values to test the use of the validator controls:

You can click on the DBWebNavigator, but you will not be able to perform any action - navigator or apply data to the database - before the validation errors are solved. Only the undo buttons will work, reverting the input fields to their original record values again.

Summary
In this session I've demonstrated how to build ASP.NET Web Form applications using Borland Delphi 8 for .NET. I've shown that we can connect to databases like InterBase (or DB2, Oracle, MS Access or SQL Server/MSDE) with the Borland Data Provider, and can use Borland's new DB Web controls to build powerful user interfaces.
Combining the DB Web controls with the ASP.NET input validation controls enable us to build robust data entry pages as web applications.


Bob Swart (aka Dr.Bob - www.drbob42.com) is an independent consultant, trainer, author & webmaster for Bob Swart Training & Consultancy (eBob42) using Delphi (for .NET), C#Builder, Kylix, and C++Builder, and has spoken at Delphi and Borland Developer Conferences since 1993.
Bob is a trainer who has presented all over the world (USA, BeNeLux, Germany, Italy, Singapore and for UK-BUG in London, Manchester, Reading and Edinburgh). Bob has written his own training material, including the Delphi 8 for .NET Essentials and Delphi 8 for .NET ASP.NET courseware material licensed by Borland.
Bob is co-author of the Revolutionary Guide to Delphi 2, Delphi 4 Unleashed, C++Builder 4 Unleashed, C++Builder 5 Developer's Guide, Kylix Developer's Guide, Delphi 6 Developer's Guide, and the upcoming C++Builder 6 Developer's Guide, as well as author for a number of computer magazines, including The Delphi Magazine, Delphi Developer, UK-BUG Developer's Magazine, SDGN Magazine and Blaise, as well as the TechRepublic (CNET), DevX, Borland-IBM DB2 web portal and the Borland Developer Network website, and his own Dr.Bob's Delphi Clinic at http://www.drbob42.com.
Together with Marco Cantù, Bob received the Spirit of Delphi award in 1999 from Borland at BorCon.

Server Response from: SC4