Google
WWW Yariv Hammer's Code Site

Saturday, January 28, 2006

Maintaining a List of All Connected Clients - A Remoting Tutorial

Introduction
This tutorial continue the the list of Remoting tutorials in this blog. In my last tutorial I showed how to recover from disconnecions both in the server and the client.
Now I am going to demonstrate how to maintain a list of connected clients both in the server and the clients. At the end, we will have a window that looks like a chat room, with all the connected users listed on the form.

The starting point of this tutorial is the end point of the previous one, so be sure to read my previous one first.

Full source code (C#) can be downloaded from here (~350 KB)

Step 1 - Create an IClientsManager Interface
In the Common assembly we will add a new interface called IClientsManager.
Here is the code:
-------------------------------------------

using System.Collections;
namespace Common
{
public interface IClientsManager
{
long RegisterNewClient();
bool NotifyClientAlive(long clientId);
bool IsClientConnected(long clientId);
ICollection GetListOfActiveClients();
}
}
--------------------------------------------------------
The client will be identified by a unique id, given by the server. Later you can extend this to be a full ClientInfo class. For now a number will do.
The interface IClientsManager has 4 methods. RegisterNewClient will be called by the client when it connects to the server. The server will return the new Id.
NotifyClientAlive will be called by the client once a period. When the method is no longer called, the server can know that the client is disconnected and thus notify the other clients.
IsClientConnected will help clients ask the server whether another client is connected.
GetListOfActiveClients will be used by the clients to refresh their list of active clients.

Step 2 - Extending the Messages
Before we jump for the implementation of the new interface we need to do some modifications.
In order to distinguish between custom messages raised by the clients, and system messages such as client disconnections, we will create a new type of message: SystemMessage.

First we will change a bit the Message class (in Common assembly)
--------------------------------------------
[Serializable()]
public class Message
{
public enum MessageType
{
Custom,
System
}
public string _msg;
public Message(string msg)
{
this._msg = msg;
}
public string Text
{
get { return _msg; }
}
public virtual MessageType Type
{
get { return MessageType.Custom;}
}
}
--------------------------------------------------

What I've done here is add an enumeration MessageType. If the message is a string message sent by a client (as before) then the message is a Custom message. If the message is sent by the server to notify about the connections or disconnection of a client then type is System.

Now we will derive from Message to create a new class SystemMessage. I put the class in the Message.cs file right under the Message class.
-------------------------------------------------------
[Serializable()]
public class SystemMessage:Message
{
private SystemMessageType _type;
private long _clientId;
public enum SystemMessageType
{
ClientConnected,
ClientDisconnected
}
public SystemMessage(SystemMessageType type, long clientId, string message):
base(message)
{
_type = type;
_clientId = clientId;
}
public override Common.Message.MessageType Type
{
get { return MessageType.System; }
}
public SystemMessageType SystemType
{
get { return _type; }
}
public long ClientId
{
get { return _clientId; }
}
}
---------------------------------------------------------
The SystemMessageType enumeration contains client connected or client disconnected types of messages. Later we might want to add more system messages. The SystemType property holds this information.
The ClientId property will be used in order to know which client connected or disconnected.

Note: I must say that rethinking about this design, I don't like it. It is very hard to extend the Message using inheritance. I would much prefer to use a pattern such as the Tag property of a Control (extend the message by composition instead of inheritance). But lets leave it like this at this point.

We will return later to modify further the Message class.

Now we add a method, BroadcastSystemMessage to the Broadcaster class in the Server:
--------------------------------------------
[Serializable()]
public class Broadcaster:MarshalByRefObject,IBroadcaster
{
public event MessageArrivedHandler MessageArrived;
public void BroadcastMessage(Message msg)
{
SafeInvokeEvent(msg);
}
public void BroadcastSystemMessage(SystemMessage msg)
{
SafeInvokeEvent(msg);
}
private void SafeInvokeEvent(Message msg)
{
if (MessageArrived == null)
return;
MessageArrivedHandler mah=null;
foreach(Delegate del in MessageArrived.GetInvocationList())
{
try
{
mah = (MessageArrivedHandler)del;
mah(msg);
}
catch (Exception ex)
{
Console.Write(ex.Message);
MessageArrived -= mah;
}
}
}
public override object InitializeLifetimeService()
{
return null;
}
}
------------------------------------------------
The BroadcastMessage and BroadcastSystemMessages are identical. We only make a distinction in order for our code to be more readable. Note that the client cannot invoke this new method because we did not add the method to the IBroadcaster interface. This is good, because only the server should raise system events.
By now you should have guessed that we are going to use our already implemented Broadcaster to distribute the connections and disconnections of clients. Thats the power of a good and solid well-designed system.

Last at this step we need to extend the functionality of MessageBroadcaster in the client. The class will raise the SystemMessageArrived event, so the client's objects can easily distinguish between the two types of events:
---------------------------------------------------------

using System;
using System.Net.Sockets;
using System.Threading;
using Common;
namespace Client
{
public class MessageBroadcaster:IBroadcaster
{
private IBroadcaster _bcaster = null;
private BroadcastEventHelper _eventHelper = null;
public MessageBroadcaster()
{
if (ClientStartup.Connector.ServerAlive)
{
InitializeObjects();
}
ClientStartup.Connector.ServerDisconnected += new EventHandler(Connector_ServerDisconnected);
ClientStartup.Connector.ServerReconnect += new EventHandler(Connector_ServerReconnect);
}
public void BroadcastMessage(Message msg)
{
if (_bcaster == null)
throw new ServerNotAliveException();
try
{
_bcaster.BroadcastMessage(msg);
}
catch (SocketException ex)
{
Console.WriteLine(ex.Message);
}
catch (ThreadAbortException ex)
{
Console.WriteLine(ex.Message);
}
}
private void HandleMessage(Message msg)
{
switch (msg.Type)
{
case Message.MessageType.Custom:
if (MessageArrived != null)
MessageArrived(msg);
break;
case Message.MessageType.System:
if (SystemMessageArrived != null)
SystemMessageArrived(msg);
break;
}
}
private void InitializeObjects()
{
_bcaster = (IBroadcaster)RemotingHelper.GetObject(typeof(IBroadcaster));
_eventHelper = new BroadcastEventHelper();
_eventHelper.MessageArrivedLocally += new MessageArrivedHandler(HandleMessage);
_bcaster.MessageArrived += new MessageArrivedHandler(_eventHelper.LocallyHandleMessageArrived);
}
private void Connector_ServerDisconnected(object sender, EventArgs e)
{
_bcaster = null;
_eventHelper = null;
}
private void Connector_ServerReconnect(object sender, EventArgs e)
{
InitializeObjects();
}
public event MessageArrivedHandler MessageArrived;
public event MessageArrivedHandler SystemMessageArrived;
}
}
--------------------------------------------------------
Step 3 - Implementing the ClientsManager Class
Now we are more than ready to implement the IClientsManager interface. Add a new class to the Server assembly called ClientsManager, derived from MarshalByRefObject and implementing the new IClientsManager interface.
First the code:
------------------------------------------------
using System;
using System.Collections;
using System.Timers;
using Common;
namespace Server
{
public class ClientsManager:MarshalByRefObject, IClientsManager
{
private Timer _aliveTimer = new Timer(1000);
private Hashtable _currentClients = new Hashtable();
private long _currentMaxClientId = -1;
public ClientsManager()
{
_aliveTimer.Elapsed += new ElapsedEventHandler(aliveTimer_Elapsed);
_aliveTimer.Start();
}
public override object InitializeLifetimeService()
{
return null;
}
public long RegisterNewClient()
{
long id = ++_currentMaxClientId;
_currentClients[id] = DateTime.Now.Ticks;
Broadcaster bcast = (Broadcaster) RemoteSingletonObjectsList. Instance. GetRemoteSingletonObject( typeof(Broadcaster));
bcast. BroadcastSystemMessage (new SystemMessage(SystemMessage.SystemMessageType. ClientConnected, id,""));
return id;
}
public bool NotifyClientAlive(long clientId)
{
if (IsClientConnected(clientId))
{
_currentClients[clientId] = DateTime.Now.Ticks;
return true;
}
return false;
}
public bool IsClientConnected(long clientId)
{
return _currentClients.ContainsKey(clientId);
}
public ICollection GetListOfActiveClients()
{
return _currentClients.Keys;
}
private void aliveTimer_Elapsed(object sender, ElapsedEventArgs e)
{
_aliveTimer.Stop();
long now = DateTime.Now.Ticks;
foreach (long id in _currentClients.Keys)
{
long lastTicks = (long)_currentClients[id];
if (now - lastTicks >= 20000000)
{
_currentClients.Remove(id);
Broadcaster bcast = (Broadcaster) RemoteSingletonObjectsList. Instance.GetRemoteSingletonObject( typeof(Broadcaster));
bcast.BroadcastSystemMessage( new SystemMessage(SystemMessage.SystemMessageType. ClientDisconnected,id,""));
break;
}
}
_aliveTimer.Start();
}
}
}
-------------------------------------------------------
_currentClients is a HashTable - the keys are the clients Ids of connected clients. The values are time stamps. The Timer (_aliveTimer) will elapse every second, and check the timestamps. If a client failed to call NotifyClientAlive for more then 20 seconds or so then the server will remove the client from _currentClients and notify everybody using the BroadcastSystemMessage method of Broadcaster.
When a client calls RegisterNewClient, it is added to _connectedClients, and a new Id is returned. The Ids are resources handled by this class. In our case the Ids are sequential, so each client gets an Id larger by 1 than the previous client. This way we ensure that the Ids are unique. A system event will be broadcasted, notifying everybody about the new client.
Last, the NotifyClientAlive should be called by clients, passing their Ids, once in a while. The timestamp will be updated every call. This way if a client fails to notify the server, the _aliveTimer will clean it up.

Next we register the new class for remoting. Open the App.Config of the server and add this line (you should already know where to put the line - in the "service" tag)
------------------------------------------------------------
<wellknown mode= "Singleton" type= "Server.ClientsManager, Server" objectUri= "ClientsManager.soap"/>
------------------------------------------------------------

We also need to register the class in Client's App.Config file. Add this line to the "client" tag:
------------------------------------------------------------
<wellknown type= "Common.IClientsManager, Common" url= "tcp://localhost:16784/ EventServer/ClientsManager.soap"/>
------------------------------------------------------------
Step 4 - Changing ClientConnector Class in the Client
The ClientConnector class was created in the previous tutorial. Its purose is throwing events whenever the client is connected and disconneted to the server.
We will now add more functionality to this class.
-------------------------------------------------------------

using System;
using System.Threading;
using System.Timers;
using Common;
using Timer = System.Timers.Timer;
namespace Client
{
public class ClientConnector
{
public ClientConnector()
{
this.ServerReconnect += new EventHandler (ClientConnector_ServerReconnect);
this.ServerDisconnected += new EventHandler (ClientConnector_ServerDisconnected);
_serverConnectivityTimer = new Timer(1000);
_serverConnectivityTimer.Elapsed += new ElapsedEventHandler(serverConnectivityTimer_Elapsed);
_serverConnectivityTimer.Start();
}
public event EventHandler ServerDisconnected;
public event EventHandler ServerReconnect;
private Timer _serverConnectivityTimer;
private void serverConnectivityTimer_Elapsed(object sender, ElapsedEventArgs e)
{
_serverConnectivityTimer.Stop();
CheckServerConnectivity();
_serverConnectivityTimer.Start();
}
private bool _serverIsAlive = false;
private void CheckServerConnectivity()
{
try
{
if (_serverIsAlive && IsClientIdValid)
{
IClientsManager manager = (IClientsManager)RemotingHelper.GetObject(typeof(IClientsManager));
manager.NotifyClientAlive(this.ClientId);
}
IServerConnector serverConnector = (IServerConnector) RemotingHelper.GetObject (typeof(IServerConnector));
bool isAlive = serverConnector.IsAlive;
if (isAlive && !_serverIsAlive)
SetServerAlive();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Thread t = new Thread(new ThreadStart(SetServerNotAlive));
t.Start();
t.Join();
}
}
private void SetServerAlive()
{
if (!_serverIsAlive && ServerReconnect != null)
ServerReconnect(this,EventArgs.Empty);
_serverIsAlive = true;
}
public void SetServerNotAlive()
{
if (_serverIsAlive && ServerDisconnected != null)
ServerDisconnected(this,EventArgs.Empty);
_serverIsAlive = false;
}
public bool ServerAlive
{
get
{
_serverConnectivityTimer.Stop();
CheckServerConnectivity();
_serverConnectivityTimer.Start();
return _serverIsAlive;
}
}
private long _clientId = -1;
public event EventHandler ClientIdChanged;
public long ClientId
{
get { return _clientId; }
}
public bool IsClientIdValid
{
get { return _clientId >= 0; }
}
private void ClientConnector_ServerReconnect (object sender, EventArgs e)
{
IClientsManager manager = (IClientsManager) RemotingHelper.GetObject (typeof(IClientsManager));
_clientId = manager.RegisterNewClient();
if (ClientIdChanged != null)
ClientIdChanged(this,EventArgs.Empty);
}
private void ClientConnector_ServerDisconnected (object sender, EventArgs e)
{
_clientId = -1;
}

}
}
------------------------------------------------------------
the _clientId property is by default equal to -1. When the event ServerReconnect is raised, we assign the client with a new client Id, by calling RegisterNewClient (See step 3). The server will notify the other clients (Next step). When the event ServerDisconnected is raised we assign the client again with Id -1.
We already had a timer to check if the server is alive (remember the ServerConnector class from the previous tutorial?). We now need to call NoitifyClientAlive in the server (See step 3).
Last I added the ClientIdChanged, because it is a good practice to have <Property>Changed events.

Step 5 - Implementing the Client Side Object
Add a new class to the Client assembly, called ConnectedClients.
------------------------------------------------------------
using System;
using System.Collections;
using Common;
namespace Client
{
public class ConnectedClients
{
private MessageBroadcaster _message = new MessageBroadcaster();
private ArrayList _activeClients = new ArrayList();
public ConnectedClients()
{
_message.SystemMessageArrived += new MessageArrivedHandler(Message_SystemMessageArrived);
ClientStartup.Connector.ServerDisconnected += new EventHandler(Connector_ServerDisconnected);
ClientStartup.Connector.ServerReconnect += new EventHandler(Connector_ServerReconnect);
}
private void Message_SystemMessageArrived(Message msg)
{
SystemMessage systemMessage = msg as SystemMessage;
switch (systemMessage.SystemType)
{
case SystemMessage.SystemMessageType.ClientConnected:
AddClient(systemMessage.ClientId);
break;
case SystemMessage.SystemMessageType.ClientDisconnected:
if (systemMessage.ClientId == ClientStartup.Connector.ClientId) return;
_activeClients.Remove(systemMessage.ClientId);
if (ClientRemoved != null)
ClientRemoved(this,new ClientEventArgs( systemMessage.ClientId));
break;
}
}
private void AddClient(long clientId)
{
if (clientId == ClientStartup.Connector.ClientId) return;
_activeClients.Add(clientId);
if (ClientAdded != null)
ClientAdded(this,new ClientEventArgs(clientId));
}
public ArrayList ActiveClients
{
get { return _activeClients; }
}
public event ClientEventHandler ClientAdded;
public event ClientEventHandler ClientRemoved;
public event EventHandler ClientsCleared;
public class ClientEventArgs:EventArgs
{
public long ClientId
{
get { return _clientId; }
}
public ClientEventArgs(long clientId)
{
this._clientId = clientId;
}
private long _clientId = -1;
}
public delegate void ClientEventHandler(object sender, ClientEventArgs e);
private void Connector_ServerDisconnected(object sender, EventArgs e)
{
ClearClients();
}
private void ClearClients()
{
_activeClients.Clear();
if (ClientsCleared != null)
ClientsCleared(this,EventArgs.Empty);
}
private void Connector_ServerReconnect(object sender, EventArgs e)
{
RefreshList();
}
public void RefreshList()
{
ClearClients();
IClientsManager manager = (IClientsManager)RemotingHelper.GetObject(typeof(IClientsManager));
ICollection clients = manager.GetListOfActiveClients();
foreach (long id in clients)
AddClient(id);
}
}
}
-----------------------------------------------------------
I will now explain the class fully (It is quite long).
First of all we have the ActiveClients property. This property is the whole purpose of this class, right?
The class has 3 events: ClientAdded, ClientRemoved, and ClientsCleared (less important). The ClientEventHandler is defined within the scopr of this class, and the ClientEventArgs as well.
The class uses three events:
1. The SystemMessageArrived (See step 2) of MessageBroadcaster. The event indicate that a client connected or disconnected. In both cases we check that the event is not raised by chance by the client itself. After that we add the client Id or remove it from _activeClients list. Then we raise the ClientAdded or ClientRemoved event.
2. ServerReconnect of ClientConnector. In this case we refresh the list of clients by calling GetListOfActiveClients (See step 3), and adding each client to _activeClients.
3. ServerDisconnected of ClientConnector. We clear the clients list, and raise the ClientsCleared event.

Next, we need to add an instance of the ConnectedClients class to ClientStartup class (same as we did for ClientConnector). Change the ClientStartup class as follows:
-----------------------------------------------------

using System;
using System.Runtime.Remoting;
using System.Windows.Forms;
namespace Client
{
public class ClientStartup
{
private static ClientConnector _connector;
public static ClientConnector Connector
{
get
{ return _connector; }
}
private static ConnectedClients _clients;
public static ConnectedClients ConnectedClients
{
get { return _clients; }
}
[STAThread]
static void Main()
{
InitRemoting();

_connector = new ClientConnector();
_clients = new ConnectedClients();
Application.Run(new ClientForm());
}
private static void InitRemoting()
{
string fileName = "client.exe.config";
RemotingConfiguration.Configure(fileName);
}
}
}
-----------------------------------------
Step 6 - Add a Client Id to the Message
We can now add to the Message the property SenderId in order to know which client sent the message (we did not know this information before). For the special case of the Server sending messages, we will set the SERVER_ID to be -100 (arbitrarily selected).
Change the Message class (in Common assembly) as follows:
-------------------------------------------------------

[Serializable()]
public class Message
{
public enum MessageType
{
Custom,
System
}
public string _msg;
protected long _senderId;
public Message(long senderId, string msg)
{
this._msg = msg;
this._senderId = senderId;
}
public string Text
{
get { return _msg; }
}
public virtual MessageType Type
{
get { return MessageType.Custom;}
}
public static long SERVER_ID
{
get { return -100; }
}
public string TextWithSenderId
{
get
{
if (!IsSenderServer)
return "Client " + SenderId.ToString() + ": " + Text;
else
return "Server: " + Text;
}
}
public virtual long SenderId
{
get { return _senderId; }
}
public bool IsSenderServer
{
get { return _senderId == SERVER_ID; }
}
}
[Serializable()]
public class SystemMessage:Message
{
private SystemMessageType _type;
private long _clientId;
public enum SystemMessageType
{
ClientConnected,
ClientDisconnected
}
public SystemMessage(SystemMessageType type, long clientId, string message):
base(SERVER_ID, message)
{
_type = type;
_clientId = clientId;
}
public override Common.Message.MessageType Type
{
get { return MessageType.System; }
}
public SystemMessageType SystemType
{
get { return _type; }
}
public long ClientId
{
get { return _clientId; }
}
}
------------------------------------------------------
Notice the TextWithSenderId method, which formats the message with the identity of the sender.

Demo
Lets test things. We will modify both ClientForm and ServerForm.

ServerForm:
So far we had a ListBox for messages. We will add another ListBox for the clients list.
This is the code (without theauto-generated code):
------------------------------------------------------

public class ServerForm : Form
{
private ListBox lsbMessages;
private Button btnSend;
private ListBox lsbConnectedClients;

private Container components = null;
public ServerForm()
{
InitializeComponent();
_bcast = (Broadcaster) RemoteSingletonObjectsList.Instance. GetRemoteSingletonObject( typeof(Broadcaster));
_bcast.MessageArrived += new MessageArrivedHandler(bcast_MessageArrived);
}

(...)

private Broadcaster _bcast = null;
public void bcast_MessageArrived(Message msg)
{
if (msg.Type == Message.MessageType.Custom)
{
_msgToShow = msg;
Thread t = new Thread(new ThreadStart(ShowMessage));
t.Start();
}
else
{
HandleSystemMessage((SystemMessage)msg);
}
}
private void HandleSystemMessage (SystemMessage systemMessage)
{
switch (systemMessage.SystemType)
{
case SystemMessage.SystemMessageType. ClientConnected:
lsbConnectedClients.Items.Add("Client " + systemMessage.ClientId);
break;
case SystemMessage.SystemMessageType. ClientDisconnected:
lsbConnectedClients.Items.Remove("Client " + systemMessage.ClientId);
break;
}
}
private Message _msgToShow=null;
private void ShowMessage()
{
if (_msgToShow == null) return;
lsbMessages.Items.Add(_msgToShow.TextWithSenderId);
lsbMessages.SelectedIndex = lsbMessages.Items.Count -1;
_msgToShow = null;
}
private long index=0;
private void btnSend_Click(object sender, System.EventArgs e)
{
_bcast.BroadcastMessage(new Message(Message.SERVER_ID, "Server: This is message #" + (index++).ToString()) );
}
-----------------------------------------------------

ClientForm:
Again, we add another ListBox to show the list of active clients. Here is the code (without the auto-generated code):
----------------------------------------------------------

public class ClientForm : System.Windows.Forms.Form
{
private System.Windows.Forms.ListBox lsbMessages;
private System.Windows.Forms.Button btnSendMessage;
private System.Windows.Forms.ListBox lsbClients;
private System.ComponentModel.Container components = null;
public ClientForm()
{
InitializeComponent();
_messageBroadcaster.MessageArrived += new Common.MessageArrivedHandler(broadcaster_MessageArrived);
ClientStartup.ConnectedClients.ClientAdded += new Client.ConnectedClients.ClientEventHandler (ConnectedClients_ClientAdded);
ClientStartup.ConnectedClients.ClientRemoved += new Client.ConnectedClients.ClientEventHandler (ConnectedClients_ClientRemoved);
ClientStartup.ConnectedClients.ClientsCleared += new EventHandler(ConnectedClients_ClientsCleared);
ClientStartup.Connector.ClientIdChanged += new EventHandler (Connector_ClientIdChanged);
ClientStartup.Connector.ServerDisconnected += new EventHandler(Connector_ServerDisconnected);
ClientStartup.Connector.ServerReconnect += new EventHandler (Connector_ServerReconnect);
if (ClientStartup.Connector.IsClientIdValid)

{
this.Text = "Client " + ClientStartup.Connector.ClientId;

ClientStartup.ConnectedClients.RefreshList();
}
else
this.Text = "Client Not Connected";
}

(...)

MessageBroadcaster _messageBroadcaster = new MessageBroadcaster();
private long _index=0;
private void btnSendMessage_Click (object sender, System.EventArgs e)
{
Thread t = new Thread(new ThreadStart(SendMessage));
t.Start();
}
private void SendMessage()
{
try
{
Message msg = new Message (ClientStartup.Connector.ClientId, "This is message #" + (_index++).ToString());
_messageBroadcaster.BroadcastMessage(msg);
msg = null;
}
catch (ServerNotAliveException ex)
{
lsbMessages.Items.Add(ex.Message);
lsbMessages.SelectedIndex = lsbMessages.Items.Count-1;
}
}
private void broadcaster_MessageArrived(Message msg)
{
lsbMessages.Items.Add(msg.TextWithSenderId);
lsbMessages.SelectedIndex = lsbMessages.Items.Count-1;
}
private void Connector_ServerDisconnected(object sender, EventArgs e)
{
this.Text = "Client Not Connected";
}
private void Connector_ServerReconnect(object sender, EventArgs e)
{
this.Text = "Client " + ClientStartup.Connector.ClientId;
}
private void Connector_ClientIdChanged(object sender, EventArgs e)
{
this.Text = "Client " + ClientStartup.Connector.ClientId;
}
private void ConnectedClients_ClientAdded(object sender, Client.ConnectedClients.ClientEventArgs e)
{
lsbClients.Items.Add("Client " + e.ClientId.ToString());
}
private void ConnectedClients_ClientRemoved(object sender, Client.ConnectedClients.ClientEventArgs e)
{
lsbClients.Items.Remove("Client " + e.ClientId.ToString());
}
private void ConnectedClients_ClientsCleared(object sender, EventArgs e)
{
lsbClients.Items.Clear();
}
}
----------------------------------------------------------

Run the server and few instances of the client. See the Ids of the clients in the header of the form, and the lists update both in the server and the clients form. Try to disconnect the server and the client. Try to send messages.

Summary
This was a tutorial for a fully functional server-client application using Remoting.NET technology. At this point you have a generic Broadcaster for events, a reconnections mechanism, and a list of connected clients. I don't think that I have ever seen such a tutorial for Remoting over the net (you are welcome to correct me on this).

The next step will be the most impressive (in my opinion): a Multicaster class, which will enable distributing events to selected clients instead of all the clients.

Comments
Two things I don't like about the code I have posted. The first is the use of inheritance to create a SystemMessage (you saw at step 2 how much work was involved, and how many layers were effected). The second is the use of long Id instead of a class to represent the Client. It should not be hard to fix the second problem.

I will finish my (very long) article by saying that this was a very complex code to design, program and document. It works as far as I can tell, but if you see a bug, or a better way to do something, you are more than welcome to comment. Any bugs I will try to fix.

Monday, January 09, 2006

DegreesUpDown Control

Introduction
The code I will post here is for a custom Control called DegreesUpDown. The control is similar to a NumericUpDown only it shows degrees (from 0 to 359). The nice thing about it is that the degrees are shown in 3 digits (000, 050, 180, etc...).

The Code
You might have thought that the control inherits from NumericUpDown. Well, it does not. It inherits from DomainUpDown which is much more flexible. I cannot format the way numbers look in NumericUpDown, so the trick is to convert strings into numeric values.

------------------------------------------------
Imports System.ComponentModel
Imports CommonControls.CGraphicsCommon
Imports System.Drawing

Public Class DegreesUpDown
   Inherits System.Windows.Forms.DomainUpDown

#Region " Windows Form Designer generated code "
Public Sub New()
   MyBase.New()
'This call is required by the Windows Form Designer.
   InitializeComponent()
   InitializeGraphics()
End Sub

'UserControl overrides dispose to clean up the component list.
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
   If disposing Then
      If Not (components Is Nothing) Then
         components.Dispose()
      End If
   End If
   MyBase.Dispose(disposing)
End Sub

'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()> _
Private Sub InitializeComponent()
   '
   'DegreesUpDown
   '
   Me.Wrap = True
End Sub
#End Region

Private Sub InitializeGraphics()
   Me.Font = CommonFont
   Me.BackColor = Color.FromArgb(238, 238, 238)
   Me.BorderStyle = BorderStyle.FixedSingle
End Sub

Private Sub InitValues()
   Me.Items.Clear()
   Dim vals As ArrayList = New ArrayList(360)
   For i As Integer = 359 To 0 Step -1
      vals.Add(ConvertTo3Digits(i))
   Next
   Me.Items.AddRange(vals)
End Sub

Public Shared Function ConvertTo3Digits(ByVal i As Integer) As String
   If (i >= 100) Then Return i.ToString()
   If (i >= 10) Then Return "0" + i.ToString()
   If (i >= 0) Then Return "00" + i.ToString()
   Return "000"
End Function

<Browsable(False)> _
Public Shadows ReadOnly Property Items() As DomainUpDownItemCollection
   Get
      Return MyBase.Items
   End Get
End Property

<Browsable(False), _
Bindable(True)> _
Public Property Value() As Long
   Get
      If (IsNothing(Me.SelectedItem)) Then Return 0
      Return Long.Parse(Me.SelectedItem)
   End Get
   Set(ByVal Value As Long)
      If IsNothing(Value) Then
         Me.SelectedItem = "000"
         RaiseEvent ValueChanged(Me, EventArgs.Empty)
         Exit Property
      End If
      If (Value >= 360 Or Value < 0) Then
         Me.Text = Value.ToString
         FixHeading()
      Else
         Me.SelectedItem = ConvertTo3Digits(Value)
      End If
      RaiseEvent ValueChanged(Me, EventArgs.Empty)
   End Set
End Property

<Category("Action"), _
Description("Raised when the value is changed")> _
Public Event ValueChanged As EventHandler

Private Sub DegreesUpDown_SelectedItemChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.SelectedItemChanged
   RaiseEvent ValueChanged(Me, EventArgs.Empty)
End Sub

Protected Overrides Sub OnCreateControl()
   Me.Items.Clear()
   If Not DesignMode Then
      Dim vals As ArrayList = New ArrayList(360)
      For i As Integer = 359 To 0 Step -1
         vals.Add(ConvertTo3Digits(i))
      Next
      Me.Items.AddRange(vals)
      Me.SelectedIndex = 359
   Else
      Me.Text = "000"
   End If
End Sub

Private Sub DegreesUpDown_Leave(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Leave
   FixHeading()
End Sub

Private Sub FixHeading()
   Try
      Dim i As Integer = Integer.Parse(Me.Text)
      If i < 0 Then
         Me.SelectedIndex = ((-i) Mod 360) - 1
      End If
      If i > 359 Then
         Me.SelectedIndex = 359 - (i Mod 360)
      End If
      If i < 359 And i > 0 Then _
         Me.SelectedIndex = 359 - i
   Catch ex As Exception
      Me.SelectedIndex = 359
   End Try
End Sub

Protected Overrides Sub UpdateEditText()
Try
   Dim i As Integer = Integer.Parse(Me.Text)
   If (i > 359 Or i < 0) Then
      FixHeading()
   Else
      If Me.Text.Length <> 3 Then _
         FixHeading()
   End If
   Catch ex As Exception
      FixHeading()
   Finally
      MyBase.UpdateEditText()
   End Try
End Sub

<Browsable(False)> _
Public Shadows Property Text() As String
   Get
      Return MyBase.Text
   End Get
   Set(ByVal Value As String)
      MyBase.Text = Value
   End Set
End Property

End Class
------------------------------------------------

Explanations
On initialization, in method InitValues, I create 360 strings (000,001,...,359), and add them to the items collection of the DomainUpDown base. The method ConvertTo3Digits does the convertion from int to 3 digits string.
In order to make the control fill more like a NumericUpDown, I added a Value property, which is numeric. Convertion to string and vice versa is done. The event ValueChanged also emulate the corresponding event of NumericUpDown control.
The Items and Text properties is no longer needed for use, so they are shadowed and marked as Browsable false.

FixHeading method is a nice feature - when the user enters a number out of range 0..359, the control automatically convert the number to the appropriate degree (For example: -90 => 270).

Extensions
You can extend the control by adding an Increment property (pressing on the UpDown buttons as of now only increase or decrease by one).
Also, you can add Maximum and Minimum properties.
In addition, the range -180..180 can be useful.

Sunday, January 08, 2006

Print Screen And Save It to File

Introduction
A task which is quite common is to press the Print Screen button, and save the screen in Paint application as an Image file.
If you ever wondered how to perform such a task in .NET, here is the solution.

Solution
The following snippet of code does the trick:
------------------------------------------
SendKeys.Send("+{PRTSC}");
if (Clipboard.GetDataObject() != null)
{
   IDataObject data = Clipboard.GetDataObject();
   if (data.GetDataPresent(DataFormats.Bitmap))
   {
      Image image = (Image)data.GetData(DataFormats.Bitmap,true);
      image.Save("image.jpg",ImageFormat.Jpeg); // pick your format and path
   }
}
------------------------------------------

First we use the Sendkeys class. This class is not very famous, but it is quite nice. Whenever you want to emulate a key press you can call the Send method with the key you want to press. In our case we don't want just a key - we want to press the Print Screen key combined with the Ctrl key. "+" stands for Ctrl, and "PRTSC" stands for the Print Screen key.

From here on its all about the Clipboard. A press on Print Screen actually copies the screen image as it is into the clipboard. Instead of pasting it in Paint and saving it, we will use the Clipboard class. GetDataObject is the method to get the object currently in the clipboard. Unfortunately, we only get an IDataObject interface, without any knowledge of the actual type. So we call GetDataPresent to varify that the object is indeed an image. Once we are there, we call the GetData method which finally reveals the Image object. The Save method will store the image in any format we like and in any location.

Saturday, January 07, 2006

Using JoinView to Show Queries From Multiple Tables

Introduction
In MS-Access there is the possibility to show queries from two tables. Take for example the case of Order and Product tables in the Northwind database. Say we want to show in a grid the amount of items purchased (per Order) with the appropriate product name and price (from the Product table). We are able to do so if there is a relation between the tables, and by using SQL Join clause.
This effect is not available in ADO.NET. Once the data is filled into the DataSet, there is no built-in way to produce this kind of query. Sure, we can show a Master-Detail view of the data. But in order to show data from several tables in one DataGrid at once we will need to work hard. DataGrid can only show data from one DataTable at a time, and DataView can only reflect data of one table.
This is a very big problem. One way of solving it is getting the data into a DataSet from the database itself using a Select Command with Join clauses. The DataSet should have a DataTable with the same schema as the resulting Join query. While this is possible in some cases, other cases require some kind of mechanism to have a joined view without accessing the database.
For example, say I have a server and clients. The server does all the access to the database. The client gets the DataSets from the server. If I want to show the data in a DataGrid in the client, I cannot afford to approach the server again. Not only this is expensive, but it can also cause concurrency problems. I have all the data I need in memory, so why do I need to access the database again?

Solution
Microsoft comes to our aid. Obviously someone understood the problem, so he or she developed a class called JoinView which acts like a DataView class, but handles data with multiple DataTables with DataRelations set between them.
The code for the class (VB.NET), with examples of usage can be found here: http://support.microsoft.com/kb/325682/EN-US/
C# source code can be found here: http://www.valley.ru/~dmoiseev/q325682_csh.html

This class is the ONLY convenient way I could find to show the data from few related tables in one table without getting the data in this structure in the first place.

Disadvantages
Unfortunately, the JoinView class is not as convenient as the DataView class. Here are few of the drawbacks I could find when using this class:
1. You cannot edit the related columns - only the main table columns. The rest are read-only and I couldn't get passed that.
2. You cannot dynamically filter the rows!!! you will need to create a new JoinView whenever the filter is changed. This is a huge drawback, as instanciating a JoinView is probably expensive.
3. Apparently the AllowNew property works well if you you set it BEFORE you assign the JoinView as a DataSource to the grid. If you set the AllowNew property to False after you assign the DataSource, the result is unexpected (I got the Add New Row displayed, and when the user pressed on this line, an exception was thrown, saying: "AddNew is not allowed").
4. It works with column styles, but i needed to make some little changes.

Summary
Basically it was a good experience working with JoinView, although it is not perfect. As I said before, it is currently the only solution i know of to work with Join without getting the data from the database in that structure.

Some Thoughts
Why has Microsoft not provided us a proper way of doing a Join on the DataSet objects in-the-box? We have DataTables and DataRelations in .NET so the JoinView class seems to be a required piece. More importantly - someone realized the need, otherwise the JoinView class would not have been published - so why it is not provided in .NET 2005?

Why can't we use Command objects on DataSets? By allowing us to use SQL commands on DataSets we will have more strength in using DataSets.

Thursday, January 05, 2006

Setting the Row Height of the Grid to Fit the Contents

Intorduction
It is very hard to set the row height of the DataGrid (Framework 1.1). Actually I was very surprised that it is not a built-in feature. Not only it is not built-in, but also it is a very non-trivial task.

Suppose I have a row with text containing the \n\r characters (meaning new line). In this case I need the row to have multiple lines. Try and put a text with this characters in a normal DataGrid - only the first row will appear, and the other lines can be reached by using the up and down arrw keys. I will show a way of showing all the lines.

The source code in this article was inspired from this article in CodeProject.

The Solution
If you followed my previous articles in my blog, you can guess that the solution involves ColumnStyles. You will need to create your own custom DataGridColumnStyle and use it in order to populate the grid. A reminder of ColumnStyles can be found here.

First, I implement a class called MultiLineColumnStyle derived from DataGridTextBoxColumn. This class has a property MultiLine which when set to true will show all the lines in the cell.
The trick of resizing the height of the row is in the GetMinimumHeight overrided method.
Here is the code
------------------------------------------------------------
public class MultiLineColumnStyle:DataGridTextBoxColumn
{
   public bool MultiLine
   {
      get
      { return _multiLine; }
      set
      { _multiLine = value; }
   }
   private int _currentCell = 0;
   private bool _multiLine = false;

   public void ResetIterations()
   {
      _currentCell = 0;
   }
   protected override int GetMinimumHeight()
   {
      if (!_multiLine)
         return base.GetMinimumHeight();
      // Get CurrencyManager
      CurrencyManager cur = (CurrencyManager)this.DataGridTableStyle.DataGrid.BindingContext[this.DataGridTableStyle.DataGrid.DataSource, this.DataGridTableStyle.DataGrid.DataMember];
      // Rows available?
      if(cur == null cur.Count == 0)
         return base.GetMinimumHeight();
      // Increment counter
      this._currentCell ++;
      // Initialize return value
      int retVal = base.GetMinimumHeight();
      // Calculate height of row at currentIteration 1
      retVal = this.CalcStringHeight(GetColumnValueAtRow(cur,currentCell - 1).ToString());
// Reset when last cell reached
      if(_currentCell == cur.Count)
         this.ResetIterations(); // sets currentIteration to 0
      return retVal;
   }
   private int CalcStringHeight(string s)
   {
      try
      {
         // Create graphics for calculation
         System.Drawing.Graphics g = this.TextBox.CreateGraphics();
         // Do measure, and add a bit (4 pixels for me)
         return (int)g.MeasureString(s,this.TextBox.Font).Height + 4;
      }
      catch
      {
         // Error, return default font height.
         return base.GetMinimumHeight();
      }
   }
}
------------------------------------------------------------
The GetMinimumHeight is called for each cell seperately starting with the topmost cell in the column. So the _currentCell variable is used to run through the cells - each call will increment this (thanks to the above website for the great idea).
Obtaining the CurrencyManager (to get the text of the cell) of the column is another problem, which is solved in a very long and not straight-forward way.
One last problem is the actual calculation of the height of the row which is done in the CalcStringHeight method - by using MeasureString method.

Indeed, very hard work!!!

Demo
Add to the main form of the application a DataGrid. Here is the Form.Load event handler:
---------------------------------------------------
private void Form1_Load(object sender, System.EventArgs e)
{
   DataTable dt = new DataTable("MyTable");
   dt.Columns.Add(new DataColumn("multiline",typeof(string)) );
   dataGrid1.DataSource = dt;
   DataRow dr;
   dr = dt.NewRow();
   dr["multiline"] = "vsvsdfsdf\n\rdfgdfgsd\n\rcvsdvgsf";
   dt.Rows.Add(dr);
   dr = dt.NewRow();
   dr["multiline"] = "gsddg\n\rasfsfswef";
   dt.Rows.Add(dr);

   DataGridTableStyle ts = new DataGridTableStyle();
   ts.MappingName = "MyTable";
   MultiLineColumnStyle cs = new MultiLineColumnStyle();
   
cs.MappingName = "multiline";
   cs.HeaderText = "multiline";
   cs.MultiLine = true;
   
ts.GridColumnStyles.Add(cs);
   dataGrid1.TableStyles.Add(ts);
}
-------------------------------------------------
Run the application, and you will see that all the rows have appropriate height.

Wednesday, January 04, 2006

Loading and Validating Xml Files

Source code in VB (with demo) can be downloaded from here (~35 KB)

Introduction
An Xml should be Well-Formed:
- must begin with the XML declaration
- must have one unique root element
- all start tags must match end-tags
- XML tags are case sensitive
- all elements must be closed
- all elements must be properly nested
- all attribute values must be quoted
- XML entities must be used for special characters

However, being Well-Formed is not enough. There can still be errors in the Xml. Those errors are in the logical aspects, rather than the syntax. Xml Schemas help us define the business rules of our Xmls. Xml Schemas are written in Xml and they support Data Types.

It is a good practice to use Xml Schemas always. Don't write just Xmls. Use schemas as well!!!

Xml and Xml Schemas In .NET
As you probably know there is a wide support for Xml in the .NET . The class XmlDocument is the DOM implementation in the .NET Framework. This is a very strong class. However, there is no easy support for loading and validating the Xml against its Schema.

Validating Against the Schema
My friend, Dror Givoli, who works with me, supplied a code to a class, ValidatingXmlDocument, derived from XmlDocument, which has a method called LoadAndValidate, with tons of overloads. Here are few:
-----------------------------------------------------------
Public Overloads Sub LoadAndValidate(ByVal filename As String, ByVal Schema As XmlSchema)
MyBase.Load(filename)
Validate(Schema)
End Sub
Public Overloads Sub LoadAndValidate(ByVal inStream As Stream, ByVal Schema As XmlSchema)
MyBase.Load(inStream)
Validate(Schema)
End Sub
Public Overloads Sub LoadAndValidate(ByVal txtReader As TextReader, ByVal Schema As XmlSchema)
MyBase.Load(txtReader)
Validate(Schema)
End Sub
Public Overloads Sub LoadAndValidate(ByVal reader As XmlReader, ByVal Schema As XmlSchema)
MyBase.Load(reader)
Validate(Schema)
End Sub
Public Sub LoadXmlAndValidate(ByVal xml As String, ByVal Schema As XmlSchema)
MyBase.LoadXml(xml)
Validate(Schema)
End Sub
Public Overloads Sub LoadAndValidate(ByVal filename As String, ByVal schemaFileName As String)
Dim Schema As XmlSchema = XmlSchema.Read(New XmlTextReader(schemaFileName), AddressOf ValidationEventHandler)
MyBase.Load(filename)
Validate(Schema)
End Sub

----------------------------------------------------------
There are more. The Validate method is as follows:
----------------------------------------------------
Public Sub Validate(ByVal Schema As XmlSchema)
Dim ms As New MemoryStream
Me.Save(ms)
ms.Position = 0
Dim rdr As New XmlTextReader(ms)
Dim vrdr As New XmlValidatingReader(rdr)
vrdr.Schemas.Add(Schema)
AddHandler vrdr.ValidationEventHandler, AddressOf ValidationEventHandler
While vrdr.Read
End While
End Sub


Private Sub ValidationEventHandler(ByVal sender As Object, ByVal args As ValidationEventArgs)
Throw New XmlValidationException(args)
End Sub

--------------------------------------------------
Last there is an implementations of the method SaveAndValidate, again with tons of overloads:
--------------------------------------------------
Public Overloads Sub SaveAndValidate(ByVal filename As String, ByVal Schema As XmlSchema)
Validate(Schema)
Save(filename)
End Sub
Public Overloads Sub SaveAndValidate(ByVal outStream As System.IO.Stream, ByVal Schema As XmlSchema)
Validate(Schema)
Save(outStream)
End Sub
Public Overloads Sub SaveAndValidate(ByVal writer As System.IO.TextWriter, ByVal Schema As XmlSchema)
Validate(Schema)
Save(writer)
End Sub

-----------------------------------------
And there are more overloads.

Demo
Here is an example of how to use this class:
------------------------------------------
Dim vDoc As New ValidatingXmlDocument
Dim xmlFile As String = "c:\MyXmlFile.xml"
Dim schemaFile As String = "c:\MySchemaFile.xsd"
Try
vDoc.LoadAndValidate(xmlFile, schemaFile)
Catch ex As XmlValidationException
Console.Out.WriteLine(ex.Args.Message)
End Try

------------------------------------------

Acknowledgment
This code was given to me by Dror Givoli, in order to post it in my blog. Thanks Dror for sharing your knowledge with others.

Feel free to use everything here. Add links to my site if you wish.

Do not copy anything to other sites without adding link to here.

All the contents of the site belong to Yariv Hammer.