Delphi Clinic C++Builder Gate Training & Consultancy Delphi Notes Weblog Dr.Bob's Webshop
Dr.Bob's Delphi Notes Dr.Bob's Delphi Clinics Dr.Bob's Delphi Courseware Manuals
 Dr.Bob Examines... #54
See Also: other Dr.Bob Examines columns or Delphi articles

This article was first published in The Developers Group NewsLetter 2004/02

.NET Remoting and DataSets
In this article, we will continue our coverage of .NET Remoting techniques, using it to build a simple multi-tier application passing a DataSet from the Server to Clients, and updates back to the Server.

Distributed Database Applications with Delphi 8 for .NET
Last time, I explained that a .NET Remoting architecture consists of a server side and a client side, as well as an object that is used "between" the server and the client. The object can be a remote object - meaning that is always remains at the server side, and methods are executed at the server side only. Alternately, you can also have so-called mobile objects, that are serialised and send to the client machine, where they are deserialised and executed.
The remote object remains at the server side, and is referenced to and used by one or more clients. The clients create a reference to the remote object and invoke (remote) methods from this server. The .NET Remoting framework supports different communication protocols (HTTP and TCP), message formats (binary and SOAP) and security (IIS security and SSL), which can all be extended as well.
Remote objects are derived from class MarshalByRefObject, specifying that the objects will be returned by reference. A remote object is activated at the server, and the client gets a reference to the remote object called a proxy (an instance of TransparentProject class). The client can then use the proxy to call the remote methods, turning the request into a serialised message by a formatter, in a specified format (binary or SOAP). The serialised message is transported to the remote server object using a transport channel that uses HTTP or TCP as communication layer. At the server side, the remote object receives an incoming message using a transport channel, and has to deserialise the message into a request for a method invocation. The response of the method call is formatted into a response message, transported using the transport channel, received by the client again, and deserialised into the answer that can be used.

Shared Assembly
Using .NET Remoting, the client and server must be able to understand each other, talking about the same (interface) definition of the remote object. One of the techniques that is used is a shared assembly, which has to be deployed on the server as well as the client machines. The shared assembly only has to contain the shared interface definition, which is also called the remote object manager interface.
The main focus this time will be to build a .NET Remoting Server that "exposes" the EMPLOYEE table from the InterBase Employee.gdb database (as a .NET DataSet), as well as a .NET Remoting Client that can receive this .NET DataSet and work with, including the ability to send updates back to the server. The "interface" for the communication between the .NET Remoting Client and Server is defined in the shared assembly.
Using Delphi 8 for .NET, this means you have to start a new package project with File | New Package, saving the package in SharedRemoteInterface. Right-click on the Requires node, and choose Add Reference. Use the Add Reference dialog to add the System.Data assembly to the requirements of the package. Next, do File | New - Unit add a new unit to the package, and save that unit in file SharedInterface.pas. The interface definition for the remote object can be placed in this unit, and is as follows:

  unit SharedInterface;
  interface
  uses
    System.Data;

  type
    IRemoteObjectManager = interface
      function GetEMPLOYEE(start, count: Integer): DataSet;
      function SetEMPLOYEE(ClientDS: DataSet): Boolean;
    end;

  implementation

  end.
The shared assembly only contains the interface definition of the remote object. The interface IRemoteObjectManager consists of two methods: GetEMPLOYEE and SetEMPLOYEE. The GetEMPLOYEE method will be used to return a .NET DataSet which is filled with the records from the EMPLOYEE table, starting with record number start, and returning count records. If you want to receive all records, you can use 0 as value for start, and a very large number as value for count.

Remote Server
Once we have the remote object interface definition, we can implement it inside a remote server project. Note that there is a little problem with the Delphi 8 for .NET IDE if you start a new project that references an assembly from a previous project (that you just created in the IDE). To avoid these problems, you have to close the IDE between an assembly project and an assembly-using project.
Restart Delphi 8 for .NET, and create a new project for the remote server. To keep it simple, create a console project for the remote server, and call it RemoteDataBaseServer. Right-click on the project node and do Add Reference to add the reference to the SharedRemoteInterface.dll. Since this assembly will not be installed in the Global Assembly Cache, you should use the Browse button to locate it. Note that after you've compiled the console project, this will automatically copy the SharedRemoteInterface.dll to your project directory.
You can now implement the remote object, by first adding the SharedInterface unit to your uses clause, and then writing the following code:

type
  RemoteObjectManager = class(MarshalByRefObject, IRemoteObjectManager)
  public
    function GetEMPLOYEE(start, count: Integer: DataSet;
    function SetEMPLOYEE(ClientDS: DataSet): Boolean;
  end;
The implementation of the GetEMPLOYEE and SetEMPLOYEE methods also require the Borland.Data.Provider assembly - use the Add Reference dialog to add the reference to this assembly as well.

Get/SetEMPLOYEE Implementation
Then, add the System.Data, Borland.Data.Provider, Borland.Data.Common, and SharedInterface units to the uses clause of the RemoteDataBaseServer project, and implement the GetEMPLOYEE and SetEMPLOYEE methods as follows:

  function RemoteObjectManager.GetEMPLOYEE(start, count: Integer: DataSet;
  var
    Connection: BdpConnection;
    DataAdapter: BdpDataAdapter;

  begin
    Result := DataSet.Create;
    Connection := BdpConnection.Create('database=localhost:C:\Program Files\' +
      'Common Files\Borland Shared\Data\employee.gdb;' +
      'assembly=Borland.Data.Interbase,Version=1.5.1.0,Culture=neutral,' +
      'PublicKeyToken=91d62ebb5b0d1b1b;vendorclient=gds32.dll;' +
      'provider=Interbase;username=sysdba;password=masterkey');
    Connection.ConnectionOptions := 'rolename=myrole;' +
      'transaction isolation=ReadCommitted;sqldialect=3;'+
      'waitonlocks=False;loginprompt=False;servercharset=;commitretain=False';
    DataAdapter := BdpDataAdapter.Create('SELECT * FROM EMPLOYEE', Connection);
    try
      DataAdapter.Fill(Result, start, count, 'EMPLOYEE')
    finally
      DataAdapter.Free;
      Connection.Free
    end
  end;
Note the version 1.5.1.0 which is the current version (after Update 2), and 1.5.0.0 is the version of Borland.Data.Interbase.dll before Update 2 of Delphi 8. And also note the start and count values that are passed to the DataAdapter's Fill method, to ensure that we do not have to retrieve the entire contents of the EMPLOYEE table at once.
  function RemoteObjectManager.SetEMPLOYEE(ClientDS: DataSet): Boolean;
  var
    Connection: BdpConnection;
    DataAdapter: BdpDataAdapter;
  begin
    Result := False;
    Connection := BdpConnection.Create('database=localhost:C:\Program Files\' +
      'Common Files\Borland Shared\Data\employee.gdb;' +
      'assembly=Borland.Data.Interbase,Version=1.5.1.0,Culture=neutral,' +
      'PublicKeyToken=91d62ebb5b0d1b1b;vendorclient=gds32.dll;' +
      'provider=Interbase;username=sysdba;password=masterkey');
    Connection.ConnectionOptions := 'rolename=myrole;' +
      'transaction isolation=ReadCommitted;sqldialect=3;'+
      'waitonlocks=False;loginprompt=False;servercharset=;commitretain=False';
    DataAdapter := BdpDataAdapter.Create('SELECT * FROM EMPLOYEE', Connection);
    try
      DataAdapter.AutoUpdate(ClientDS, 'EMPLOYEE', BdpUpdateMode.All);
      Result := True // success!
    finally
      DataAdapter.Free;
      Connection.Free
    end
  end;
Note that most of the code is used to initialise the BdpConnection component in both methods. The last few lines, working with the DataAdapter are more interesting. In case of the SetEMPLOYEE we can call the AutoUpdate method of the DataAdapter which will send the updates back to the database. I'll cover error handling (for example when a record is already deleted or has been changed by another user) at some other time.

System.Runtime.Remoting
Right-click on the project node and add a reference to the System.Runtime.Remoting assembly. This assembly contains a number of useful namespaces, like System.Runtime.Remoting, System.Runtime.Remoting.Channels and System.Runtime.Remoting.Channels.HTTP, that we need to add to the uses clause as well. The complete uses clause should now be as follows:

  uses
    System.Data,
    Borland.Data.Provider,
    Borland.Data.Common,
    SharedInterface,
    System.Runtime.Remoting,
    System.Runtime.Remoting.Channels,
    System.Runtime.Remoting.Channels.HTTP;
We can now implement the server as follows:
  const
    PortNumber = 4242;
    ServerResource = 'RemoteObjectManager.soap';
  var
    Channel: HttpChannel;
  begin
    writeln('RemoteServer started.');
    Channel := HTTPChannel.Create(PortNumber);
    ChannelServices.RegisterChannel(Channel);
    writeln('Listening for SOAP messages on HTTP port ', PortNumber);
    RemotingConfiguration.RegisterWellKnownServiceType(
      typeof(RemoteObjectManager),
      ServerResource,
      WellKnownObjectMode.Singleton); // only one remote object
    writeln('Hit  to stop.');
    readln
  end.
Note that you can only run once instance of the remote server application - the WellKnownObjectMode.Singleton will make sure of that.

Remote Client
Time to start the .NET Remoting client application. Since we should be able to use the result of the GetEMPLOYEE method, and send changes back using the SetEMPLOYEE method, we should create a new Windows Forms Application for the client, and save it as RemoteDataBaseClient. Place a DataGrid. Then, right-click on the project and add the System.Runtime.Remoting and SharedRemoteInterface assemblies as references. Also, add SharedInterface, System.Runtime.Remoting, System.Runtime.Remoting.Channels, and the System.Runtime.Remoting.Channels.HTTP units to the uses clause. We need to create a HttpChannel again in order to communicate with the RemoteServer. This time, however, we do not have to specify a portnumber for the channel, since this is part of the RemoteServer information. This, as well as the first call to GetEMPLOYEE can be done in the OnLoad event of the WinForm (note that Channel and ObjManager are declared as private fields of the WinForm):

  type
    TWinForm = class(System.Windows.Forms.Form)
      ...
    private
      { Private Declarations }
      Channel: HttpChannel;
      ObjManager: IRemoteObjectManager;
    end;

  ...

  const
    RemoteServer = 'http://localhost:4242/';
    ServerResource = 'RemoteObjectManager.soap';

  procedure TWinForm.TWinForm_Load(sender: System.Object; e: System.EventArgs);
  begin
    Channel := HTTPChannel.Create; // no PortNumber needed
    ChannelServices.RegisterChannel(Channel);
    try
      ObjManager := Activator.GetObject(typeof(IRemoteObjectManager),
        RemoteServer + ServerResource);
    except
      MessageBox.Show('Could not get reference to IRemoteObjectManager.')
    end;
    // now we can use the ObjManager
    DataGrid1.DataSource := ObjManager.GetEMPLOYEE(0,10); // first 10 records
    DataGrid1.DataMember := 'EMPLOYEE'
  end;
Note that the last two lines of the OnLoad event handler already call the GetEMPLOYEE method and add it to the DataSource property of the DataGrid, as well as assigning the name EMPLOYEE to the DataMember property. This is enough for the .NET Remoting client to create the Remote Object and call the remote method with the DataSet as result, showing the first 10 records of the EMPLOYEE table. Apart from calling GetEMPLOYEE in the OnLoad event handler, it would be more flexible to have a button with the text "Connect" and only connect to the .NET Remoting server and call the GetEMPLOYEE method if we click on the Connect button. That would ensure that the .NET Remoting client application doesn't hang if you accidentally start it without starting the server first. The implementation for the OnClick event handler of btnConnect is as follows:
  procedure TWinForm.btnConnect_Click(sender: System.Object; e: System.EventArgs);
  begin
    DataGrid1.DataSource := ObjManager.GetEMPLOYEE(0,20); // only 20 records
    DataGrid1.DataMember := 'EMPLOYEE';
  end;
You can now maintain the number of records as well as the last record number, and add more buttons like "Next 20", "Previous 20", "First", etc. almost like a navigator that will retrieve sets of 20 records. As long as you assign the new DataSet to the DataSource property of the DataGrid, the client will only show 20 records at a time. I leave that as exercise for the reader.

Applying Updates
The next step is not difficult: drop two additional buttons, call them btnUndo and btnUpdate, and implement their Click event handlers as follows:

  procedure TWinForm.btnUndo_Click(sender: System.Object; e: System.EventArgs);
  begin
    (DataGrid1.DataSource as DataSet).RejectChanges
  end;

  procedure TWinForm.btnUpdate_Click(sender: System.Object; e: System.EventArgs);
  var
    Changes: DataSet;
  begin
    try
      Changes := (DataGrid1.DataSource as DataSet).GetChanges;
      if ObjManager.SetEMPLOYEE(Changes) then
      begin

        (DataGrid1.DataSource as DataSet).AcceptChanges
      end
    except
      on Ex: Exception do
        MessageBox.Show(Ex.StackTrace, Ex.Message)
    end
  end;
Note that the Update only sends the changes back to the server, using GetChanges. This avoids having to send the entire DataSet (including the changes but also all original record field values), and can save a lot of bandwidth. See http://www-106.ibm.com/developerworks/db2/library/techarticle/dm-0403swart/ for an article where I implemented a similar distributed dataset architecture, based on ASP.NET Web Services instead of .NET Remoting, but did not send only the changes back from the client to the server (but the entire DataSet instead).
Anyway, the resulting .NET Client application is a thin-client - independent of the database type used, and can be used to view, edit and update the EMPLOYEE table using a DataGrid, as shown in the screenshot below:

Note that the Undo and Update buttons are enabled, which is only the case if and only if the HasChanges property of the DataSet returns true. The DataSet is actually the DataSource property of the DataGrid, so the check for changes has to be done as follows:

  procedure TWinForm.DataGrid1_CurrentCellChanged(sender: System.Object; e: System.EventArgs);
  begin
    btnUndo.Enabled := (DataGrid1.DataSource as DataSet).HasChanges;
    btnUpdate.Enabled := btnUndo.Enabled
  end;
This OnCurrentCellChanged event handler will cast the DataGrid's DataSource property to a DataSet (which it is, since it's the result of the GetEMPLOYEE method), and call the HasChanges method. It this returns True, then both the Undo and Update button should be enabled, otherwise they remain disabled.
Obviously, the .NET Remoting server must be running before the client can connect to it, and it will report the calls made to GetEMPLOYEE and SetEmployee, as shown below:

Summary
In this article, we've examined how we can build simple distributed applications with Delphi 8 for .NET, using .NET Remoting as technique to allow clients to obtain a reference to a remote object. The example implemented a GetEMPLOYEE and SetEMPLOYEE method, passing a .NET DataSet from server to client applications.

For more information, see my BorCon 2004 paper on Multi-tier/Distributed Database Applications in .NET.


This webpage © 2004-2010 by Bob Swart (aka Dr.Bob - www.drbob42.com). All Rights Reserved.