Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Simple 2 User Whiteboard application in C#/.NET

0.00/5 (No votes)
17 Jul 2002 1  
This article is my attempt at developing an extremely simple Whiteboard application in C#/ WinForms and sockets for messaging, to be used simultaneously by 2 users.

Introduction

>This article is my attempt at developing an extremely simple whiteboard application to be used simultaneously by two users. Although I started my attempt with support for more than two users, I dropped it to two to reduce the complexity of the project.

Here are some of the features this whiteboard supports:

  • It allows two users to connect to each other, with either one running as a listener with a valid IP on a valid port, and the other one running as a client that connects to the listener�s IP/Port.
  • It allows 4 drawing primitives, namely scribbling via a pencil tool, drawing rectangles with a rubberbanding like effect in Paintbrush, drawing ellipses, clearing the screen.

System Requirements

To compile the solution you need to have Microsoft Visual Studio .NET installed. To run any of the client executable you need to have the .NET framework installed.

Here is a snapshot that allows the client and server both running on the same machine, but make sure the correct IP is specified in the "ConnectTo" box.

Starting the application:

To start the application, do the following:

  • Fire up an instance of the application on one machine.

  • Select the mode this instance would run in, i.e. client mode or server mode. To make it run as a server listener, select the "Start as Listener" option as shown and accept the default 8888 as the listening port, unless you have another application on your system listening on it, then click 'Start listening'

    Note: Unfortunately this application is unable to accept requests from another Whiteboard client if it's behind a firewall, to make it work you need to poke a hole in the firewall for port 8888.(eeeeks !)

  • Fire up another instance of the Whiteboard application on another machine. This time select the Connect to option as shown:

  • The ConnectedPeers title on the top right would add a node with the peer's IP.

A user drawing on one end is reflected on the other, i.e. shown below at the listener end.

Sorry, Chris, that's the best I could get with my limited drawing skills as well as the limited no of drawing tools provided in this app.

Explaining the implementation

The following diagram illustrates the internal architecture of the app.

The application consists of a WinForms based client which contains a couple of UI controls for user input, like textboxes for IP address/port and treeview control, toolbar, buttons, radio buttons, etc. It contains a custom control which takes care of all the drawing related code, see DrawAreaCtrl.cs. To persist the scribbled data on the whiteboard, the custom control has code that makes EXACT changes as that made on the whiteboard drawing area on a Bitmap object. This ensures that the changes made aren�t lost.

protected override void OnPaint(PaintEventArgs e)
{
    Graphics g = e.Graphics;
    g.DrawImage(m_bmpSaved, 0,0,m_bmpSaved.Width, m_bmpSaved.Height);
}

The UI has two modes of operation:

  • Server mode (Listener)
  • Client mode (Connects to a listener)

If you select the first mode as shown above the application starts listening on the machine�s IP on the port specified as shown in the following snippet.

    m_enNetMgrMode = NETWORK_MANAGER_MODE.enServerMode;
    string StrHostName        = Dns.GetHostName();
    m_SockListener            = new Socket(0, SocketType.Stream, ProtocolType.Tcp);
    IPHostEntry IpEntry        = Dns.GetHostByName (StrHostName);
    IPAddress [] IpAddress    = IpEntry.AddressList; 
    IPEndPoint LocEndpoint    = new IPEndPoint(IpAddress[0], m_iPort);
    m_SockListener.Bind(LocEndpoint);
    m_SockListener.Blocking = true;
    m_SockListener.Listen(-1);
    m_SockListener.BeginAccept(m_AsyncCallbackAccept, m_SockListener);

If you select the second option AFTER SPECIFYING the correct IP/Port of another listening whiteboard, the app connects to the server as shown.

    IPAddress    hostadd    = Dns.Resolve(StrHostIp).AddressList[0];
    int            iHostPort = Convert.ToInt32(StrHostPort);
    IPEndPoint    EPhost    = new IPEndPoint(hostadd,iHostPort);

    m_StrHostIp = StrHostIp;
    m_StrHostPort = StrHostPort; 
    m_SockServer = new Socket(
        AddressFamily.InterNetwork, 
        SocketType.Stream,
        ProtocolType.Tcp 
        );
    try
    {
        m_SockServer.Connect(EPhost);
        m_Creator.EnableDisableConnectModeControls(true);
        string msg = "Connected to remote Whiteboard on: " + StrHostIp ;
        msg += " listening on port: " + StrHostPort;
        m_Creator.SetMusicalStatus(msg, "ding.wav");
        m_HndlrForListeningWB = new ClientHandler(m_SockServer, m_Creator);
        m_HndlrForListeningWB.ProcessClientRequest();
            
    }
    catch(NullReferenceException exception)
    {
        m_Creator.SetStatus ("Connect failed. " + exception.Message);
        m_SockServer = null;
    }

Messaging architecture

Once the socket is established between the two users, each peer communicates with the other peer through something called WhiteBoard Messages.(see WhiteBoardMessages.cs). This file contains an abstract class called WBMessage that every other message like WBMsgDrawBegin, WBMsgDrawEnd, WBMsgDrawLine, WBMsgDrawRectangle and WBMsgDrawEllipse inherits from.

For example:

[Serializable]
public class WBMsgDrawBegin :  WBMessage 
{
    //Implement the abstract property 

    private const WHITEBOARD_MESSAGE_TYPE m_enMsgType = 
                                  WHITEBOARD_MESSAGE_TYPE.enWBBegin;
    public override WHITEBOARD_MESSAGE_TYPE MessageType
    {
        get
        {
            return m_enMsgType;
        }
    }
    public Point m_PtBegin;
    public bool  m_bMouseDown;
}

Notice the Serializable attribute before each of these classes. The static methods of the class WBMessageHelper (Serialize, Deserialize, DeserializeL) are the heart of the message encoding/decoding infrastructure of the application.

.NET provides object serialization support through the use of Formatter classes like the BinaryFormatter and SoapFormatter.

This application keeps transporting mouse messages (mousedown, mousemove and mouseup coordinates) from one user to another remote user. Using the SoapFormatter would have meant transporting a LOT more data (since SOAP is an XML based serialization mechanism) than in a Binary Format.

Without BinaryFormatters I could have sent raw structs with longs, ints and floats within them and manipulated byte pointers in unsafe code. But I decided to use what .NET already provides without delving into the unsafe world.

BinaryFormatters serializes and deserializes an object, or an entire graph of connected objects, in binary format. For example, the following function serializes a long into a memory stream object whose raw buffer contents could be passed on the wire through the opened socket and then unpacked with a similar Deserialize routine:

public static MemoryStream Serialize(long lObject)
{
    MemoryStream ms            = new MemoryStream(); 
    BinaryFormatter formatter    = new BinaryFormatter();
    formatter.AssemblyFormat    = FormatterAssemblyStyle.Simple;
    formatter.TypeFormat        = FormatterTypeStyle.TypesWhenNeeded;
    formatter.Serialize(ms, lObject);
    return ms;
}

The weird thing about BinaryFormatters is that even a single long of 8 bytes when serialized into a memory stream takes up 56 bytes of buffer space, which is seven times bigger than just sending a raw long of 8 bytes.

Lets take a use case on how the Scribble line tool works (assuming 2 peers connected to each other with one as listener and other connecting to it) When someone clicks on the scribble button on the toolbar.

The DrawAreaCtrl custom control shifts to the WHITEBOARD_DRAW_MODE of enModeLine

Once this draw mode is enabled, all mouse events are interpreted for drawing related to scribbling lines.

MouseDown

On a mousedown an instance of the serializable class WBMsgDrawBegin is created. This class is then serialized to a byte buffer using the BinaryFormatter class and sent across the socket. But every serialized object's buffer is preceded with a serialized long that specifies the length of the buffer that is going to follow it. This way the client on the other end knows how much data to parse and deserialize.

public void SendWBMessage(WBMessage msg)
{
    if(m_NS != null)
    {
        MemoryStream ms = WBMessageHelper.Serialize(msg);
        MemoryStream msLength = WBMessageHelper.Serialize(ms.Length);
        m_NS.BeginWrite(msLength.GetBuffer(), 0, (int)msLength.Length, 
                        m_AsyncCallbackWrite, null);
        m_NS.BeginWrite(ms.GetBuffer(), 0, (int)ms.Length, m_AsyncCallbackWrite, 
                        null);
        ms.Close();
        msLength.Close();
    }        
}

These bytes, when sent across, look like this in memory:

// ------------------------------------------------------------

// | Data Length | Data ... | Data Length | Data ... | ...

// ------------------------------------------------------------

public void ParseData(byte [] ByteBuff, int iCbRead)
{
    int offset = 0;
    //long lLenData = 0;

    m_iByteCacheLen = 0;

    while(iCbRead >= m_iHdrSize)
    {
        if(m_bOnHeader)
        {
            m_iDataPackLen = (int)WBMessageHelper.DeserializeL(ByteBuff, offset, 
                                                               m_iHdrSize);
            offset += m_iHdrSize;
            m_bOnHeader = false;
            iCbRead -= m_iHdrSize;
        }
        //Parse the data 

        else
        {
            if(m_iDataPackLen == 0) return; 
            if(m_iDataPackLen > iCbRead)
            {
                Array.Clear(m_ByteCache, 0, 2048);
                Array.Copy(ByteBuff, offset, m_ByteCache, 0, iCbRead);
                m_iByteCacheLen = iCbRead;
                iCbRead -= iCbRead;
                //break;

            }
            else
            {
                //Complete Data packet, deserialize and RouteRequest

                WBMessage msg = WBMessageHelper.Deserialize(ByteBuff, offset, 
                                                            m_iDataPackLen);
                RouteRequest(msg);
                offset += m_iDataPackLen;
                m_iByteCacheLen = 0;
                iCbRead -= m_iDataPackLen;
                m_iDataPackLen = 0;
                m_bOnHeader = true;
            }
        }
    }
    //This is where we have surplus/incomplete data packets

    if(iCbRead > 0)
    {
        Array.Clear(m_ByteCache, 0, 2048);
        Array.Copy(ByteBuff, offset, m_ByteCache, 0, iCbRead);
        m_iByteCacheLen = iCbRead;
    }

}

>Some limitations, bugs and of course a disclaimer

  • Doesn�t support more than 2 clients (for now, but the code could be extended);
  • Concurrency checks for concurrent drawings not present, so behaviour unpredictable. Right now its implemented by disabling mouse input from the user over the whiteboard if another client is in the process of updating/drawing on it;
  • Pen size, brush type, and background color are all fixed for now, which could be easily incorporated through the UI and some additional messages;
  • Doesnt work with a firewall restricting access between the two clients. (hint: see personal firewall settings to poke a hole for port 8888 to make it to work);
  • Better mouse tracking required on the client;
  • I have assumed that a BinaryFormatter serializes a long data type to a memory stream object in the form of 56 byes of memory (try it yourself to see);
  • This is BY NO MEANS an article that explains the best software development practises or explains excellent C# coding skills. This is just a proof of concept application that I wanted to write out as soon as I can and see how much time it takes for me to develop.

References

  • Programming Microsoft Windows with C# - Charles Petzold.
  • MSDN

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here