Click here to Skip to main content
15,880,469 members
Articles / Programming Languages / Visual Basic 14
Alternative
Article

.NET Remoting Events Explained for VB.NET

Rate me:
Please Sign up or sign in to vote.
4.82/5 (5 votes)
25 Oct 2015CPOL13 min read 13.6K   583   7  
This is a VB.NET Version for ".NET Remoting Events Explained"

Introduction

As an intermediate developer, I spent a great deal of time looking for .NET Remoting examples for VB.NET. I finally had to resort to recoding the article by Ron Beyer, ".NET Remoting Events Explained". Most of the credit for this article goes to him and I’d like to also express my gratitude to him for his permission to make a version of this article for VB.NET. I will try to present the VB.NET code in the same layout as this will be a good side by side comparison between VB.NET and C#. This code was written to be compatible with .NET4.0 in VB.NET 2013 and up. I am currently using it in VB.NET 2017.

Remoting with .NET is both a daunting endeavor the first time, and a way to make life a lot simpler. .NET's goal with the remoting framework is to make serialization and deserialization of data across application and machine boundaries as simple as possible while providing all the flexibility and power of the .NET framework. Here, we will take a look at using events in a remoting environment and the proper way to design an application for the use of events.

Background

Events make life a lot simpler to the downstream application, and using them in a client/server environment is no different. Notifying clients when something has changed on the server or when some event has occurred without the clients needing to poll the server means a much simpler implementation on the client side.

The problem with .NET is that the server side activating the event needs to know something about the actual implementation of the event on the consuming side. Too many times, I see a .NET remoting example with events that require either the server referencing the client application (sometimes the .EXE itself, yuck!) and/or the client having a reference to the full implementation of the server.

Good programming practice on both the server and client side is to separate the implementation from each other, so that the server does not need to know anything about how the client is implemented, and that the client doesn't need to know how the server is implemented.

Application Design

For our example application, we are going to have a server and a number of clients. The clients will be on separate machines but on the same internal network. The clients will be loosely coupled to the server; that is, the connected state of the client can change at any time for any reason.

The clients will send a message to the server, which must notify all the connected clients that a new message has arrived and what that message is. The clients will display the message when they are notified. Using the little bit above, we can determine that:

  1. The server must control its own lifetime.
  2. We will use the TCP protocol (IPC is inappropriate across machines).
  3. We will use .NET Events.
  4. The client and server cannot know each other's implementation details.

Common Library

So, separating the implementation apart, we will need some sort of common library to hold the data that is shared between the client and the server. Our common library will have the following:

  1. Event Declarations
  2. Event Proxy
  3. Server Interface

Let's start off with the event declarations (EventDeclarations.vb):

VB.NET
Namespace RemotingEvents.Common

    Public Delegate Sub MessageArrivedEvent(Message As String)

End Namespace

Pretty simple. All we do is declare a delegate called MessageArrivedEvent that identifies the function we will use as an event. Now, we'll skip ahead to the Server Interface (and come back to the EventProxy later):

VB.NET
Namespace RemotingEvents.Common

    Public Interface IServerObject

        Event MessageArrived As MessageArrivedEvent

        Sub PublishMessage(Message As String)

    End Interface
End Namespace

This is also pretty simple. We are declaring the interface to our server object here, but not the implementation. The client won't know anything about how these functions are implemented on the server side, just the interface to call into it or get notified of events. The server (as we'll see in the next section) adds a lot to this interface, but none of that is usable from the client side.

Now, let's take a look at the EventProxy class. First, the code:

VB.NET
Imports System.Runtime.Remoting.Lifetime

Namespace RemotingEvents.Common

    Public Class EventProxy
        Inherits MarshalByRefObject

        Public Event MessageArrived As MessageArrivedEvent

        Public Overrides Function InitializeLifetimeService()
            Dim baseLease As ILease = MyBase.InitializeLifetimeService()
            If (baseLease.CurrentState = LeaseState.Initial) Then
                ' Make Lease Infinite
                baseLease.RenewOnCallTime = TimeSpan.Zero
                baseLease.SponsorshipTimeout = TimeSpan.Zero
            End If

            Return baseLease

            'Returning null holds the object alive
            'until it is explicitly destroyed

            'Return Nothing

        End Function

        'Public Sub LocallyHandleMessageArrived(Message As String) Handles Me.MessageArrived
        Public Sub LocallyHandleMessageArrived(Message As String)

            Try
                If Not IsNothing(MessageArrivedEvent) Then
                    RaiseEvent MessageArrived(Message)
                End If
            Catch ex As Exception
                MsgBox(ex.InnerException)
            End Try

        End Sub

    End Class
End Namespace

Not an overly complicated class, but let's pay attention to some of the details. First, the class inherits from MarshalByRefObject. This is because the EventProxy is serialized and deserialized to and from the client side, so the remoting framework needs to know how to marshal the object. Using MarshalByRefObject here means that the object is marshaled across boundaries by reference, and not by value (through a copy).

The function InitializeLifetimeService() is overridden from the MarshalByRefObject class. Returning Nothing from this class means that we want the .NET environment to keep the proxy alive until explicitly destroyed by the application. We could also return a new ILease here, with the timeout set to TimeSpan.Zero to do the same thing.

The reason we have this proxy class is because the server side needs to know about the implementation of the event consumer on the client side. If we didn't use a proxy class, the server would have to reference the client implementation so it knows how and where to call the function. We'll see how to use this proxy class in the section on Client Implementation.

Server Implementation

Now, let's move on to the server implementation. The server is implemented in a separate project called (in our example) RemotingEvents.Server. This project creates a reference to the RemotingEvents.Common project so we can use the interface, event declaration, and event proxy (indirectly). Here is the full code:

VB.NET
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Tcp
Imports System
Imports System.ComponentModel
Imports System.Reflection
Imports RemotingEvents.Common.RemotingEvents.Common

Namespace RemotingEvents.Server

    Public Class RemotingServer
        Inherits MarshalByRefObject
        Implements IServerObject

        Private serverChannel As TcpServerChannel
        Private tcpPort As Integer
        Private internalRef As ObjRef
        Private serverActive As Boolean = False
        Private Const serverURI As String = "serverExample.Rem"

        Public Event MessageArrived As MessageArrivedEvent Implements IServerObject.MessageArrived

        Public Sub PublishMessage(Message As String) Implements IServerObject.PublishMessage

            SafeInvokeMessageArrived(Message)

        End Sub

        Public Sub StartServer(port As Integer)

            If (serverActive) Then
                Return
            End If

            Dim props As Hashtable = New Hashtable()
            props("port") = port
            props("name") = serverURI

            'Set up for remoting events properly
            Dim serverProv As BinaryServerFormatterSinkProvider = _
                                               New BinaryServerFormatterSinkProvider()
            serverProv.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full

            serverChannel = New TcpServerChannel(props, serverProv)

            Try

                ChannelServices.RegisterChannel(serverChannel, False)
                internalRef = RemotingServices.Marshal(Me, props("name").ToString())
                serverActive = True

            Catch re As RemotingException

                'Could not start the server because of a remoting exception

            Catch ex As Exception

                'Could not start the server because of some other exception
            End Try
        End Sub

        Public Sub StopServer()

            If Not serverActive Then
                Return
            End If

            RemotingServices.Unmarshal(internalRef)

            Try

                ChannelServices.UnregisterChannel(serverChannel)

            Catch ex As Exception
            End Try
        End Sub

        Private Sub SafeInvokeMessageArrived(Message As String)

            If (Not serverActive) Then
                Return
            End If

            If MessageArrivedEvent = Nothing Then
                Return         'No Listeners
            End If

            Dim listener As MessageArrivedEvent = Nothing
            Dim dels() As [Delegate] = MessageArrivedEvent.GetInvocationList()

            For Each del As [Delegate] In dels

                Try

                    listener = CType(del, MessageArrivedEvent)
                    listener.Invoke(Message)

                Catch ex As Exception

                    'Could not reach the destination, so remove it
                    'from the list
                    RemoveHandler MessageArrived, listener
                End Try
            Next
        End Sub

    End Class
End Namespace

It's a lot to absorb, so let's cut this down piece by piece:

VB.NET
Public Class RemotingServer
    Inherits MarshalByRefObject
    Implements IServerObject

Our class inherits from MarshalByRefObject and implements IServerObject. The MarshalByRefObject is because we want our server to be marshaled across boundaries using a reference to the server object, and the IServerObject means we are implementing the server interface that is known to the clients.

VB.NET
Private serverChannel As TcpServerChannel
Private tcpPort As Integer
Private internalRef As ObjRef
Private serverActive As Boolean = False
Private Const serverURI As String = "serverExample.Rem"

Public Event MessageArrived As MessageArrivedEvent Implements IServerObject.MessageArrived

Public Sub PublishMessage(Message As String) Implements IServerObject.PublishMessage

    SafeInvokeMessageArrived(Message)

End Sub

Here is the private working variable set and the implementation of the IServerObject members. TheTcpServerChannel is a reference to the TCP remoting channel that we are using for our server. The tcpPort andserverActive are pretty self-explanatory. ObjRef holds an internal reference to the object being presented (marshaled) for remoting. We don't necessarily need to marshal our own class, we could marshal some other class; I just prefer to put the service code inside the object being marshaled.

We'll take a look at SafeInvokeMessageArrived in a moment. First, let's take a look at starting and stopping the server service:

VB.NET
        Public Sub StartServer(port As Integer)

            If (serverActive) Then
                Return
            End If

            Dim props As Hashtable = New Hashtable()
            props("port") = port
            props("name") = serverURI

            'Set up for remoting events properly
            Dim serverProv As BinaryServerFormatterSinkProvider = _
                                             New BinaryServerFormatterSinkProvider()
            serverProv.TypeFilterLevel = _
                         System.Runtime.Serialization.Formatters.TypeFilterLevel.Full

            serverChannel = New TcpServerChannel(props, serverProv)

            Try

                ChannelServices.RegisterChannel(serverChannel, False)
                internalRef = RemotingServices.Marshal(Me, props("name").ToString())
                serverActive = True

            Catch re As RemotingException

                'Could not start the server because of a remoting exception

            Catch ex As Exception

                'Could not start the server because of some other exception
            End Try
        End Sub

        Public Sub StopServer()

            If Not serverActive Then
                Return
            End If

            RemotingServices.Unmarshal(internalRef)

            Try

                ChannelServices.UnregisterChannel(serverChannel)

            Catch ex As Exception
            End Try
        End Sub

I'm not going to run through all of this in extreme detail, but let's take a look at what is very important for remoting events:

VB.NET
Dim serverProv As BinaryServerFormatterSinkProvider = New BinaryServerFormatterSinkProvider()
serverProv.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full

serverChannel = New TcpServerChannel(props, serverProv)

Here, we set up our BinaryServerFormatterSinkProvider. We will need a similar matching set up on our client side (we'll see that in the next section). This identifies how we provide events across remoting boundaries (in this case, we chose binary implementation instead of XML). We need to set TypeFilterLevel to Full in order for events to work properly.

Since the only way to provide a sink provider to the constructor of TcpServerChannel is through the use of aHashtable, we need to use the hash table we constructed to hold the name of the server (which is used in the URI or "uniform resource identifier") and the port on which we remote.

For my machine, the resulting URI was tcp://192.168.1.68:15000/serverExample.Rem. This is used later on the client side, and is a bit difficult to understand (and determine) at first. You should note that using the functions of the internal referenced object to get the URIs results in a very strange looking string, and none of them represent what you can use to connect to your server.

Now, let's look at the SafeInvokeMessageArrived function:

VB.NET
Private Sub SafeInvokeMessageArrived(Message As String)

    If (Not serverActive) Then
        Return
    End If

    If MessageArrivedEvent = Nothing Then
        Return         'No Listeners
    End If

    Dim listener As MessageArrivedEvent = Nothing
    Dim dels() As [Delegate] = MessageArrivedEvent.GetInvocationList()

    For Each del As [Delegate] In dels

        Try

            listener = CType(del, MessageArrivedEvent)
            listener.Invoke(Message)

        Catch ex As Exception

            'Could not reach the destination, so remove it
            'from the list
            RemoveHandler MessageArrived, listener
        End Try
    Next
End Sub

This is how you should implement all your event invocation code, not just those with remoting. While I'm explaining why this should be with regards to remoting, the same can be held true for any application, it's just good practice.

Here, we first check if the server is active. If the server is not active, then we don't try to raise any events. This is just a sanity check. Next, we check to see if we have any attached listeners, which means the MessageArrived delegate (event) will be null. If it is, we just return.

The next two lines are important. We create a temporary delegate for the listener and then store the current invocation list that our event holds. We do this because while we are iterating through the invocation list, a client could remove itself (on purpose) from the invocation list and we could get into a thread un-safe situation.

Next, we loop through all the delegates and try to invoke them with the message. If the invocation throws an exception, we remove it from the invocation list, effectively removing that client from receiving notifications.

There are a couple points to remember here. First is that you do not want to declare your event with the [OneWay]attribute. Doing this makes this whole exercise invalid as the server will not wait to check for a result, and will always invoke each item in the invocation list regardless of whether it is connected or not. This isn't a big problem for short-lifetime server applications, but if your server runs for months or years at a time, your invocation list could grow to the point of taking your server down, and that's a hard bug to find.

You also need to realize that events are synchronous (more on this later), so the server will wait for the client to return from the function call before invoking the next listener. More on this later.

Client Implementation

Let's take a quick look at the client:

VB.NET
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Tcp
Imports RemotingEvents.Common.RemotingEvents.Common

Public Class Form1

    Public remoteServer As IServerObject
    Public eventProxy As EventProxy
    Private tcpChan As TcpChannel
    Private clientProv As BinaryClientFormatterSinkProvider
    Private serverProv As BinaryServerFormatterSinkProvider
    'Replace with your IP
    Private serverURI As String = "tcp://127.0.0.1:15000/serverExample.Rem"
    Private connected As Boolean = False

    Private Delegate Sub SetBoxText(Message As String)

    Public Sub New()

        InitializeComponent()

        clientProv = New BinaryClientFormatterSinkProvider()
        serverProv = New BinaryServerFormatterSinkProvider()
        serverProv.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full

        eventProxy = New EventProxy
        AddHandler eventProxy.MessageArrived, _
                  New MessageArrivedEvent(AddressOf eventProxy_MessageArrived)

        Dim props As Hashtable = New Hashtable()
        props("name") = "remotingClient"
        props("port") = 0      'First available port

        tcpChan = New TcpChannel(props, clientProv, serverProv)
        ChannelServices.RegisterChannel(tcpChan, False)

        RemotingConfiguration.RegisterWellKnownClientType(
          New WellKnownClientTypeEntry(GetType(IServerObject), serverURI))

    End Sub

    Sub eventProxy_MessageArrived(Message As String)
        SetTextBox(Message)
    End Sub

    Private Sub bttn_Connect_Click(sender As Object, e As EventArgs) Handles bttn_Connect.Click
        If (connected) Then
            Return
        End If

        Try

            remoteServer = CType(Activator.GetObject_
                              (GetType(IServerObject), serverURI), IServerObject)
            remoteServer.PublishMessage("Client Connected")
            'This is where it will break if we didn't connect

            'Now we have to attach the events...
            AddHandler remoteServer.MessageArrived, _
                               AddressOf eventProxy.LocallyHandleMessageArrived
            connected = True

        Catch ex As Exception

            connected = False
            SetTextBox("Could not connect: " + ex.Message)
        End Try
    End Sub

    Private Sub bttn_Disconnect_Click(sender As Object, e As EventArgs) _
                                        Handles bttn_Disconnect.Click

        If (Not connected) Then
            Return
        End If

        'First remove the event
        RemoveHandler remoteServer.MessageArrived, _
                       (AddressOf eventProxy.LocallyHandleMessageArrived)

        'Now we can close it out
        ChannelServices.UnregisterChannel(tcpChan)
    End Sub

    Private Sub bttn_Send_Click(sender As Object, e As EventArgs) Handles bttn_Send.Click

        If (Not connected) Then
            Return
        End If
        remoteServer.PublishMessage(tbx_Input.Text)
        tbx_Input.Text = ""
    End Sub

    Private Sub SetTextBox(Message As String)

        If (tbx_Messages.InvokeRequired) Then

            Me.BeginInvoke(New SetBoxText(AddressOf SetTextBox), New Object() {Message})
            Return

        Else
            tbx_Messages.AppendText(Message & vbNewLine)
        End If
    End Sub

End Class

Our client is a Windows form that has a reference to the RemotingEvents.Common library and, as you can see, holds a reference to the IServerObject and the EventProxy classes. Even though the IServerObject is an interface, we can make calls to it just like it were a class. If you run this example, you will need to change the URI in the code to match the IP of your server!!!

VB.NET
Public Class Form1

    Public remoteServer As IServerObject
    Public eventProxy As EventProxy
    Private tcpChan As TcpChannel
    Private clientProv As BinaryClientFormatterSinkProvider
    Private serverProv As BinaryServerFormatterSinkProvider
    'Replace with your IP
    Private serverURI As String = "tcp://127.0.0.1:15000/serverExample.Rem"
    Private connected As Boolean = False

    Private Delegate Sub SetBoxText(Message As String)

    Public Sub New()

        InitializeComponent()

        clientProv = New BinaryClientFormatterSinkProvider()
        serverProv = New BinaryServerFormatterSinkProvider()
        serverProv.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full

        eventProxy = New EventProxy()
        AddHandler eventProxy.MessageArrived, _
                 New MessageArrivedEvent(AddressOf eventProxy_MessageArrived)

        Dim props As Hashtable = New Hashtable()
        props("name") = "remotingClient"
        props("port") = 0      'First available port

        tcpChan = New TcpChannel(props, clientProv, serverProv)
        ChannelServices.RegisterChannel(tcpChan, False)

        RemotingConfiguration.RegisterWellKnownClientType(
          New WellKnownClientTypeEntry(GetType(IServerObject), serverURI))

    End Sub

In the constructor for the form, we set up the information about the remoting channel. You see, we create two sink providers, one for the client and one for the server. The server is the only one that you need to set theTypeFilterLevel to Full; the client side just needs a reference to a sink provider.

We also create the EventProxy and register the local event handler here. We will connect the server to the proxy when we connect to the server. All that is left is to create the TcpChannel object using our hash table and sink providers, register the channel, then register a WellKnownClientTypeEntry.

VB.NET
    Private Sub bttn_Connect_Click(sender As Object, e As EventArgs) Handles bttn_Connect.Click
        If (connected) Then
            Return
        End If

        Try

            remoteServer = CType(Activator.GetObject_
                             (GetType(IServerObject), serverURI), IServerObject)
            remoteServer.PublishMessage("Client Connected")
            'This is where it will break if we didn't connect

            'Now we have to attach the events...
            AddHandler remoteServer.MessageArrived, _
                                AddressOf eventProxy.LocallyHandleMessageArrived
            connected = True

        Catch ex As Exception

            connected = False
            SetTextBox("Could not connect: " + ex.Message)
        End Try
    End Sub

    Private Sub bttn_Disconnect_Click(sender As Object, e As EventArgs) _
                                Handles bttn_Disconnect.Click

        If (Not connected) Then
            Return
        End If

        'First remove the event
        RemoveHandler remoteServer.MessageArrived, _
                                  (AddressOf eventProxy.LocallyHandleMessageArrived)

        'Now we can close it out
        ChannelServices.UnregisterChannel(tcpChan)
    End Sub

Here is the connect and disconnect code. The only thing that I want to make a point of is that when we register the event for the remoteServer, we actually point it to our eventProxy.LocallyHandleMessageArrived, which just passes through the event to our application.

You should also note, that because of my hasty implementation of the client, if you click the Disconnect button, you will not be able to reconnect unless you restart the application. This is because I unregister the channel in the disconnect, but I don't register it in the connect function.

Quick Bit on Cross-Thread Calls

Real quick, I want to touch on cross-thread calls, as you will run into that with remoting and UI applications. An event handler runs on a separate thread than the one that services the user interface, so calling your TextBox.Text=property will throw that wonderful IllegalCrossThreadCallException. This can be turned off if you call Control.CheckForIllegalCrossThreadCalls = false, which will turn off the exception, but not fix the problem.

What will happen is you will create a deadlock while one thread waits for another and the other thread waits for the first. This will make both your client and the server hang (see the Events are Synchronous? section), and will keep the rest of your clients from getting the event.

You'll see in the client code, I have the following code:

VB.NET
Private Sub SetTextBox(Message As String)

    If (tbx_Messages.InvokeRequired) Then

        Me.BeginInvoke(New SetBoxText(AddressOf SetTextBox), New Object() {Message})
        Return

    Else
        tbx_Messages.AppendText(Message & vbNewLine)
    End If
End Sub

Which uses this.BeginInvoke to service setting the textbox using the UI thread that created the code. This can be expanded to take a textbox parameter so you don't have to create this function for each textbox. The important thing to remember is to not disable the cross thread calls check, and think multi-threaded.

Running the Application

Running the application as downloaded from the VS2013 IDE will start both the server and the client projects on the same machine. Clicking "Start Server" will start the remoting server. Connect the client to the remoting server by clicking "Connect" on the client screen, then type anything into the box and click "Send". This will make the message show up on both the client and server. The client receives the message through an event from the server, not directly from the text box.

You can start as many instances of the client as you want, even on the same machine, and send messages, all the messages should show up on each connected client. Try killing one of the clients (through the Task Manager) and send the message. You should notice a small delay in some of the clients getting the event. This is because the server must wait for the TCP socket to determine that the client is unreachable, which can take about 1.5 seconds (on my machine).

Events are Synchronous?

Don't do any long operations in the event handler code, your other clients will not receive events until it is finished, and events can stack up on the server side.

You can make the events asynchronous by using the Delegate.BeginInvoke function, but there are some important things to think about first:

First is that using BeginInvoke consumes a thread from the thread pool. .NET only gives you 25 threads per processor from the thread pool to consume, so if you have a lot of clients, you could use up your thread pool very quickly.

The second is that when you use BeginInvoke, you have to use EndInvoke. If your client application is still not ready to be ended, you can either force it to end, or you can make your server thread wait (bad idea) for it to finish, using the IAsyncResult.WaitOne function.

Lastly, it's difficult to determine (not saying impossible) if the client is reachable or not using asynchronous events.

What to Remember about Events

Events should only be used in the following situations:

  1. Event consumers are on the same network as the server.
  2. There are a small number of events.
  3. The client services the event quickly and returns.

Also, remember:

  1. Events are synchronous!
  2. Event delegates can become unreachable.
  3. Events make your application multi-threaded.
  4. Never use the [OneWay] attribute.

Alternatives to Remoting Events

Try to avoid .NET Remoting events if at all possible. Some technologies that can help you do notifications are:

  • UDP Message Broadcasting
  • MessageQueue Services
  • IP MultiCasting

References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --