Delphi Programming Guide
Delphi Programmer 

Menu  Table of contents

Part I - Foundations
  Chapter 1 – Delphi 7 and Its IDE
  Chapter 2 – The Delphi Programming Language
  Chapter 3 – The Run-Time Library
  Chapter 4 – Core Library classes
  Chapter 5 – Visual Controls
  Chapter 6 – Building the User Interface
  Chapter 7 – Working with Forms
Part II - Delphi Object-Oriented Architectures
  Chapter 8 – The Architecture of Delphi Applications
  Chapter 9 – Writing Delphi Components
  Chapter 10 – Libraries and Packages
  Chapter 11 – Modeling and OOP Programming (with ModelMaker)
  Chapter 12 – From COM to COM+
Part III - Delphi Database-Oriented Architectures
  Chapter 13 – Delphi's Database Architecture
  Chapter 14 – Client/Server with dbExpress
  Chapter 15 – Working with ADO
  Chapter 16 – Multitier DataSnap Applications
  Chapter 17 – Writing Database Components
  Chapter 18 – Reporting with Rave
Part IV - Delphi, the Internet, and a .NET Preview
  Chapter 19 – Internet Programming: Sockets and Indy
  Chapter 20 – Web Programming with WebBroker and WebSnap
  Chapter 21 – Web Programming with IntraWeb
  Chapter 22 – Using XML Technologies
  Chapter 23 – Web Services and SOAP
  Chapter 24 – The Microsoft .NET Architecture from the Delphi Perspective
  Chapter 25 – Delphi for .NET Preview: The Language and the RTL
       
  Appendix A – Extra Delphi Tools by the Author
  Appendix B – Extra Delphi Tools from Other Sources
  Appendix C – Free Companion Books on Delphi
       
  Index    
  List of Figures    
  List of tables    
  List of Listings    
  List of Sidebars  

 
Previous Section Next Section

Building Web Services

If calling a web service in Delphi is straightforward, the same can be said of developing a service. If you go into the Web Services page of the New Items dialog box, you can see the SOAP Server Application option. Select it, and Delphi presents you with a list that's quite similar to what you see if you select a WebBroker application. A web service is typically hosted by a web server using one of the available web server extension technologies (CGI, ISAPI, Apache modules, and so on) or the Web App Debugger for your initial tests.

After completing this step, Delphi adds three components to the resulting web module, which is just a plain web module with no special additions:

  • The HTTPSoapDispatcher component receives the web request, as any other HTTP dispatcher does.

  • The HTTPSoapPascalInvoker component does the reverse operation of the HTTPRIO component; it can translate SOAP requests into calls of Pascal interfaces (instead of shifting interface method calls into SOAP requests).

  • The WSDLHTMLPublish component can be used to extract the WSDL definition of the service from the interfaces it support, and performs the opposite role of the Web Services Importer Wizard. Technically, this is another HTTP dispatcher.

A Currency Conversion Web Service

Once this framework is in place—something you can also do by adding the three components listed in the previous section to an existing web module—you can begin writing a service. As an example, I've taken the euro conversion example from Chapter 3, "The Run-Time Library," and transformed it into a web service called ConvertService. First, I've added to the program a unit defining the interface of the service, as follows:

type
  IConvert = interface(IInvokable)
  ['{FF1EAA45-0B94-4630-9A18-E768A91A78E2}']
    function ConvertCurrency (Source, Dest: string; Amount: Double): Double;
      stdcall;
    function ToEuro (Source: string; Amount: Double): Double; stdcall;
    function FromEuro (Dest: string; Amount: Double): Double; stdcall;
    function TypesList: string; stdcall;
  end;

Defining an interface directly in code, without having to use a tool such as the Type Library Editor, provides a great advantage, as you can easily build an interface for an existing class and don't have to learn using a specific tool for this purpose. Notice that I've given a GUID to the interface, as usual, and used the stdcall calling convention, because the SOAP converter does not support the default register calling convention.

In the same unit that defines the interface of the service, you should also register it. This operation will be necessary on both the client and server sides of the program, because you will be able to include this interface definition unit in both:

uses InvokeRegistry;
   
initialization
  InvRegistry.RegisterInterface(TypeInfo(IConvert));

Now that you have an interface you can expose to the public, you have to provide an implementation for it, again by means of the standard Delphi code (and with the help of the predefined TInvokableClass class:

type
  TConvert = class (TInvokableClass, IConvert)
  protected
    function ConvertCurrency (Source, Dest: string; Amount: Double): Double;
      stdcall;
    function ToEuro (Source: string; Amount: Double): Double; stdcall;
    function FromEuro (Dest: string; Amount: Double): Double; stdcall;
    function TypesList: string; stdcall;
  end;

The implementation of these functions, which call the code of the euro conversion system from Chapter 3, is not discussed here because it has little to do with the development of the service. However, it is important to notice that this implementation unit also has a registration call in its initialization section:

InvRegistry.RegisterInvokableClass (TConvert);

Publishing the WSDL

By registering the interface, you make it possible for the program to generate a WSDL description. The web service application (since the Delphi 6.02 update) is capable of displaying a first page describing its interfaces and the detail of each interface, and returning the WSDL file. By connecting to the web service via a browser, you'll see something similar to Figure 23.3.

Click To expand
Figure 23.3: The description of the Convert-Service web service provided by Delphi components
Note 

Although other web service architectures automatically provide you with a way to execute the web service from the browser, this technique is mostly meaningless, because using web services makes sense in an architecture where different applications interoperate. If all you need to do is show data on a browser, you should build a website!

This auto-descriptive feature was not available in web services produced in Delphi 6 (which provided only the lower-level WSDL listing), but it is quite easy to add (or customize). If you look at the Delphi 7 SOAP web module you'll notice a default action with an OnAction event handler invoking the following default behavior:

WSDLHTMLPublish1.ServiceInfo(Sender, Request, Response, Handled);

This is all you have to do to retrofit this feature into an existing Delphi web service that lacks it. To provide similar functionality manually, you must call into the invocation registry (the InvRegistry global object), with calls like GetInterfaceExternalName and GetMethExternalName.

What's important is the web service application's ability to document itself to any other programmer or programming tool, by exposing the WSDL.

Creating a Custom Client

Let's move to the client application that calls the service. I don't need to start from the WSDL file, because I already have the Delphi interface. This time the form doesn't even have the HTTPRIO component, which is created in code:

private
  Invoker: THTTPRio;
   
procedure TForm1.FormCreate(Sender: TObject);
begin
  Invoker := THTTPRio.Create(nil);
  Invoker.URL := 'http://localhost/scripts/ConvertService.exe/soap/iconvert';
  ConvIntf := Invoker as IConvert;
end;

As an alternative to using a WSDL file, the SOAP invoker component can be associated with an URL. Once this association has been done and the required interface has been extracted from the component, you can begin writing straight Pascal code to invoke the service, as you saw earlier.

A user fills the two combo boxes, calling the TypesList method, which returns a list of available currencies within a string (separated by semicolons). You extract this list by replacing each semicolon with a line break and then assigning the multiline string directly to the combo items:

procedure TForm1.Button2Click(Sender: TObject);
var
  TypeNames: string;
begin
  TypeNames := ConvIntf.TypesList;
  ComboBoxFrom.Items.Text := StringReplace (TypeNames, ';', sLineBreak,
    [rfReplaceAll]);
  ComboBoxTo.Items := ComboBoxFrom.Items;
end;

After selecting two currencies, you can perform the conversion with this code (Figure 23.4 shows the result):

procedure TForm1.Button1Click(Sender: TObject);
begin
  LabelResult.Caption := Format ('%n', [(ConvIntf.ConvertCurrency(
    ComboBoxFrom.Text, ComboBoxTo.Text, StrToFloat(EditAmount.Text)))]);
end;

Figure 23.4:  The ConvertCaller client of the Convert-Service web service shows how few German marks you used to get for so many Italian liras, before the euro changed everything.

Asking for Database Data

For this example, I built a web service (based on the Web App Debugger) capable of exposing data about employees of a company. This data is mapped to the EMPLOYEE table of sample InterBase database we've used so often throughout the book. The Delphi interface of the web service is defined in the SoapEmployeeIntf unit as follows:

type
  ISoapEmployee = interface (IInvokable)
    ['{77D0D940-23EC-49A5-9630-ADE0751E3DB3}']
    function GetEmployeeNames: string; stdcall;
    function GetEmployeeData (EmpID: string): string; stdcall;
  end;

The first method returns a list of the names of all the employees in the company, and the second returns the details of a given employee. The implementation of this interface is provided in the Soap-EmployeeImpl unit with the following class:

type
  TSoapEmployee = class(TInvokableClass, ISoapEmployee)
  public
    function GetEmployeeNames: string; stdcall;
    function GetEmployeeData (EmpID: string): string; stdcall;
  end;

The implementation of the web service lies in the two previous methods and some helper functions to manage the XML data being returned. But before we get to the XML portion of the example, let me briefly discuss the database access section.

Accessing the Data

All the connectivity and SQL code in this example are hosted in a separate data module. Of course, I could have created some connection and dataset components dynamically in the methods, but doing so is contrary to the approach of a visual development tool like Delphi. The data module has the following structure:

object DataModule3: TDataModule3
  object SQLConnection: TSQLConnection
    ConnectionName = 'IBConnection'
    DriverName = 'Interbase'
    LoginPrompt = False
    Params.Strings = // omitted 
  end
  object dsEmplList: TSQLDataSet
    CommandText = 'select EMP_NO, LAST_NAME, FIRST_NAME from EMPLOYEE'
    SQLConnection = SQLConnection
    object dsEmplListEMP_NO: TStringField
    object dsEmplListLAST_NAME: TStringField
    object dsEmplListFIRST_NAME: TStringField
  end
  object dsEmpData: TSQLDataSet
    CommandText = 'select * from EMPLOYEE where Emp_No = :id'
    Params = <
      item
        DataType = ftFixedChar
        Name = 'id'
        ParamType = ptInput
      end>
    SQLConnection = SQLConnection
  end
end

As you can see, the data module has two SQL queries hosted by SQLDataSet components. The first is used to retrieve the name and ID of each employee, and the second returns the entire set of data for a given employee.

Passing XML Documents

The problem is how to return this data to a remote client program. In this example, I've used the approach I like best: I've returned XML documents, instead of working with complex SOAP data structures. (I don't get how XML can be seen as a messaging mechanism for SOAP invocation—along with the transport mechanism provided by HTTP—but then, it is not used for the data being transferred. Still, very few web services return XML documents, so I'm beginning to wonder if it's me or many other programmers who can't see the full picture.)

In this example, the GetEmployeeNames method creates an XML document containing a list of employees, with their first and last names as values and the related database ID as an attribute, using two helper functions MakeXmlStr (already described in the last chapter) and MakeXmlAttribute (listed here):

function TSoapEmployee.GetEmployeeNames: string;
var
  dm: TDataModule3;
begin
  dm := TDataModule3.Create (nil);
  try
    dm.dsEmplList.Open;
    Result := '<employeeList>' + sLineBreak;
    while not dm.dsEmplList.EOF do
    begin
      Result := Result + '  ' + MakeXmlStr ('employee',
        dm.dsEmplListLASTNAME.AsString + ' ' +
        dm.dsEmplListFIRSTNME.AsString,
        MakeXmlAttribute ('id', dm.dsEmplListEMPNO.AsString)) + sLineBreak;
      dm.dsEmplList.Next;
    end;
    Result := Result + '</employeeList>';
  finally
    dm.Free;
  end;
end;
   
function MakeXmlAttribute (attrName, attrValue: string): string;
begin
  Result := attrName + '="' + attrValue + '"';
end;

Instead of the manual XML generation, I could have used the XML Mapper or some other technology; but as you should know from Chapter 22 ("Using XML Technologies"), I rather prefer creating XML directly in strings. I'll use the XML Mapper to process the data received on the client side.

Note 

You may wonder why the program creates a new instance of the data module each time. The negative side of this approach is that each time, the program establishes a new connection to the database (a rather slow operation); but the plus side is that you have no risk related to the use of a multithreaded application. If two web service requests are executed concurrently, you can use a shared connection to the database, but you must use different dataset components for the data access. You could move the datasets in the function code and keep only the connection on the data module, or have a global shared data module for the connection (used by multiple threads) and a specific instance of a second data module hosting the datasets for each method call.

Let's now look at the second method, GetEmployeeData. It uses a parametric query and formats the resulting fields in separate XML nodes (using another helper function, FieldsToXml):

function TSoapEmployee.GetEmployeeData(EmpID: string): string;
var
  dm: TDataModule3;
begin
  dm := TDataModule3.Create (nil);
  try
    dm.dsEmpData.ParamByName('ID').AsString := EmpId;
    dm.dsEmpData.Open;
    Result := FieldsToXml ('employee', dm.dsEmpData);
  finally
    dm.Free;
  end;
end;
   
function FieldsToXml (rootName: string; data: TDataSet): string;
var
  i: Integer;
begin
  Result := '<' + rootName + '>' + sLineBreak;;
  for i := 0 to data.FieldCount - 1 do
    Result := Result + '  ' + MakeXmlStr (
      LowerCase (data.Fields[i].FieldName),
      data.Fields[i].AsString) + sLineBreak;
  Result := Result + '</' + rootName + '>' + sLineBreak;;
end;

The Client Program (with XML Mapping)

The final step for this example is to write a test client program. You can do so as usual by importing the WSDL file defining the web service. In this case, you also have to convert the XML data you receive into something more manageable—particularly the list of employees returned by the GetEmployeeNames method. As mentioned earlier, I've used Delphi's XML Mapper to convert the list of employees received from the web service into a dataset I can visualize using a DBGrid.

To accomplish this, I first wrote the code to receive the XML with the list of employees and copied it into a memo component and from there to a file. Then, I opened the XML Mapper, loaded the file, and generated from it the structure of the data packet and the transformation file. (You can find the transformation file among the source code files of the SoapEmployee example.) To show the XML data within a DBGrid, the program uses an XMLTransformProvider component, referring to the transformation file:

object XMLTransformProvider1: TXMLTransformProvider
  TransformRead.TransformationFile = 'EmplListToDataPacket.xtr'
end

The ClientDataSet component is not hooked to the provider, because it would try to open the XML data file specified by the transformation. In this case, the XML data doesn't reside in a file, but is passed to the component after calling the web service. For this reason the program moves the data to the ClientDataSet directly in code:

procedure TForm1.btnGetListClick(Sender: TObject);
var
  strXml: string;
begin
  strXml := GetISoapEmployee.GetEmployeeNames;
  strXML := XMLTransformProvider1.TransformRead.TransformXML(strXml);
  ClientDataSet1.XmlData := strXml;
  ClientDataSet1.Open;
end;

With this code, the program can display the list of employees in a DbGrid, as you can see in Figure 23.5. When you retrieve the data for the specific employee, the program extracts the ID of the active record from the ClientDataSet and then shows the resulting XML in a memo:

procedure TForm1.btnGetDetailsClick(Sender: TObject);
begin
  Memo2.Lines.Text := GetISoapEmployee.GetEmployeeData(
    ClientDataSet1.FieldByName ('id').AsString);
end;
Click To expand
Figure 23.5: The client program of the SoapEmployee web service example

Debugging the SOAP Headers

One final note for this example relates to the use of the Web App Debugger for testing SOAP applications. Of course, you can run the server program from the Delphi IDE and debug it easily, but you can also monitor the SOAP headers passed on the HTTP connection. Although looking at SOAP from this low-level perspective can be far from simple, it is the ultimate way to check if something is wrong with either a server or a client SOAP application. As an example, in Figure 23.6 you can see the HTTP log of a SOAP request from the last example.

Click To expand
Figure 23.6:  The HTTP log of the Web App Debugger includes the low-level SOAP request.

The Web App Debugger might not always be available, so another common technique is to handle the events of the HTTPRIO component, as the BabelFishDebug example does. The program's form has two memo components in which you can see the SOAP request and the SOAP response:

procedure TForm1.HTTPRIO1BeforeExecute(const MethodName: String;
  var SOAPRequest: WideString);
begin
  MemoRequest.Text := SoapRequest;
end;
   
procedure TForm1.HTTPRIO1AfterExecute(const MethodName: String;
  SOAPResponse: TStream);
begin
  SOAPResponse.Position := 0;
  MemoResponse.Lines.LoadFromStream(SOAPResponse);
end;

Exposing an Existing Class as a Web Service

Although you might want to begin developing a web service from scratch, in some cases you may have existing code to make available. This process is not too complex, given Delphi's open architecture in this area. To try it, follow these steps:

  1. Create a web service application or add the related components to an existing WebBroker project.

  2. Define an interface inheriting from IInvokable and add to it the methods you want to make available in the web service (using the stdcall calling convention). The methods will be similar to those of the class you want to make available.

  3. Define a new class that inherits from the class you want to expose and implements your interface. The methods will be implemented by calling the corresponding methods of the base class.

  4. Write a factory method to create an object of your implementation class any time a SOAP request needs it.

This last step is the most complex. You could define a factory and register it as follows:

procedure MyObjFactory (out Obj: TObject);
begin
  Obj := TMyImplClass.Create;
end;
   
initialization
  InvRegistry.RegisterInvokableClass(TMyImplClass, MyObjFactory);

However, this code creates a new object for every call. Using a single global object would be equally bad: Many different users might try to use it, and if the object has state or its methods are not concurrent, you might be in for big problems. You're left with the need to implement some form of session management, which is a variation on the problem we had with the earlier web service connecting to the database.


 
Previous Section Next Section


 


 

Delphi Sources


Copyright © 2004-2024 "Delphi Sources" by BrokenByte Software. Delphi Programming Guide
ร๐๓๏๏เ ยส๎ํ๒เ๊๒ๅ   Facebook   ั๑๛๋๊เ ํเ Twitter