.NET Remoting - Part II...

Part II: Practicalities: Creating a remotable class, SAOs and CAOs.
Knowledge assumed for series: VB.NET, VS.NET, Windows Forms, Part I.


By: Chris Sully Date: September 29, 2003 Download the code.

Introduction

Remoting provides a very flexible environment for distributed applications in the .NET arena. In part one of this series of four articles (note that this is an extension of the initially planned three as indicated in article I) I introduced the background to distributed applications and the .NET remoting architecture that aims to support such applications. In this article and the next two articles in the series we'll look at some examples of the application of this theory. In particular in this article we shall be creating a remotable class, a server activated object and a client activated object. The remaining topics for articles III and IV will then be declarative configuration issues, interface assemblies, the role of IIS and asynchronous remoting.

Creating a remoteable class

Remotable classes are created by inheriting from the MarshalByRefObject class. Our example class is going to connect and retrieve data from as instance of SQLServer. In our implementation this instance, somewhat artificially for a remoting scenario, shall be local. If this isn’t your scenario and your database isn't local you'll need to amend the corresponding occurrences of connection string information in the code. Similar applies if you're not using integrated security.

Note that this series of examples is based on those presented in Mike Gunderloy’s 'Developing XML Web Services and Server Components with VB.NET and the .NET Framework' (see references), a book I can recommend, particularly if studying for Microsoft exam 70-310.

The first thing we need to do is create a new VS.NET solution in which to create the projects we'll need. Next add a VB.NET class library project to this solution named RemotingDb, rename the default class library to DBConnect and add the following code:

Imports System
Imports System.Data
Imports System.Data.SqlClient

'Marshal-By-Reference remotable object
Public Class DbConnect
  Inherits MarshalByRefObject
  Private sqlConn As SqlConnection

  'default constructor connects to the Northwind database
  Public Sub New()
    sqlConn = New SqlConnection("data source=(local);" & _
      "initial catalog=Northwind;integrated security=SSPI")
    Console.WriteLine("Created a new connection to the Northwind database")
  End Sub

  'parameterized constructor connects to the specified database
  Public Sub New(ByVal DbName As String)
    sqlConn = New SqlConnection("data source=(local);" & _
      "initial catalog=" & DbName & ";integrated security=SSPI")
    Console.WriteLine("Created a new connection to the " & DbName & " database")
  End Sub

  Public Function ExecuteQuery(ByVal strQuery As String) As DataSet
    Console.Write("Starting to execute the query...")

    'create a SqlCommand to represent the query
    Dim sqlcmd As SqlCommand = sqlconn.CreateCommand()
    sqlcmd.CommandType = CommandType.Text
    sqlcmd.CommandText = strQuery

    'create a SqlDataAdapter object
    Dim sqlDa As SqlDataAdapter = New SqlDataAdapter
    sqlda.SelectCommand = sqlcmd

    'create a DataSet to hold the results
    Dim ds As DataSet = New DataSet
    Try
      'fill the DataSet using the DataAdapter
      sqlDa.Fill(ds, "Results")
    Catch ex As Exception
      Console.WriteLine(ex.Message, "Error executing query")
    End Try
    Console.WriteLine("Done.")
    ExecuteQuery = ds
  End Function

End Class

Build the project. The implementation is simple so far: as you can see it's just a standard class that inherits from the MarshalByRefObject class to provide the necessary infrastructure. We now have a remotable class but for it to be useful we now need to connect it to the remoting framework.

Creating a SAO

A remotable class is usually connected with the remoting framework through a separate server application, a remoting server. This application listens for the client request on a specified communication channel and instantiates the remote object or invokes its class members as required.

A remoting server must complete the following steps:

  1. Create a server channel that listens on a particular port for the incoming requests from connected application domains.
  2. Register this channel with the remoting framework, telling the framework which requests received via this channel should be directed to a given server application.
  3. Register the remotable class with the remoting framework, telling the framework which classes this server application can create for remote clients.

I'll introduce implementation of these steps for both the activation modes of an SAO: SingleCall and Singleton, albeit briefly in the case of the latter. I'll also show how we link up the client application at the other end of the channel thus providing a complete working example.

The server process for the example will be long running UI-less process that will continue to listen for incoming client requests on a channel. We're going to implement the classes as console applications. Any reader who knows anything about Windows services might be wondering why we're not implementing such an activity as a Windows service. Normally you would, or you might use an existing Windows service such as IIS to work as a remoting server. The choice is down to helping you, the reader, understand what's going on … the console Window shall be utilised to display various informative messages to reinforce the involved concepts.

SingleCall SAO

Create a new VS.NET VB.NET project of type console application within the existing solution. Add a reference to System.Runtime.Remoting. Rename the default vb file to DBConnectSingleCallServer and add the following code:

Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Tcp
Imports RemotingDB

Module DbConnectSingleCallServer

  Public Sub Main()

    'step 1: create and register a TCP server channel that listens on port 54321
    Dim channel As TcpServerChannel = New TcpServerChannel(54321)

    'step 2: register the channel
    ChannelServices.RegisterChannel(channel)

    'step 3: register the service that publishes DbConnect for remote access in SingleCall mode
    RemotingConfiguration.RegisterWellKnownServiceType(GetType(DbConnect), "DbConnect", WellKnownObjectMode.SingleCall)

    'write an informative message to the console
    Console.WriteLine("Started server in the " & "SingleCall mode")
    Console.WriteLine("Press <ENTER> to terminate " & "server...")
    Console.ReadLine()

  End Sub

End Module

Note that we create a TCP channel on a semi-arbitrary port 54321. This is a number in the private range so this should be fine on a company network assuming the port is not being used by another application. For a more widely distributed Internet application you will need to register the number with the appropriate authority – the IANA (Internet Assigned Numbers Authority).

The Client

We now have a remotable object and a remoting server. We now need a remoting client which must perform the following steps:

  1. Create and register a compatible client channel that is used by the remoting framework to send messages to the remoting server.
  2. Register the remotable class as a valid type in the client’s application domain.
  3. Instantiate the SAO on the server. Remember you can only use the default constructor with SAOs.

We shall implement the client as a Windows form. Add a Windows form project to the solution naming it DBConnectClientSAO. Add references to System.Runtime.Remoting and to the RemotingDB dll just created. Rename the default form to DBConnectClient. Add a test box to enter the query (txtQuery), a button to execute the query (btnExecute) and a datagrid (dgResults) to display the results of the query. Add the following code behind for the form:

Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Tcp
Imports RemotingDB

Public Class DbConnectClient
  Inherits System.Windows.Forms.Form

  'declare a remote object
  Dim dbc As DbConnect

  Private Sub btnExecute_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles btnExecute.Click
    Try
      'invoke a method on the remote object
      Me.dgResults.DataSource = dbc.ExecuteQuery(Me.txtQuery.Text)
      dgResults.DataMember = "Results"
    Catch ex As Exception
      MessageBox.Show(ex.Message, "Query Execution Error")
    End Try
  End Sub

  Private Sub DbConnectClient_Load(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles MyBase.Load

    'step 1: create and register a TCP client channel
    Dim channel As TcpClientChannel = New TcpClientChannel
    ChannelServices.RegisterChannel(channel)

    'step 2: register the remote class as a valid type in the client's application domain
    RemotingConfiguration.RegisterWellKnownClientType(GetType(DbConnect), "tcp://localhost:54321/DbConnect")

    'step 3: instantiate the remote class using the default constructor
    dbc = New DbConnect

  End Sub

End Class

As previously, comments are used in the above as explanatory text.

We now have three projects in our solution. Build the complete solution. We also need to specify multiple startup projects and in what order they are started. This is achieved via the property pages of the solution (Startup project). Select 'Multiple Startup Projects' and specify the settings and order as follows:

RemotingDBnone
DBConnectSingleCallServerstart
DBConnectClientSAOstart

Run the client project. You should get a command window popping up telling you the server object has been created in SingleCall mode, shortly followed by the Windows client. Enter a query like 'SELECT * from Customers'.

Have you spotted an issue with the client implementation above? The code imports a reference to the server dll, which is also in the client project. Why? Because:

This situation is counter-intuitive as well as being undesirable for a variety of other reasons. The situation can be improved however via the use of interface assemblies – assemblies that contain just interface information, not actual business logic. We'll return to this topic in the next article in this series.

Singleton SAO

We've implemented a SingleCall SAO. What about a SingletonSAO? The same remotable object can be activated in different modes without making any changes to the remotable object itself and with SAOs the choice of activation is specified at the server. Thus we can use the same client program as per the last example, simply modifying the remoting server code that registers the SAO, as follows:

RemotingConfiguration.RegisterWellKnownServiceType(GetType(DbConnect), "DbConnect", _
WellKnownObjectMode.Singleton)

Of course when you run this you will see little difference … but you'll know things are operating slightly differently under the hood and your appreciation of these differences when you come to more complex real world scenarios is key.

Creating a CAO

Progressing onto Client Activated Objects, no changes are required to the remotable class itself but changes are required to the remoting server (i.e. registration of the remotable class) and to the client.

Both tasks are very similar to what we've already seen with SAOs, so we'll jump into the code just highlighting the differences. You'll see that in this code we make use of one on the benefits of using CAOs – multiple constructors – via an extension of the implementation. We are going to additionally enable selection of a database within the SQLServer instance from the client.

Create a new Console application project in the same solution we've been using throughout this article, renaming the default file to DBConnectCAOServer and add the following code:

Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Tcp
Imports RemotingDB

Module DbConnectCAOServer

  Sub Main()

    'create and register a TCP Channel on port 54321
    Dim channel As TcpServerChannel = New TcpServerChannel(54321)
    ChannelServices.RegisterChannel(channel)

    'register the client activated object
    RemotingConfiguration.RegisterActivatedServiceType(GetType(DbConnect))

    'write some informative output to the console
    Console.WriteLine("Started server in the Client Activation mode")
    Console.WriteLine("Press <ENTER> to terminate server...")
    Console.ReadLine()
  End Sub

End Module

Next we need our new client application. Create a new Windows form project called DBConnectClientCAO; rename the default form to DBConnectClient. The form UI elements are as per the previous client application with the addition of a drop down list box (aka combobox) to allow selection of the target database and a corresponding button. The former should be named cboDatabases and the latter named btnSelect with a text property of 'select'. The controls have also been grouped into areas: Database, Query, Results with groupbox controls named grpDatabases, grpQuery and grpResults. A screen shot of the form design might assist:

The code behind also differs a little:

Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Tcp
Imports RemotingDB

Public Class DbConnectClient
  Inherits System.Windows.Forms.Form

  'declare a remote object
  Dim dbc As DbConnect

  Private Sub DbConnectClient_Load(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles MyBase.Load
    cboDatabases.SelectedIndex = 0
    grpQuery.Enabled = False
  End Sub

  Private Sub btnSelect_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles btnSelect.Click
    'disable the databases group box and enable the Query group box
    grpDatabases.Enabled = False
    grpQuery.Enabled = True

    'register a TCP client channel
    Dim channel As TcpClientChannel = New TcpClientChannel
    ChannelServices.RegisterChannel(channel)

    'register the remote class as a valid type in the client's application domain
    'by passing the Remote class and its URL
    RemotingConfiguration.RegisterActivatedClientType( _
    GetType(DbConnect), "tcp://localhost:54321")

    'instantiate the remote class
    dbc = New DbConnect(cboDatabases.SelectedItem.ToString())
  End Sub

  Private Sub btnExecute_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles btnExecute.Click
    Try
      'invoke a method on the remote object
      Me.dgResults.DataSource = _
      dbc.ExecuteQuery(Me.txtQuery.Text)
      dgResults.DataMember = "Results"
    Catch ex As Exception
      MessageBox.Show(ex.Message, "Query Execution Error")
    End Try
  End Sub

End Class

Again we need to configure the project properties: ensure the SAO server and previous associated client are not set to start and that DBConnectCAOServer and DBConnectCAOClient are set to start without debugging, and in this order. Make sure the startup objects for the individual projects are also set correctly, similarly to the SAO example. Start the solution without debugging and make sure all is working.

Note that:

  1. we are now able to use the parameterized constructor.
  2. an instance of the remotable object is created for each client.

Conclusion

We've run through implementations of the different approaches to remoting: singlecall and singleton SAOs and CAOs. We highlighted the interface assembly issue, which we'll discuss further in the next article in this series along with the use of declarative configuration files and the benefits they bring to the table.

References

.NET SDK

Developing XML WebServices and Server Components with VB.NET and the .NET Framework
Mike Gunderloy
Que