Click here to Skip to main content
15,878,945 members
Articles / Programming Languages / C#

.NET Remoting Events Explained

Rate me:
Please Sign up or sign in to vote.
4.84/5 (22 votes)
3 Mar 2010CPOL13 min read 113.4K   3.5K   70   36
Explains producing and consuming .NET remoting events, the drawbacks, and advantages.

Image 1

Introduction

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

Spending some time upfront in the application design really saves some headaches down the line. Too many times, I see developers jump right into the implementation, and then half way through the programming, they find out that some fundamental aspect of the programming is wrong, and have to spend significant time restructuring or refactoring when a little time up front would have saved all that work.

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 can not 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.cs):

C#
namespace RemotingEvents.Common
{
    public delegate void MessageArrivedEvent(string Message);
}

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):

C#
namespace RemotingEvents.Common
{
    public interface IServerObject
    {

        #region Events

        event MessageArrivedEvent MessageArrived;

        #endregion

        #region Methods

        void PublishMessage(string Message);

        #endregion

    }
}

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:

C#
namespace RemotingEvents.Common
{
    public class EventProxy : MarshalByRefObject
    {

        #region Event Declarations

        public event MessageArrivedEvent MessageArrived;

        #endregion

        #region Lifetime Services

        public override object InitializeLifetimeService()
        {
            return null;
            //Returning null holds the object alive
            //until it is explicitly destroyed
        }

        #endregion

        #region Local Handlers

        public void LocallyHandleMessageArrived(string Message)
        {
            if (MessageArrived != null)
                MessageArrived(Message);
        }

        #endregion

    }
}

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 null 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:

C#
namespace RemotingEvents.Server
{
    public class RemotingServer : MarshalByRefObject, IServerObject
    {

        #region Fields

        private TcpServerChannel serverChannel;
        private int tcpPort;
        private ObjRef internalRef;
        private bool serverActive = false;
        private static string serverURI = "serverExample.Rem";

        #endregion

        #region IServerObject Members

        public event MessageArrivedEvent MessageArrived;

        public void PublishMessage(string Message)
        {
            SafeInvokeMessageArrived(Message);
        }

        #endregion

        public void StartServer(int port)
        {
            if (serverActive)
                return;

            Hashtable props = new Hashtable();
            props["port"] = port;
            props["name"] = serverURI;

            //Set up for remoting events properly
            BinaryServerFormatterSinkProvider serverProv = 
                  new BinaryServerFormatterSinkProvider();
            serverProv.TypeFilterLevel = 
                  System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;

            serverChannel = new TcpServerChannel(props, serverProv);

            try
            {
                ChannelServices.RegisterChannel(serverChannel, false);
                internalRef = RemotingServices.Marshal(this, 
                                 props["name"].ToString());
                serverActive = true;
            }
            catch (RemotingException re)
            {
                //Could not start the server because of a remoting exception
            }
            catch (Exception ex)
            {
                //Could not start the server because of some other exception
            }
        }

        public void StopServer()
        {
            if (!serverActive)
                return;

            RemotingServices.Unmarshal(internalRef);

            try
            {
                ChannelServices.UnregisterChannel(serverChannel);
            }
            catch (Exception ex)
            {

            }
        }

        private void SafeInvokeMessageArrived(string Message)
        {
            if (!serverActive)
                return;

            if (MessageArrived == null)
                return;         //No Listeners

            MessageArrivedEvent listener = null;
            Delegate[] dels = MessageArrived.GetInvocationList();

            foreach (Delegate del in dels)
            {
                try
                {
                    listener = (MessageArrivedEvent)del;
                    listener.Invoke(Message);
                }
                catch (Exception ex)
                {
                    //Could not reach the destination, so remove it
                    //from the list
                    MessageArrived -= listener;
                }
            }
        }
    }
}

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

C#
public class RemotingServer : MarshalByRefObject, IServerObject

Our class inherits from MarshalByRefObject and 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.

C#
#region Fields

private TcpServerChannel serverChannel;
private int tcpPort;
private ObjRef internalRef;
private bool serverActive = false;
private static string serverURI = "serverExample.Rem";

#endregion

#region IServerObject Members

public event MessageArrivedEvent MessageArrived;

public void PublishMessage(string Message)
{
    SafeInvokeMessageArrived(Message);
}

#endregion

Here is the private working variable set and the implementation of the IServerObject members. The TcpServerChannel is a reference to the TCP remoting channel that we are using for our server. The tcpPort and serverActive 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:

C#
public void StartServer(int port)
{
    if (serverActive)
        return;

    Hashtable props = new Hashtable();
    props["port"] = port;
    props["name"] = serverURI;

    //Set up for remoting events properly
    BinaryServerFormatterSinkProvider serverProv = 
                new BinaryServerFormatterSinkProvider();
    serverProv.TypeFilterLevel = 
      System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;

    serverChannel = new TcpServerChannel(props, serverProv);

    try
    {
        ChannelServices.RegisterChannel(serverChannel, false);
        internalRef = RemotingServices.Marshal(this, props["name"].ToString());
        serverActive = true;
    }
    catch (RemotingException re)
    {
        //Could not start the server because of a remoting exception
    }
    catch (Exception ex)
    {
        //Could not start the server because of some other exception
    }
}

public void StopServer()
{
    if (!serverActive)
        return;

    RemotingServices.Unmarshal(internalRef);

    try
    {
        ChannelServices.UnregisterChannel(serverChannel);
    }
    catch (Exception ex)
    {

    }
}

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:

C#
BinaryServerFormatterSinkProvider serverProv = 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 a Hashtable, 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:

C#
private void SafeInvokeMessageArrived(string Message)
{
    if (!serverActive)
        return;

    if (MessageArrived == null)
        return;         //No Listeners

    MessageArrivedEvent listener = null;
    Delegate[] dels = MessageArrived.GetInvocationList();

    foreach (Delegate del in dels)
    {
        try
        {
            listener = (MessageArrivedEvent)del;
            listener.Invoke(Message);
        }
        catch (Exception ex)
        {
            //Could not reach the destination, so remove it
            //from the list
            MessageArrived -= listener;
        }
    }
}

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:

C#
namespace RemotingEvents.Client
{
    public partial class Form1 : Form
    {

        IServerObject remoteServer;
        EventProxy eventProxy;
        TcpChannel tcpChan;
        BinaryClientFormatterSinkProvider clientProv;
        BinaryServerFormatterSinkProvider serverProv;
        //Replace with your IP
        private string serverURI = 
          "tcp://192.168.1.100:15000/serverExample.Rem";
        private bool connected = false;

        private delegate void SetBoxText(string Message);

        public Form1()
        {
            InitializeComponent();

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

            eventProxy = new EventProxy();
            eventProxy.MessageArrived += 
              new MessageArrivedEvent(eventProxy_MessageArrived);

            Hashtable props = new Hashtable();
            props["name"] = "remotingClient";
            props["port"] = 0;      //First available port

            tcpChan = new TcpChannel(props, clientProv, serverProv);
            ChannelServices.RegisterChannel(tcpChan);

            RemotingConfiguration.RegisterWellKnownClientType(
              new WellKnownClientTypeEntry(typeof(IServerObject), serverURI));

        }

        void eventProxy_MessageArrived(string Message)
        {
            SetTextBox(Message);
        }

        private void bttn_Connect_Click(object sender, EventArgs e)
        {
            if (connected)
                return;

            try
            {
                remoteServer = (IServerObject)Activator.GetObject(
                                  typeof(IServerObject), serverURI);
                remoteServer.PublishMessage("Client Connected");
                //This is where it will break if we didn't connect
            
                //Now we have to attach the events...
                remoteServer.MessageArrived += 
                  new MessageArrivedEvent(eventProxy.LocallyHandleMessageArrived);
                connected = true;
            }
            catch (Exception ex)
            {
                connected = false;
                SetTextBox("Could not connect: " + ex.Message);
            }
        }

        private void bttn_Disconnect_Click(object sender, EventArgs e)
        {
            if (!connected)
                return;

            //First remove the event
            remoteServer.MessageArrived -= eventProxy.LocallyHandleMessageArrived;

            //Now we can close it out
            ChannelServices.UnregisterChannel(tcpChan);
        }

        private void bttn_Send_Click(object sender, EventArgs e)
        {
            if (!connected)
                return;

            remoteServer.PublishMessage(tbx_Input.Text);
            tbx_Input.Text = "";
        }

        private void SetTextBox(string Message)
        {
            if (tbx_Messages.InvokeRequired)
            {
                this.BeginInvoke(new SetBoxText(SetTextBox), new object[] { Message });
                return;
            }
            else
                tbx_Messages.AppendText(Message + "\r\n");
        }
    }
}

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!!!

C#
public Form1()
{
    InitializeComponent();

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

    eventProxy = new EventProxy();
    eventProxy.MessageArrived += 
      new MessageArrivedEvent(eventProxy_MessageArrived);

    Hashtable props = new Hashtable();
    props["name"] = "remotingClient";
    props["port"] = 0;      //First available port

    tcpChan = new TcpChannel(props, clientProv, serverProv);
    ChannelServices.RegisterChannel(tcpChan);

    RemotingConfiguration.RegisterWellKnownClientType(
      new WellKnownClientTypeEntry(typeof(IServerObject), serverURI));

}

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 the TypeFilterLevel 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.

C#
private void bttn_Connect_Click(object sender, EventArgs e)
{
    if (connected)
        return;

    try
    {
        remoteServer = (IServerObject)Activator.GetObject(
                           typeof(IServerObject), serverURI);
        remoteServer.PublishMessage("Client Connected");
        //This is where it will break if we didn't connect
    
        //Now we have to attach the events...
        remoteServer.MessageArrived += 
          new MessageArrivedEvent(eventProxy.LocallyHandleMessageArrived);
        connected = true;
    }
    catch (Exception ex)
    {
        connected = false;
        SetTextBox("Could not connect: " + ex.Message);
    }
}

private void bttn_Disconnect_Click(object sender, EventArgs e)
{
    if (!connected)
        return;

    //First remove the event
    remoteServer.MessageArrived -= eventProxy.LocallyHandleMessageArrived;

    //Now we can close it out
    ChannelServices.UnregisterChannel(tcpChan);
}

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:

C#
private delegate void SetBoxText(string Message);

private void SetTextBox(string Message)
{
    if (tbx_Messages.InvokeRequired)
    {
        this.BeginInvoke(new SetBoxText(SetTextBox), 
                         new object[] { Message });
        return;
    }
    else
        tbx_Messages.AppendText(Message + "\r\n");
}

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 do not disable the cross thread calls check, and think multi-threaded.

Running the Application

Running the application as downloaded from the VS2008 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?

Let's run an experiment in the client code. Change the following function to resemble this:

C#
void eventProxy_MessageArrived(string Message)
{
    MessageBox.Show(Message);
    //SetTextBox(Message);
}

Run the project again, starting a couple clients. You'll notice that when the first client starts the message box, you have to click "OK" in the message box before the next client gets it. This is because events are synchronous! The server waits for a reply from the client before continuing.

The lesson here is to service your event and get out! 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

Book: Advanced .NET Remoting (Ingo Rammer / Mario Szpuszta) ISBN: 978-1-59509-417-9.

License

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


Written By
President 6D Systems LLC
United States United States
I studied Software Engineering at Milwaukee School of Engineering for 2 years before switching to Management of Information Systems for a more business oriented approach. I've been developing software since the age of 14, and have waded through languages such as QBasic, TrueBasic, C, C++, Java, VB6, VB.NET, C#, etc. I've been developing professionally since 2002 in .NET.

Comments and Discussions

 
QuestionI get an exception when calling Invoke Pin
Member 126141631-Jul-16 4:38
Member 126141631-Jul-16 4:38 
QuestionVB.NET Version of Code Pin
Greg Nutt10-Oct-15 13:36
Greg Nutt10-Oct-15 13:36 
GeneralMy vote of 5 Pin
Eugene Sadovoi29-Sep-15 18:05
Eugene Sadovoi29-Sep-15 18:05 
QuestionCould Client and Sever be on Different LANs? Pin
YanRaf18-Dec-13 21:38
YanRaf18-Dec-13 21:38 
AnswerRe: Could Client and Sever be on Different LANs? Pin
Ron Beyer19-Dec-13 5:08
professionalRon Beyer19-Dec-13 5:08 
QuestionSingleton Pin
OManzelli17-Dec-13 23:43
OManzelli17-Dec-13 23:43 
AnswerRe: Singleton Pin
Ron Beyer18-Dec-13 2:23
professionalRon Beyer18-Dec-13 2:23 
AnswerRe: Singleton Pin
OManzelli18-Dec-13 2:34
OManzelli18-Dec-13 2:34 
GeneralRe: Singleton Pin
Ron Beyer18-Dec-13 4:33
professionalRon Beyer18-Dec-13 4:33 
AnswerRe: Singleton Pin
OManzelli18-Dec-13 21:45
OManzelli18-Dec-13 21:45 
GeneralRe: Singleton Pin
Ron Beyer19-Dec-13 5:02
professionalRon Beyer19-Dec-13 5:02 
QuestionserverChannel times out after 2 minutes Pin
Pittsburger27-Sep-13 21:09
professionalPittsburger27-Sep-13 21:09 
QuestionBrilliant Article... One problem with my application. Pin
viggi855-Feb-13 8:36
viggi855-Feb-13 8:36 
AnswerRe: Brilliant Article... One problem with my application. Pin
Ron Beyer6-Feb-13 3:01
professionalRon Beyer6-Feb-13 3:01 
GeneralRe: Brilliant Article... One problem with my application. Pin
viggi856-Feb-13 11:58
viggi856-Feb-13 11:58 
GeneralRe: Brilliant Article... One problem with my application. Pin
Ron Beyer6-Feb-13 13:53
professionalRon Beyer6-Feb-13 13:53 
GeneralRe: Brilliant Article... One problem with my application. Pin
viggi857-Feb-13 4:07
viggi857-Feb-13 4:07 
GeneralRe: Brilliant Article... One problem with my application. Pin
Ron Beyer7-Feb-13 4:24
professionalRon Beyer7-Feb-13 4:24 
QuestionCalling from COM Pin
Ken Alexander19-Sep-12 19:25
Ken Alexander19-Sep-12 19:25 
GeneralMy vote of 5 Pin
loyal ginger13-Jun-12 5:20
loyal ginger13-Jun-12 5:20 
Thanks for your time.
QuestionClient wont respond after sending message Pin
ExoticmE2-Nov-11 11:19
ExoticmE2-Nov-11 11:19 
GeneralUnmarshal Pin
Leviathan662-Mar-11 20:38
Leviathan662-Mar-11 20:38 
GeneralRe: Unmarshal Pin
Moxxis28-Sep-11 20:11
Moxxis28-Sep-11 20:11 
GeneralIt dosn't work! Pin
gamabuntique13-Sep-10 11:44
gamabuntique13-Sep-10 11:44 
GeneralExcluding calling client Pin
obzekt21-Jul-10 3:56
obzekt21-Jul-10 3:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.