Sunday, December 30, 2007

Working with .NET data in Delphi

Source: http://www.gekko-software.nl/DotNet/Art08.htm

 

Webservices offer a rich world of functionality. This world is available to the Delphi programmer with the introduction of the Webservice importer introduced in Delphi 6, with version 6.02 also available in Delphi pro. A webservice can work with pretty complex data, with .NET it is a snap to return and  receive complete XML datasets. Delphi does not know how to work with these datasets natively. In this paper I will show how to work with .NET data using the GekkoDotNetDataset componenet.

The .NET webservice

The webservice I am going to build will be a web-wrapper round a database. The database is an Access database local on the web-server. It  contains  customers, invoices and invoice details.

Methods of the webservice will expose this data to the web as strongly typed XML datasets. Other methods will accept XML datasets to update the database. Importing these tables in a .NET application will result in a nice XSD schema

For a step by step story how to build such a service you can read one of my dotnetjunkies stories, for the moment I will concentrate on the actual webmethods.

The Customers method returns a dataset containing all customers. It does so by creating a new typed dataset object : DataSetCustomers. This object is filled by the oleDbDataAdapter, the internals of this component do the real access to the database.

[WebMethod]
public DataSetCustomers Customers()
{
    DataSetCustomers ds =
new DataSetCustomers();
    oleDbDataAdapterCustomers.Fill(ds);
   
return ds;
}

The entire resulting XML dataset is returned. One of the many nice things of an XML dataset is that it can be serialized, it can be represented as one long string of characters. Which is something which is very easy to transport over the standard HTTP protocol.

To fill the contents of the invoices dataset, as described by the schema, takes a little more effort. The three tables can live together in one dataset but for every table another oleDbDataAdapter is needed.

[WebMethod]
public DataSetInvoices Invoices()
{
    DataSetInvoices ds =
new DataSetInvoices();
    oleDbDataAdapterCustomers.Fill(ds.Customers);
    oleDbDataAdapterInvoices.Fill(ds.Invoices);
    oleDbDataAdapterInvoiceDetails.Fill(ds.InvoiceDetails);
   
return ds;
}

Using dataAdapters all query possibilities of the database can be used. For an example you are still invited at the dotnetjunkies. DataAdapters can also be used to write to a database. The updates to be written are passed as a typed XML dataset. Which make the implementation of of the webmethod a one-liner:

[WebMethod]
public void UpDateCustomers(DataSetCustomers ds)
{
    oleDbDataAdapterCustomers.Update(ds.Customers);
}

Multiple tables can be updated in one go in the UpdateInvoices method. The order in these updates will be performed is important:

[WebMethod]
public void UpdateInvoices(DataSetInvoices ds)
{
    oleDbDataAdapterCustomers.Update(ds.Customers);
    oleDbDataAdapterInvoices.Update(ds.Invoices);
    oleDbDataAdapterInvoiceDetails.Update(ds.InvoiceDetails);
}

You cannot enter a new invoice if you don't know the customer yet. These integrity checks are also performed in an .NET XML dataset object. But in there they can be switched off by setting the EnforceConstraints property to false.

A .NET webservice consumer at work

In .NET you can build a windows client application which imports the webservice and it will work perfect with all functionality of the webservice. Which means that a windows application can update an Access database somewhere on a webserver on the other side of the globe using plain HTTP.

The client reads that data from the webserver like this

localhost.DataSetWebService ws = new localhost.DataSetWebService();
dataSetCustomers1.Clear();
dataSetCustomers1.Merge(ws.Customers());

The dataset is bound to a grid. Here the user can do some editing after which the update is invoked

localhost.DataSetWebService ws = new localhost.DataSetWebService();
ws.UpDateCustomers(dataSetCustomers1);

See this consumer at work in the dotnetjunkies story.

Importing the webservice in Delphi

It would be very nice to work with this webservice in Delphi. I will use Delphi 6.02 pro which has full support for webservices clients. Delphi has a webservice importer which is found under the file | New | Other | Webservices menu. After entering the URL of the webservice Delphi will generate a unit describing the webservice.

The webservice has two types, being the Customers and the Invoices XSD schema. The service has four methods who use these types in their parameters or as result-type. And this is what the Delphi makes out of it :

Which looks pretty disappointing. The Return types of Customers and Invoices webmethods are recognized as composite types Customers and Invoices. Alas, these types contain no members at all. Things get worse with the update methods. Both have a parameter named ds of type Invoices or Customers. The importer generates two methods with a parameter named ds of a type named ds as well. This ds type is declared and does not have any members either. That's not going to work. Many webservices work very well with Delphi but in this case it will need some extra help.

Introducing the GekkoDotNetDataSet component 

I have built the GekkoDotNetDataSet component. This component is based on the HTTPrio component and can be found in the demo code. It takes the following approach to the problem:

  • It wraps up one XML dataset.
  • It provides the data to other Delphi components as (client-)datasets.
  • The webservice has to have a function member which returns the typed dataset.
  • The webservice has to have an update member which accepts the typed XML dataset in a parameter.

The component introduces two new published events and one new published property, visible in the object inspector.

  • OnRequestGetInvocation, an event which is fired when the component requests the XMLdataset from the service.
  • Paramname. A string to store the name of the parameter of the updatemethod.
  • OnRequestUpdateInvocation, an event which is fired when the component requests the webservice to update the data.

The component has two public methods and two public properties to read and write data

  • The Get method reads the data into the componennt.
  • The DataTable publishes all data in an array of Delphi (Client)datasets.
  • TableCount counts the number of Delphi datasets.
  • The Update method sends all updates to the webservice

This componenet is part of the GekkoDotNetPackage, it will install itself on the webservices page.

Reading data with GekkoDotNetDataSet component 

I drop two of these components on the form. One for the Web Services' Customers dataset and the other for the Invoices dataset. Despite it's emptiness I can use the imported Service1.pas. The GekkoDotNetDataSet component is a HTTPrio descendent so I have to set the WSDLlocation in both components , it will be http://localhost/WebServices/DataService/Service1.asmx?wsdl. The component's only new property is ParamName, it is the name of the parameter of the Update methode, ds for both components.

The real new stuff is in two new events. These get fired when the component Get's or Update's data. As the component does not know which member of the webservice to invoke to read or write data it will make a callback to the component's user. Requesting the user to do the actual invoke.

procedure TForm1.DataSetCustomersRequestGetInvocation(Sender: TObject);
var Iservice : DataSetWEbServiceSoap;
   begin
   Iservice:= DataSetCustomers as DataSetWebServiceSoap;
   Iservice.Customers;
   end;

You have to get to the actual webservice by typecasting the component to the interface of the service, which can be done because the component is a HTTPrio wrapping up the webservice. The declaration of DataSetWebServiceSoap is in the imported Service1.pas. On this interface you call the function which will return the intended XML dataset. In the requestGetInvocation-eventhandler of the other componenent the Invoices method will be invoked.

The click of a button will fill the form with a dataset, which dataset depends on a radiogroup

procedure TForm1.ButtonGetClick(Sender: TObject);
var i : integer;
    DNdataSet : TGekkoDotNetDataSet;

   begin
   case RadioGroup1.ItemIndex of
        0 : DNdataSet:= DataSetCustomers;
        1 : DNdataSet:= DataSetInvoices;
        else DNdataSet:= nil;
        end;
   if Assigned(DNdataSet) then
      begin
      DNdataSet.Get;
      CreateDataGrids(DNdataSet);
      end;
   end;

By calling get on the customers dataset the DataSetCustomersRequestGetInvocation eventhandler will be executed. Which will make the right invocation.

Now the tables of the are filled I will show what's in them. The form has an empty pagecontrol. For every dataset a page is added to the pagecontrol : 

procedure TForm1.CreateDataGrids(Sender: TObject);
   var tP : tTabSheet;
   ds : tDataSource;
   dn : tDBNavigator;
   dg : tDBGrid;
   i : integer;

   DNdataSet : TGekkoDotNetDataSet;

   begin

   DNdataSet:= sender as TGekkoDotNetDataSet;
   if DNdataSet <> nil then
      begin
      for i:= 0 to DNdataSet.TableCount - 1 do
          begin
          tP:=tTabSheet.Create(self);
          tP.PageControl:= PageControl1;
          tP.Caption:= DNdataSet.DataTable[i].Name;

          ds:= tDataSource.Create(Self);

          dn:= tDBnavigator.Create(self);
          dn.Align:= alTop;
          dn.Parent:= tP;
          dn.DataSource:= ds;

          dg:= tDBGrid.Create(self);
          dg.Align:= alClient;
          dg.Parent:= tP;
          dg.DataSource:= ds;

          ds.DataSet:= DNdataSet.DataTable[i];
          end;
      end;

   end;

For every (client-)dataset in the GekkoDotNetDataSet I create a new page with a datagrid and a dbNavigator.

When running this application I can browse and update the data in my Delphi form.

Updating data with GekkoDotNetDataSet component 

To return all updates to the webservice the component uses another event

procedure TForm1.DataSetInvoicesRequestUpdateInvocation(Sender: TObject);
var Iservice : DataSetWEbServiceSoap;
   begin
   Iservice:= DataSetInvoices as DataSetWebServiceSoap;
   Iservice.UpDateInvoices(ds.Create);
   end;

This event is fired by the componenet when it's Update method is called. The component does not know which member to invoke, in this eventhandler the component' user is requested to invoke the desired method. The ds class is declared in the imported unit. It has no members but it will do to invoke the method.

The form sends the updates by the click of a button

procedure TForm1.ButtonSaveClick(Sender: TObject);
var DNdataSet : TGekkoDotNetDataSet;
   begin
   case RadioGroup1.ItemIndex of
      0 : DNdataSet:= DataSetCustomers;
      1 : DNdataSet:= DataSetInvoices;
      else DNdataSet:= nil;
      end;
   if Assigned(DnDataSet) then
      DNdataSet.Update;
   end;

Now we have a Delphi application which works with a .NET webservice and can read or write XML dataset data.

Inpecting the webservice's request and response

To get an idea what is going on I have set the GekkoDotNetDataSet componenet's before- and after- execution event to show the full SOAP request and response in explorer windows.

The request is passed to the eventhandler as a string and the response as a stream. The ShowXML method will send the stream to a browser, it does so by saving the xml as a temporary file and pointing a browser to it. In the BeforeExcute event the string request has to be written to a stream before being sent to the ShowXML method.

procedure TForm1.HTTPRIO1BeforeExecute(const MethodName: String; var SOAPRequest: WideString);
   var ts : tStringStream;
   buffer : string;
begin
   buffer:= SOAPrequest;
   ts:= tStringStream.Create(buffer);
   Showxml(ts, Send);
end;

procedure TForm1.HTTPRIO1AfterExecute(const MethodName: String; SOAPResponse: TStream);
begin
   ShowXML(SOAPResponse, Receive);
end;

Now you can see the full requests as they are sent to the .NET webservice and the response.

(Don't) try this ay home !

The GekkoDotNetDataSet component relies on the tDNdataSet, whose internals are described in another paper. The class has been created by trial and error in investigating the results of .NET built webservices. If you want to use the class in your own code please consider the following points (this is a disclaimer !)

  • The dataset does (by long) not support all possible field-types.
  • The functionality is based on the diffgram structure as found, this structure is not (as far as I know) backed up by some kind of formal specification. Special testing deserve the roworder attribute and the localization settings.
  • New updated versions of the component and these papers will appear on this website.

You are welcome to experiment with the component and any suggestions, remarks, comments, or other feedback will be greatly appreciated. It will all be used in the  updates.

Where are we ?

In this paper we have seen how a Delphi webservice client can work with a webservice which works with XML (diffgram) datasets. All functionality is stored in the GekkoDotNetDataSet component, which is based on Delphi's HTTPrio component.

What's next ?



Be a better friend, newshound, and know-it-all with Yahoo! Mobile. Try it now.

0 Comments: