Developing ASP components in delphi
Creating the beginning framework
First things first, you create the project, and then you start the project by creating a new Application. Creating a new Application is accomplished by accessing File - New Application from the Delphi menu. After creating the application, create a new ActiveX library by selecting File - New... from the menu, accessing the ActiveX tab and then clicking on the ActiveX Library, as shown in the following figure:
After creating the ActiveX library, you'll still need to create the ASP/MTS Component. Do this by selecting File - New... one more time, selecting the ActiveX tab and then selecting the Automation object. A dialog opens asking for the class name of the new component, the instancing support and the threading model. In the example the class name is MyTest, the default instancing support value of multiple instances is left unchanged, and the threading model is changed to Both. Event support code is not generated for this component, as shown in the following figure:
After the information is entered into the class definition dialog, Delphi then generates the framework necessary to begin the development of the new component.
Adding a method to the Interface
Once the component's class definition has been created, Delphi creates the associated interface for the class using a naming convention of "I" plus class name. In the case of our test example, the interface is named "IMyTest". The project also creates an associated type library for the interface, and opens the Type Library Editor in the Delphi project.
At this time, we'll create a method for the ASP component. In the Type Library Editor, right click on the interface, and then select New...Method from the context menu, as shown next.
This action will create a new method which we can then rename and define. By default the method contains no parameters, is named "Method1" and has a void return type. For the example component, we rename the method to "test", select the Parameters tab for the method dialog and pick HResult from the Return Type dropdown listbox.
To propogate the interface method definition to the interface's implementation class, the Refresh Implementation button on the Type Library Editor toolbar is pushed. This results in adding the function prototype (signature) to the implementation class.
Implementing the component method: Adding in support for ASP and MTS
Implementing the method for the new component is easy. All you have to do is find the class function and add the code you want. However, if you want to have the component use any ASP functionality, or MTS functionality for that matter, you'll have to add in support for ASP and MTS.
If you were using Delphi Enterprise, you would have access to an MTS component wizard and pre-defined MTS Pascal object types to use in your component. I would then recommend that you follow the Delphi documentation for creating an MTS component rather than following this next section. However, you can add in support for ASP using the type library regardless of which technique you use to add in MTS support.
Adding in support for ASP does require adding in support for MTS in order to access the pre-built ASP objects. Adding support to the project for both is accomplished quite simply just by selecting the Project - Import Type Library... option from the menu. What happens next is a dialog opens that lists all of the type libraries registered on the machine, including the ones for MTS and ASP. Clicking on a type library then accomplishes two things. First, Delphi wraps the type library with Pascal so that the library components can be accessed within the object. Secondly, the Pascal wrapped library is added to the component's uses section, making the library's component's available to the new component. The next figure shows the import dialog for the ASP object, labeled "Microsoft Active Server Pages Object Library".
Support for MTS is added in the same manner, but this time by selecting the type library labeled "Microsoft Transaction Server Type Library".
Adding in support for a type library in this manner adds in support for vtable binding from the component to the library. This type of support, also known as early binding is the most efficient method for accessing COM objects from an application or another COM object.
Now that the support for MTS and ASP are added, its time to implement the function. The Pascal implementation file for MyTest has, at this time, the following code:
unit MyTest;
interface
uses
ComObj, ActiveX, test_TLB,
MTxAS_TLB, ASPTypeLibrary_TLB;
type
TMyTest = class(TAutoObject, IMyTest)
protected
function test: HResult; safecall;
{ Protected declarations }
end;
implementation
uses ComServ;
function TMyTest.test: HResult;
begin
end;
initialization
TAutoObjectFactory.Create(ComServer, TMyTest, Class_MyTest,
ciMultiInstance, tmBoth);
end.
Before beginning to create the actual component code, a change is made to the Uses section of the Pascal file: Windows is added to the group. Without adding Windows, using pre-defined constant values such as S_OK will generate compiler errors:
unit MyTest;
interface
uses
ComObj, Windows, ActiveX, test_TLB,
MTxAS_TLB, ASPTypeLibrary_TLB;
For the example component, the test function writes a message to the Web page, the ubiquitous "Hello World" common for first programs of all languages, all tools. To write out content to a Web page from an ASP component, our component requires access to the Response built-in ASP object. The reason for this is that the Response object handles communication from the server to the client. To access the Response object, we also need to access the MTS object ObjectContext, as the ASP built-in objects are accessed through ObjectContext.
One thing unique about ObjectContext is that MTS creates an object of this type for each ASP component, and the component needs to access this already created component rather than creating one of its own.
In Visual Basic the ObjectContext object would be accessed by making a call to GetObjectContext. In Visual J++, we would call this method on a predefined class named "Mtx". However, trying either of these techniques with Delphi will generate an error when we compile the component. Instead, taking a look at the Pascal wrapper for the component we find that the GetObjectContext method on an interface named IMTxAS. So, we want to access this interface in order to get a reference to the ObjectContext object.
Further investigation of the Pascal wrapped MTS type library also shows that IMTxAS will be returned when the Create method of the implemented class CoAppServer is called. So, using this to get a reference to the IMTxAS interface, and then using the GetObjectContext method on this interface returns the reference to the ObjectContext object we need:
function TMyTest.test: HResult;
var
mtx: IMTxAS;
piContextObject: ObjectContext;
begin
mtx := CoAppServer.Create;
piContextObject := mtx.GetObjectContext;
Result := S_OK;
end;
A return value of S_OK signals a successful method call in COM.
Once we have access to the ObjectContext object, we can use this to access any of the ASP built-in objects. The built-in objects are accessed through using the Get_Item method on ObjectContext, which returns a value of type OleVariant. However, all COM automation objects inherit from the automation interface IDispatch, and we'll use this interface type, instead of OleVariant, to define the variable that receives the result of the Get_Item method call. The reason why we'll use IDispatch will be shown shortly.
For our example, we'll use IDispatch and QueryInterface to get access to the Response object. The Response object is accessed through the IResponse interface, so a variable of IResponse is added to the variable declaration section of the function, and the function at this time looks like the following:
function TMyTest.test: HResult;
var
mtx: IMTxAS;
piContextObject: ObjectContext;
piResponse: IResponse;
piIdisp: IDispatch;
begin
mtx := CoAppServer.Create;
piContextObject := mtx.GetObjectContext;
piIdisp := piContextObject.Get_Item('Response');
Result := S_OK;
end;
In order to access the IResponse interface as an IResponse interface, the IDispatch interface will need to be queried for IResponse. With C++ we would use something such as the QueryInterface standard COM method to query for and access the interface. However, with Pascal we can use the As operator. The As operator runs QueryInterface for us, and it is the dynamic binding provided by As that allows us to assign a value originally accessed from ObjectContext to a specific interface reference. Then, once we have the reference to the IResponse interface, we can invoke this interface's methods and call the Write method to write out our message.
The code for the function now looks as follows:
function TMyTest.test: HResult;
var
mtx: IMTxAS;
piContextObject: ObjectContext;
piResponse: IResponse;
piIdisp: IDispatch;
begin
mtx := CoAppServer.Create;
piContextObject := mtx.GetObjectContext;
piIdisp := piContextObject.Get_Item('Response');
piResponse := piIdisp As Response;
piResponse.Write('<H1>Hello World</H1>');
Result := S_OK;
end;
The component can now be compiled and can be accessed and the test method called from within a Web page:
<%
Dim myObject
set myObject = Server.CreateObject("test.MyTest")
myObject.test
%>
Viola! We have our first pass Delphi component. Of course, we're on a roll now and can't stop there. Time to up the ante and try something a little tougher, such as accessing an ASP built-in object collection. Sends shivers up and down your spine, doesn't it?
A gentleman from Inprise who works on the Delphi IDE (an excellent IDE BTW -- one of the more intuitively clear IDEs to work with) sent the following information I wanted to pass on to you:
When using the safecall calling convention (which is turned on by
default for the TLB Editor), return values of HRESULT and COM exception
handling (ISupportErrorInfo, etc) are taken care of for you. You may
run into problems with the code that you have generated if you are
expecting to get the HRESULT return value that you have set...
for instance:
function Hello(value: WideString): HRESULT; safecall;
would translate into:
function Hello([in] value: BSTR; [out, retval] returnval: HRESULT):
HRESULT; stdcall;
In essence, the safecall mapping takes the return type that you have
stated and makes it an invisible out param at the end of the function.
Also, if a function is declared as safecall and it raises an exception,
it automatically returns a failure code and populates an IErrorInfo
interface for the caller. If a Delphi application calls a function that
is declared as safecall, it also automatically checks for valid HRESULT
return types (no need to do SUCCEEDED or FAILED in most cases for
instance).
Sparky, thanks very much for this information and clarification.
ASP Collection Enumeration
Several of the ASP built-in objects have collections associated with the specific object. Examples are the Server Variables collection associated with the Request object, or the StaticObjects and Contents collections associated with both the Application and the Session objects.
Collection objects can be accessed individually, usually be name, but they are also accessed through enumeration. A collection that can be enumerated is one in which certain methods have been defined to do things such as get the number of entries of the collection, check to see if the collection is at the last member, and get the next member when accessing the collection sequentially.
To demonstrate, we'll add to the MTS/ASP test component and extend the test method to enumerate through the Request object ServerVariables collection, printing out each member's name and value.
We'll have to get access to the Request object interface and then we'll have to access the ServerVariables collection. This collection is actually accessible by an ASP helper object, the IRequestDictionary object. Adding in support for these two objects modifies the test component as follows:
function TMyTest.test: HResult;
var
mtx: IMTxAS;
piContextObject: ObjectContext;
piResponse: IResponse;
piIdisp: IDispatch;
piRequest: IRequest;
piReqDict: IRequestDictionary;
begin
mtx := CoAppServer.Create;
piContextObject := mtx.GetObjectContext;
piIdisp := piContextObject.Get_Item('Response');
piResponse := piIdisp As Response;
piResponse.Write('<H1>Hello World</H1>');
// get the Request object
piIdisp := m_piContextObject.Get_item('Request');
piRequest := piIdisp As Request;
// get ServerVariables and enum for variables
piReqDict := piRequest.Get_ServerVariables;
Result := S_OK;
end;
A helper object such as IRequestDictionary is an object provided by ASP for working with specific collections. There are other helper objects for working with other types of collections.
After getting the IRequestDictionary interface reference, we need to access the enumerated collection from the interface. The enumerated collection is another COM object, IEnumVARIANT.
The IEnumVariant object is accessed by using the Get__NewEnum method on the IRequestDictionary collection, and assigning the value to an IUnknown interface variable. The short cut method to query for the IEnumVARIANT interface, As, is then used to access the enumerated collection:
// get ServerVariables and enum for variables
piReqDict := piRequest.Get_ServerVariables;
piIUnknown := piReqDict.Get__NewEnum;
piIEnum := piIUnknown As IEnumVARIANT;
Once we have the enumerated collection, all we need to do is traverse the collection sequentially, accessing the collection element name with the Next method, and then using this to access the collection element's actual value by using Get_Item from the IRequestDictionary interface. The code for the complete component's test method is shown below:
function TMyTest.test: HResult;
var
mtx: IMTxAS;
piContextObject: ObjectContext;
piResponse: IResponse;
piIdisp: IDispatch;
piRequest: IRequest;
piReqDict: IRequestDictionary;
piIEnum: IEnumVARIANT;
piIUnknown: IUnknown;
liReturn: Longint;
ovName: OleVariant;
ovValue: OleVariant;
begin
mtx := CoAppServer.Create;
piContextObject := mtx.GetObjectContext;
piIdisp := piContextObject.Get_Item('Response');
piResponse := piIdisp As Response;
piResponse.Write('<H1>Hello World</H1>');
// get the Request object
piIdisp := m_piContextObject.Get_item('Request');
piRequest := piIdisp As Request;
// get ServerVariables and enum for variables
piReqDict := piRequest.Get_ServerVariables;
piIUnknown := piReqDict.Get__NewEnum;
piIEnum := piIUnknown As IEnumVARIANT;
// while S_OK get name and value, print
while piIEnum.Next(1, ovName, Addr(liReturn)) = S_OK do
begin;
m_piResponse.Write(ovName);
m_piResponse.Write(' = ');
ovValue := piReqDict.Get_Item(ovName);
m_piResponse.Write(ovValue);
m_piResponse.Write('<p>');
end;
test := 0;
end;
Running the test page and calling the test method now results in the display of "Hello World", but also in a listing of all the Request object's ServerVariables collection.
Well, the test Delphi component we've built has successfully accessed the MTS ObjectContext object, and successfully accessed ASP built-in objects. It has also successfully used the ASP built-in objects, including accessing a built-in object collection. There is one thing the test method has not done, and that's demonstrate how method parameters are handled between a component written in Delphi, and an ASP test page, a page usually written in VBScript. This is demonstrated in the next section.
Function Arguments
Much of the communication between a component and the ASP page and application will occur through the ASP built-in objects...but not all of it. For other communication we need to provide data and receive data through function parameters.
ASP components can support in parameters (parameters passed by value), out parameters ( parameters passed by reference and whose value is set within the component), and in and out parameters (parameters passed by reference, set in the ASP page and modified in the component). ASP methods can also return values.
To demonstrate parameter passing with Delphi ASP components, another method is added to the sample component, is then named "MethodWithParm", and three parameters are added: vtOut as OleVariant and defined with the var modifer; bstrIn as WideString and defined with the const modifer, and bstrOut as OleVariant, defined with the out modifier. These three parameters represent the [in,out], [in], and [out] parameter possibilities. The method is also defined as a function, with a return type of Hresult, which represents the last and final COM parameter type of [out,retval]. The method prototype is:
function MethodWithParm(var vtOut: OleVariant; const bstrIn: WideString;
out bstrOut: OleVariant): HResult; safecall;
The notation of "[in,out]" and "[out,retval]" is from Interface Definition Language, IDL, the programming language neutral language used to define interfaces. You can see this IDL directly by selecting the button to generate IDL from the Type Library Editor.
All the method does is take the value represented in the in/out parameter, vtOut, and add HTML header formatting to the value. The method also takes the input parameter, bstrOut, and assigns this to the output only parameter, bstrOut:
function TMyTest.MethodWithParm(var vtOut: OleVariant;
const bstrIn: WideString; out bstrOut: OleVariant): HResult;
var
mtx: IMTxAS;
piContextObject: ObjectContext;
piResponse: IResponse;
piIdisp: IDispatch;
tst: WideString;
begin
mtx := CoAppServer.Create;
piContextObject := mtx.GetObjectContext;
piIdisp := piContextObject.Get_Item('Response');
piResponse := piIdisp As Response;
tst := vtOut;
vtOut := '<h1>' + tst + 'World!</H1>';
bstrOut := bstrIn;
Result := S_OK;
end;
Accessing the new method using ASP VBScript as follows results in two headers being output to the returned Web page:
Dim strInOut
Dim strIn
Dim strOut
strIn = "<H1>Flame On!</H1>"
strInOut = "Yo!"
myObject.MethodWithParm strInOut, strIn, strOut
Response.Write strInOut
Response.Write strOut
Okay, so let's recap. We've demonstrated creating an ASP component, accessing the MTS object and the ASP built-in objects, and now we've demonstrated the different method parameter passing techniques. However, there is one missing piece to the component. Notice how the code that accesses the ObjectContext and IResponse interface repeats with both component methods? This doesn't seem very efficient. A better approach would be to assign these values to component data members and use these members in all of the component methods. Then, another method could be created specifically to initialize the values.
For component data member initialization, we could create a new public method that performs the initialization and call this method directly from the ASP client, but this also doesn't seem efficient. What happens if the ASP developer using the component forgets to call this method?
A preferred technique would be to initialize the values when the component is activated, without any intervention from the component client code. It just so happens that MTS provides us with the component activation methods through the ObjectControl interface, discussed in the last section of this tutorial.
ObjectControl and Just-in-Time Activation
MTS does more than handle transactions and provide access to the ASP built-in objects: it also support just-in-time activation. What is just-in-time activation? This type of component activation means that MTS, not the client, controls a components activation and deactivation. The client code proceeds as usual with the assumption it is holding on to an active component reference, when the client is really holding on to a reference created by MTS that represents the component. In actuality, MTS may have deactivated the component, and the component and its resources may already be unloaded from memory. When the client accesses the component again, MTS activates the component and the component reloads the appropriate resources.
MTS determines when to mark a component for deactivation based on statuses set by the component -- that it, the component, is finished with its work and can be unloaded from memory. The component does this by using ObjectContent method calls of SetComplete and SetAbort, not demonstrated in this tutorial.
To provide further support for just-in-time activation, MTS also provides support for ObjectControl which contains three methods: Activate, Deactivate, and CanBePooled. The Activate method is called when the component is activated, the Deactivate method is called when the component is deactivated, and the CanBePooled method provides information to MTS about whether the component supports Object Pooling. As Object Pooling is not currently supported by MTS, and is more of an MTS rather than an ASP issue, this method won't be discussed further. What is of interest to us now is the Activate and Deactivate methods. Particulary the Activate method, which is an ideal location for our data member initialization.
First, to add in component interactive support for just-in-time activation, the component must implement ObjectControl. This occurs easily in Delphi by adding the ObjectControl interface to the class type definition:
type
TMyTest = class(TAutoObject, IMyTest, ObjectControl)
One of the real key implementation factors for COM objects is that one implementation class can implement more than one interface, in this case MyClass is the implementation class or coClass for the IMyTest and the ObjectControl interfaces, as well as the Delphi automation interface template TAutoObject.
After adding ObjectControl to the class definition, if we were to compile the component at this time we would receive an error about the three methods -- Activate, Deactivate, and CanBePooled -- not being implemented. These methods are defined as pure virtual methods, which means they must be implemented within the component.
The three methods don't need to be added to the IMyTest interface, as they are already defined as methods to the ObjectControl interface in the MTS type library wrapper code. However, you will need to add the method prototypes to the Delphi component header section. The prototypes for the three methods are:
function Activate: HResult; stdcall;
function Deactivate: HResult; stdcall;
function CanBePooled: WordBool; stdcall;
If you were to access the MTxAS_TLB file, you will see that these prototypes are identical to the ones shown with ObjectControl in this file.
Once the prototypes are added the methods are implemented. The CanBePooled function returns a value of "false":
function TMyTest.Deactivate: HResult;
begin
Deactivate := S_OK;
end;
The Activate method is used to implement the new component data members. These members are added manually (they are not exposed as properties) to the component header section as private members:
private
m_piContextObject: ObjectContext;
m_piResponse : IResponse;
The members are then initialized within Activate:
function TMyTest.Activate: HResult;
var
mtx: IMTxAS;
piIdisp: IDispatch;
begin
// first, access context object
mtx := CoAppServer.Create;
m_piContextObject := mtx.GetObjectContext;
// then, get Response object and say hello
piIdisp := m_piContextObject.Get_item('Response');
m_piResponse := piIdisp As Response;
m_piResponse.Write('<H1>Hello World, from Activate</H1>');
Activate := S_OK;
end;
In addition to creating the component data members, the Activate function also says hello to the world. This will be useful in demonstrating how the Active method is called before the component's exposed method is invoked, though normally the Activate method works "silently".
The references to the ObjectContext and Response interfaces are released within the Deactivate method:
function TMyTest.Deactivate: HResult;
begin
m_piResponse._Release;
m_piContextObject._Release;
Deactivate := S_OK;
end;
Once the data members are initialized, these can be used in all of the methods. All of this is shown in the final code listing of this tutorial, which shows the entire component code for MyTest:
unit MyTest;
interface
uses
ComObj, Windows, ActiveX, test_TLB,
MTxAS_TLB, ASPTypeLibrary_TLB;
type
TMyTest = class(TAutoObject, IMyTest, ObjectControl)
public
function test: HResult; safecall;
function MethodWithParm(var vtOut: OleVariant; const bstrIn: WideString;
out bstrOut: OleVariant): HResult; safecall;
function Activate: HResult; stdcall;
function Deactivate: HResult; stdcall;
function CanBePooled: WordBool; stdcall;
private
m_piContextObject: ObjectContext;
m_piResponse : IResponse;
end;
implementation
uses ComServ;
// test code to say hello to world and
// then enumerate through ServerVariables, printing
// out values
function TMyTest.test: HResult;
var
piIdisp: IDispatch;
piRequest: IRequest;
piReqDict: IRequestDictionary;
piIEnum: IEnumVARIANT;
piIUnknown: IUnknown;
liReturn: Longint;
ovName: OleVariant;
ovValue: OleVariant;
begin
m_piResponse.Write('<H1>Hello World</H1>');
// get the Request object
piIdisp := m_piContextObject.Get_item('Request');
piRequest := piIdisp As Request;
// get ServerVariables and enum for variables
piReqDict := piRequest.Get_ServerVariables;
piIUnknown := piReqDict.Get__NewEnum;
piIEnum := piIUnknown As IEnumVARIANT;
// while 0 (S_OK) get name and value, print
while piIEnum.Next(1, ovName, Addr(liReturn)) = 0 do
begin;
m_piResponse.Write(ovName);
m_piResponse.Write(' = ');
ovValue := piReqDict.Get_Item(ovName);
m_piResponse.Write(ovValue);
m_piResponse.Write('<p>');
end;
Result := S_OK;
end;
// method to demonstrate different parameter
// passing types
function TMyTest.MethodWithParm(var vtOut: OleVariant;
const bstrIn: WideString; out bstrOut: OleVariant): HResult;
var
tst: WideString;
begin
tst := vtOut;
vtOut := '<h1>' + tst + 'World!</H1>';
bstrOut := bstrIn;
MethodWithParm := 0;
end;
// ObjectControl Activate method,
// called when MTS loads component into memory
function TMyTest.Activate: HResult;
var
mtx: IMTxAS;
piIdisp: IDispatch;
begin
// first, access context object
mtx := CoAppServer.Create;
m_piContextObject := mtx.GetObjectContext;
// then, get Response object and say hello
piIdisp := m_piContextObject.Get_item('Response');
m_piResponse := piIdisp As Response;
m_piResponse.Write('<H1>Hello World, from Activate</H1>');
Activate := S_OK;
end;
// ObjectControl Deactivate, called when component is
// unloaded
function TMyTest.Deactivate: HResult;
begin
m_piResponse._Release;
m_piContextObject._Release;
Deactivate := S_OK;
end;
// Object Control CanBePooled, to let MTS know
// if object participates in Object Pooling
function TMyTest.CanBePooled: WordBool;
begin
CanBePooled := false;
end;
initialization
TAutoObjectFactory.Create(ComServer, TMyTest, Class_MyTest,
ciMultiInstance, tmBoth);
end.
Once a component is successfully built, it can be registered, either using Delphi registration, using regsvr32 or by being registered by inclusion into an MTS package.
You don't have to register ASP components with MTS, even when using ObjectContext. However, I would strongly recommend that you register the component with MTS. If you are using just-in-time activation, such as using the Activate and Deactivate methods, register your component with MTS.
Now, when the component is created and the first method is called on the component, such as test, MTS invokes the Activate method first, in the background, before the implicit method call is processed. For instance, the following ASP file will return a page with a header created in the Activate method, the header created in test, and ServerVariables listing, and then the headers resulting from the call to MethodWithParm:
<HTML>
<HEAD>
<BODY>
<%
Dim myObject
set myObject = Server.CreateObject("test.MyTest")
myObject.test
Dim strInOut
Dim strIn
Dim strOut
strIn = "<H1>Flame On!</H1>"
strInOut = "Yo!"
myObject.MethodWithParm strInOut, strIn, strOut
Response.Write strInOut
Response.Write strOut
%>
</BODY>
</HTML>
Summary
In this tutorial we covered creating an ASP component that directly accessed the MTS and ASP objects. Additionally, we covered the different parameter passing types that can be used between the component and the ASP page, and just-in-time activation. I'd say that's more than enough to at least get you all started, or into trouble, whatever the case may be.