Inside: Bonus Content
Inside: Bonus Content
Inside: Bonus Content
Delphi
Informant
Magazine
Bonus
Content
INSIDE
BONUS CONTENT
Active Delphi
In the last two installments of this column we examined only the surface
of a very simple ASP.NET application in Delphi 8. We saw what an ASP.NET
application looked like from the developers perspective that is, what
the application looked like on the surface. As Nick Hodges tells us,
there is more than cosmetics to even an empty, single-page ASP.NET
application. This series wrap-up takes a look at the global.asax/pas file
and the web.config file.
A C T I V E
ASP.NET
D E L P H I
DELPHI 8
By Nick Hodges
ASP.NET
Part III: global.asax/pas & web.config
Active
Delphi
ASP.NET
Event
Description
Application_Start
Session_Start
This event fires whenever a new, unrecognized visitor comes to your site. An unrecognized visitor is one
that doesnt have a valid session cookie. Every time a
new visitor makes a request, a new session is started,
and this event is fired. Use it to write code you want to
occur each time a new user shows up.
Application_BeginRequest
This event is fired at the very beginning of each individual HTTP request. You can use this code to do
any initialization that you need to do to get ready to
respond to the request.
Application_EndRequest
Application_AuthenticateRequest
Application_Error
Session_End
Application_End
Active
Delphi
ASP.NET
Property
Description
Tag Name
Description
Application
This points to the Application object, which contains information available to all users throughout
the application. In other words, the information
held here is global, in that all users see the same
information.
<compilation>
Context
Modules
Request
<customErrors>
Response
Server
Session
<authentication>
User
This tag determines how users are authenticated within your application. It can only
be defined in application-level or higher
config files. You tell your application to
use Windows authentication, Forms-based
authentication, Passport authentication, or
no authentication.
<trace>
<sessionState>
<globalization>
mation that the framework will look for and use. In addition, you can add your own data into the file.
A web.config file is also specific not only to the application
as a whole, but it can be used to provide specific configuration information to specific portions of an application.
For instance, all ASP.NET applications require a web.config
file to reside in the root directory of the application. You
can also place a web.config file in a directory below your
root directory, and provide customized configuration information for that directory and all sub-directories below it.
web.config files in a subdirectory can both override and
augment the web.config files in parent directories. Note
that this inheritance hierarchy is not based on the physical
directories on your hard-drive, but on the virtual directory
structure defined by your application and the Web server.
For example, you may have an administrator function in
your application you want to be accessible only to properly
authenticated users. To make that subdirectory and all directories below it available only to authenticated users, you can
add a specialized web.config file in that directory with the
following entry in it:
<system.Web>
<authorization>
<deny users="?"/>
</authorization>
</system.Web>
Active
Delphi
ASP.NET
C O L U M N S
ADO.NET
&
R O W S
By Bill Todd
Columns
&
Rows
procedure CountryForm.OpenCountry;
var
CountryPrimaryKey: array of DataColumn;
CountryDataView: DataView;
begin
if (EmployeeConn.State = ConnectionState.Closed) then
EmployeeConn.Open;
// Set name of Table that fill will create to Country.
if (not CountryAdapter.TableMappings.Contains('Table1')) then
CountryAdapter.TableMappings.Add('Table1', 'Country');
CountryAdapter.Fill(EmpDataSet, 'Country');
// Create dynamic array of DataColumn objects, add Country
// column to the array, and set the PrimaryKey property of
// the DataTable.
SetLength(CountryPrimaryKey, 1);
CountryPrimaryKey[0] :=
EmpDataSet.Tables['Country'].Columns['Country'];
EmpDataSet.Tables['Country'].PrimaryKey := CountryPrimaryKey;
// Bind the DataGrid to the DataTable.
CountryDataView := EmpDataSet.Tables['Country'].DefaultView;
CountryGrid.DataSource := CountryDataView;
CountryDataView.Sort := 'Country';
end;
Columns
&
Rows
COUNTRY
COUNTRY = ?, CURRENCY = ?
COUNTRY = ?
CURRENCY = ?
COUNTRY
COUNTRY = 'USA', CURRENCY = 'Dollar'
COUNTRY = 'USA'
CURRENCY = NULL
COUNTRY
COUNTRY = 'USA', CURRENCY = 'Dollar'
COUNTRY = 'USA'
CURRENCY IS NULL
COUNTRY
COUNTRY = ?, CURRENCY = ?
COUNTRY = ?
(CURRENCY = ? OR (? IS NULL AND CURRENCY IS NULL))
If you are working with a table that has columns that allow
nulls you will have to change the WHERE clause of the
UPDATE and DELETE statements generated by the Generate
SQL button manually to allow for a null original value.
8
Columns
&
Rows
procedure CountryForm.UpdateDatabaseItem_Click(
sender: System.Object; e: System.EventArgs);
var
CountryGridCurrencyMgr: CurrencyManager;
ColStyle: DataGridColumnStyle;
ColNumber: Integer;
begin
// Call DataGrid.EndEdit to post changes to current field.
ColNumber := CountryGrid.CurrentCell.ColumnNumber;
ColStyle :=
CountryGrid.TableStyles[0].GridColumnStyles[ColNumber];
CountryGrid.EndEdit(ColStyle, CountryGrid.CurrentCell.RowNumber, False);
// Call CurrencyManaager.EndEdit to post changes to current row.
CountryGridCurrencyMgr := CountryGrid.BindingContext[
EmpDataSet.Tables['Country'].DefaultView] as CurrencyManager;
CountryGridCurrencyMgr.EndCurrentEdit;
// If there are pending changes, update the database.
if (EmpDataSet.Tables['Country'].GetChanges.Rows.Count > 0) then
CountryAdapter.Update(EmpDataSet, 'Country');
end;
procedure CountryForm.UpdateDatabaseItem_Click(
sender: System.Object; e: System.EventArgs);
var
CountryGridCurrencyMgr: CurrencyManager;
ColStyle: DataGridColumnStyle;
ColNumber: Integer;
CurrentCountry: String;
begin
// Call DataGrid.EndEdit to post changes to the current field.
ColNumber := CountryGrid.CurrentCell.ColumnNumber;
ColStyle := CountryGrid.TableStyles[0].GridColumnStyles[ColNumber];
CountryGrid.EndEdit(
ColStyle, CountryGrid.CurrentCell.RowNumber, False);
// Call CurrencyManaager.EndEdit to post changes to the current row.
CountryGridCurrencyMgr := CountryGrid.BindingContext[
EmpDataSet.Tables['Country'].DefaultView] as CurrencyManager;
CountryGridCurrencyMgr.EndCurrentEdit;
// If there are pending changes, update the database.
if (EmpDataSet.Tables['Country'].GetChanges.Rows.Count > 0) then
CountryAdapter.Update(EmpDataSet, 'Country');
// Reread the data to see changes made by other users.
RowNumber := CountryGridCurrencyMgr.Position;
CurrentCountry := EmpDataSet.Tables[
'Country'].DefaultView[RowNumber]['Country'].ToString;
EmpDataSet.Tables['Country'].Clear;
CountryAdapter.Fill(EmpDataSet, 'Country');
CountryGridCurrencyMgr.Position :=
EmpDataSet.Tables['Country'].DefaultView.Find(CurrentCountry);
end;
ClientDataSet.Post;
ClientDataSet.ApplyUpdates(0);
Columns
&
Rows
procedure CountryForm.ViewInsertItem_Click(
sender: System.Object; e: System.EventArgs);
begin
MessageBox.Show(
CountryCmdBldr.GetInsertCommand.CommandText,
'Insert Statement');
end;
Why use the CommandBuilder when the IDE will generate the INSERT, UPDATE, and DELETE statements for Figure 10: The GetTransaction method.
you at design time? The only reason is when you dont
know the SELECT statement at design time. If your appliBut what about cases where you need to call Fill or Update
cation lets the user generate a SELECT statement and the
for more than one DataAdapter in the same transaction?
fields in the SELECT clause can vary, your application must
For example, how do you update both a master and a detail
generate matching INSERT, UPDATE, and DELETE statetable in a single transaction so that the updates to both
ments after the SELECT statement is known.
tables succeed or fail together?
The disadvantage of using the CommandBuilder is performance. The CommandBuilder has to query the database to
get the metadata it needs to construct the SQL statements.
The metadata query takes time and network bandwidth and
increases the load on the database server.
Using AutoUpdate
If you really want to take the easy way out, and
you are using a BdpDataAdapter, you can call the
BdpDataAdapter.AutoUpdate method. The Update803 sample application uses this technique, as shown in Figure
9. The Update Database Click event handler in this example
is identical to the one in Figure 7, except for the code
shown in Figure 9.
When you call AutoUpdate it creates a BdpCommandBuilder
and uses it to generate the INSERT, UPDATE, and DELETE
statements. Notice that the parameters passed to AutoUpdate
are identical to those passed to Update, with the addition of
a parameter to specify the update mode. The two modes are
BdpUpdateMode.All and BdpUpdateMode.Key.
Using Transactions
Because you cannot access data in an InterBase database in
any way outside the context of a transaction, it is obvious
that the BDP InterBase driver starts and commits transactions each time you call the BdpDataAdapters Fill or Update
methods. That makes transaction control easy and automatic
when each call to Fill or Update can use its own transaction.
10
Columns
&
Rows
C O L U M N S
ADO.NET
&
R O W S
By Bill Todd
Columns
&
Rows
tion uses the COMPANY table to show how to get the values for COMPANY_ID and COMPANY_VERSION that are
assigned by the triggers. Figure 5 shows the applications
form at design time. The tray contains a BdpConnection,
a BdpDataAdapter, DataSet, and MainMenu component.
The BdpConnection is connected to the Contact database
and the DataAdapter selects all rows and columns from
the COMPANY table. The form contains a ToolBar and
DataGrid. The DataGrid is connected to the Company
DataTable in the ContactDataSet DataSet object.
The solution is easy: Add a field to the table that is updated by a trigger using a generator each time the record is
updated. All you have to do is include that field in the
WHERE clause of the UPDATE statement, and you will
accurately detect changes made by another user to any
field including a blob. Although the COMPANY table does
not contain any blob fields, I have included a column
named COMPANY_VERSION to demonstrate this technique.
Using Stored Procedures
If you use stored procedures to insert and update rows, you
need triggers to assign the next value to the primary key and
version columns. Figure 3 shows the before insert trigger that
sets the value of the COMPANY_ID and COMPANY_VERSION
fields. The COMPANY_ID field is assigned a value from the
COMPANY_ID_GEN generator, but only if the COMPANY_ID
column is null or negative in the new row. This gives you the
option to get the next value from the generator and assign it
to the COMPANY_ID column in a client application, as you
will see later in this article.
Figure 4 shows the before update trigger for the COMPANY
table. This trigger generates a new value for the
COMPANY_VERSION column each time the table is updated.
The sample application is called GetValues and it comes in
two versions. The one in the GetValuesSP folder demonstrates using stored procedures; the one in GetValuesCMD
shows how to get the value of server-generated fields
using Command objects. The GetValues sample applicaCREATE TRIGGER COMPANY_ID FOR COMPANY
ACTIVE BEFORE INSERT POSITION 10 AS BEGIN
IF ((NEW.COMPANY_ID IS NULL) OR (NEW.COMPANY_ID < 0)) THEN
NEW.COMPANY_ID = GEN_ID(COMPANY_ID_GEN, 1);
NEW.COMPANY_VERSION = GEN_ID(COMPANY_VERSION_GEN, 1);
END;
13
Columns
&
Rows
UPDATE COMPANY
SET COMPANY_NAME = :COMPANY_NAME,
COMPANY_ADDRESS = :COMPANY_ADDRESS,
COMPANY_CITY = :COMPANY_CITY,
COMPANY_STATE = :COMPANY_STATE,
COMPANY_ZIP = :COMPANY_ZIP,
COMPANY_VERSION = :COMPANY_VERSION_NEW
WHERE COMPANY_ID = :COMPANY_ID
AND COMPANY_VERSION = :COMPANY_VERSION;
END;
Columns
&
Rows
procedure TCompanyForm.UpdateDatabase;
var
Trans: BdpTransaction;
CompanyGridCurrencyMgr: CurrencyManager;
begin
// Post the current record so it will be included in the updates.
CompanyGrid.EndEdit(CompanyGrid.TableStyles[0].GridColumnStyles[
CompanyGrid.CurrentCell.ColumnNumber],
CompanyGrid.CurrentCell.RowNumber, False);
CompanyGridCurrencyMgr := CompanyGrid.BindingContext[
ContactDataSet.Tables['Company'].DefaultView] as
CurrencyManager;
CompanyGridCurrencyMgr.EndCurrentEdit;
// If there are pending changes, update the database.
if (ContactDataSet.Tables['Company'].GetChanges <> nil) then
begin
Trans := GetTransaction;
try
CompanyAdapter.Update(
CompanyTable.Select('', '', DataViewRowState.Added));
CompanyAdapter.Update(
CompanyTable.Select('', '', DataViewRowState.ModifiedCurrent));
CompanyAdapter.Update(
CompanyTable.Select('', '', DataViewRowState.Deleted));
Trans.Commit;
except
on E: DBConcurrencyException do begin
// If this is the BDP update stored procedure bug,
// eat the error, otherwise display it.
if (E.Message = 'Concurrency violation: the UpdateCommand affected 0 records.') then
begin
Trans.Commit;
if (E.Row.HasErrors) then
E.Row.ClearErrors;
end
else
begin
Trans.Rollback;
MessageBox.Show(E.Message, 'Concurrency Error');
end;
end;
on E: Exception do begin
Trans.Rollback;
MessageBox.Show(E.Message, 'Update Error');
end;
end;
end;
end;
To understand why you need to remember that the normal way to see if the record was updated by another user
is to include the original value of the fields in the WHERE
clause of the UPDATE statement. The BDP executes
each update statement then checks the number of rows
affected by the update. If no rows were affected the BDP
assumes that the row was not found because another
15
Columns
&
Rows
There are only two changes in the code for this version of
the sample program. First, the:
on E: DbConcurrencyException
SELECT ASEQUENCE.NEXTVALUE
FROM DUAL
Columns
&
Rows
procedure TCompanyForm.CompanyAdapter_RowUpdating(
sender: System.Object;
e: Borland.Data.Provider.BdpRowUpdatingEventArgs);
var
Rdr: BdpDataReader;
begin
if (e.StatementType = StatementType.Insert) then
begin
Rdr := CompanyIdVersionCmd.ExecuteReader;
Rdr.Read;
e.Row['COMPANY_ID'] := Rdr['COMPANY_ID'];
e.Row['COMPANY_VERSION'] := Rdr['COMPANY_VERSION'];
end
else if (e.StatementType = StatementType.Update) then
begin
E.Row['COMPANY_VERSION'] := CompanyVersionCmd.ExecuteScalar;
end;
end;
17
not true in all cases. Consider a SQL Server timestamp field. If you want a field that changes each
time any column in the row changes a timestamp
is perfect. However, there is no function call to get
the last timestamp value. With a stored procedure
it is easy to return the new value. Using an event
handler there is no way to get the new value except
re-reading the row. The same applies to any value
assigned by a trigger or constraint. To fully support
the ADO.NET standard Borland needs to provide
stored procedure support for DataAdapters as soon
as possible.
Conclusion
If you are not using the BDP returning values assigned
on the server is easy. Just use stored procedures
to insert and update rows and return the server
assigned values in output parameters. With the BDP
the best alternative is to use the RowUpdating or
RowUpdated event and call a stored procedure that gets
the server assigned value. If you need to control the order
in which inserts, updates, and deletes are applied use the
DataTable.Select method to return a DataTable that contains
just the rows with the type of update you specify. This
makes it easy to control the update order among related
tables.
The example projects referenced in this article are available for
download on the Delphi Informant Magazine Complete Works
CD located in INFORM\2004\SEP\DI200409TB.