Simple Chat Messenger – A Demo of a WCF Self-Hosted Service & Client “Tester” Windows Form Application Exchanging Messages

About

This project presents a simple Chat Message Service and Client Application demonstration. The Chat Service is a self-hosted (service host) WCF application launched and managed with a simple console interface. The service prints to the console window the user activities (log on, log off) and what messages are sent to users. The client “tester” has a simplified GUI user interface to quickly demo and test the service (Windows Form Application). The GUI has features to log on, log off, send a message, and receive messages from other users on the chat service.

Architecture

The demo project consists of these component topics:

  • Shared Class Library Project “SharedLibrary”
    • IChatService (Interface for Service)
    • IChatServiceCallback (Interface for Service Callback)
    • DuplicateUserFault (Class describing a WCF Exception as a Fault)
    • ChatMessage (Class Describing a Chat Message Object)
  • Service Class Library Project “ChatServiceLibrary”
    • ChatService (Code that Implements the Service Interface)
    • config (Configuration Reference for Service Host)
    • Reference to the Shared Class Library
  • WCF Service (Host) Application Project “ChatServiceHost”
    • Program (Starts, Manages, Stops the Service)
    • config (Configuration for Service Host)
    • Reference to the ChatServiceLibrary
  • Client “Tester to Service” Windows Form Application Project “Client”
    • Reference to the Shared Class Library
    • Main Form GUI User Interface
    • Form Code – Processes GUI User Interface

The service interface is defined not in the service application but in a Shared Library. This library defines the interface contracts for the chat message services (ex: Send Message) and is referenced by both the client and service host projects.  The SharedLibrary also has a class definition that defines a chat message object with a name, timestamp, and message properties. Furthermore, a callback contract is defined for the client to implement so they can receive chat messages while connected to the service.  Lastly, since exceptions (errors) are really faults in WCF services, a custom error (fault) class is defined in the shared library so that the client can understand and process the duplicate user faults (errors).

The ChatServiceLibrary implements the Chat Message service and contracts as defined in the SharedLibrary. The ChatServiceHost is a simple console application that is responsible for starting the Chat service, hosting, and managing the service (self-hosted).

The service behaviors were designed to allow all the clients to connect to a single instance of the service that sends messages to all connected clients. Further improvements and features such as thread locks should have been implemented to safeguard the shared resources, however that was not the purpose of this short demo application as I budgeted limited time for this project.

A client “tester” windows form application tests the service and provides output to the user in a simple GUI.

Shared Class Library

A Class Library project (SharedLibrary) was added to my Visual Studio solution. This library is shared amongst the client and the service. The code is available on GitHub [here].

IChatService (Interface for Service)

The ServiceContract for the Chat Message service contains the following operation contracts.

  • void Login(string userName);
    • Logins the user and client and also registers a callback to this user/client to receive the messages
  • void Logoff(string userName);
    • Logs off the user/client from the service and unregisters it to receive callbacks
  • bool LogInState(string userName);
    • Returns the loggedin/connection state of the user
  • void SendMessage(string userName, string message);
    • After the client sends a message to the service it EXPECTS a callback from this operation with the ChatMessage object that is created on the Service from it’s request.
  • List<ChatMessage> GetMessageHistory();
    • Returns the Message History limited to Last X (defined) Chat Messages. This is stored as a Queue on the Service.

Discussion

The Login method implements a Fault (Exception in WCF) contract for the client to be able to receive errors if it tries to log in with a duplicate user.

The SendMessage method implements bi-directional communication with its attribute (IsOneWay = false). After the client sends a message to the service it expects a callback from this operation with the ChatMessage object that is created on the Service from its request. This hinges on the Service Contract being configured to handle callbacks with a specific interface of instructions (ex: [ServiceContract(CallbackContract = typeof(IChatServiceCallback))]).

The code is available on GitHub [here].

    /// <summary>
    /// IChatService
    /// Interface definition for the Chat Service
    /// There is a callback contract to the client for one
    /// service operation (see notes and the callback 
    /// interface definition)
    /// </summary>
    [ServiceContract(CallbackContract = typeof(IChatServiceCallback))]
    public interface IChatService
    {
        /// <summary>
        /// Login
        /// Logins the user and client and also registers a callback
        /// to this user/client to receive the messages
        /// </summary>
        /// <param name="userName">the user name to login (string)</param>
        [OperationContract]
        [FaultContract(typeof(DuplicateUserFault))]
        void Login(string userName);

        /// <summary>
        /// Logoff
        /// Logs off the user/client from the service and unregisters
        /// it to receive callbacks
        /// </summary>
        /// <param name="userName">the user name to log off (string)</param>
        [OperationContract]
        void Logoff(string userName);

        /// <summary>
        /// LogInState
        /// Returns the loggedin/connection state of the user
        /// </summary>
        /// <param name="userName">string user name</param>
        /// <returns></returns>
        [OperationContract]
        bool LogInState(string userName);

        // After the client sends a message to the service it EXPECTS
        // a callback from this operation with the ChatMessage object that is
        // created on the Service from it's request. 
        [OperationContract(IsOneWay = false)]
        void SendMessage(string userName, string message);

        /// <summary>
        /// GetMessageHistory
        /// Returns the chat message history as a list
        /// Limited to Last X (defined on the service) Chat Messages
        /// This is stored as a Queue on the Service
        /// </summary>
        /// <param name="messages">a list (string) of formatted messages</param>
        [OperationContract]
        List<ChatMessage> GetMessageHistory();

    } // end of interface

IChatServiceCallback (Interface for Service Callback)

The ServiceContract for the Chat Message service contains only one callback to the client. Whenever a client is connected to the service, it will receive callbacks from the service with the recent chat messages happening at that instant in time. The client is required to implement the service callback interface in order to receive the instant chat messages. In addition, the operation behavior is set to one-way so that the service does not wait on a response from the client, which could cause performance issues. Therefore, this is a one-way message feature.

The code is available on GitHub [here].

    /// <summary>
    /// IChatServiceCallback
    /// Interface for the chat service callback
    /// </summary>
    [ServiceContract]
    public interface IChatServiceCallback
    {
        /// <summary>
        /// SendClientMessage
        /// Sends the Client recent messages
        /// (When it is connected)
        /// The service does not want to wait on a response from the client
        /// so there is no secondary callback to the service. This is oneway
        /// to the client only. 
        /// </summary>
        /// <param name="message">a ChatMessage object</param>
        [OperationContract(IsOneWay = true)]
        void SendClientMessage(ChatMessage message);

    } // end of interface

ChatMessage (Class for Chat Message Object)

The ChatMessage class represents a chat message object in the application. It describes the message ChatMessage originator (sender’s name as a string), the DateTime that it was sent (universal time reference on the service), and the message (string). In addition, there is a custom override of its ToString() object representation for display in the client form application list window of the chat messages. The object string representation formats the ChatMessage to display as one long string, but the client does some additional formatting to show the message “word wrap”. The code is available on GitHub [here].

   /// <summary>
    /// ChatMessage
    /// Describes an object that stores information 
    /// about a chat message (single)
    /// </summary>
    [DataContract]
    public class ChatMessage
    {
        #region Properties

        /// <summary>
        /// Name of the ChatMessage Originator
        /// </summary>
        [DataMember]
        public string Name { get; set; } = "";

        /// <summary>
        /// Timestamp of when the message was sent
        /// </summary>
        [DataMember]
        public DateTime TimeStamp { get; set; } = DateTime.Now;

        /// <summary>
        /// Message content
        /// </summary>
        [DataMember]
        public string Message { get; set; } = "";

        #endregion Properties

        #region constructors

        // Default Constructor
        public ChatMessage()
        {

        }

        /// <summary>
        /// ChatMessage
        /// Paramterized constructor
        /// </summary>
        /// <param name="name">Name of the message sender (string)</param>
        /// <param name="time">TimeStamp (DateTime universal) of the sender</param>
        /// <param name="message">Message of the sender (string)</param>
        public ChatMessage(string name, DateTime time, string message)
        {
            Name = name;
            TimeStamp = time;
            Message = message;

        } // end of constructor

        #endregion constructors

        /// <summary>
        /// ToString()
        /// Overrides the ChatMessage ToString() object
        /// Time is Displayed in Local Time but Serialized in universal
        /// </summary>
        /// <returns>a formatted string</returns>
        public override string ToString()
        {
            return $"{TimeStamp.ToLocalTime()} {Name.PadRight(15, ' ')} : {Message}";
        } 

    } // end of ChatMessage Object

DuplicateUserFault (Class for Error/Exception/Fault Object)

The DuplicateUserFault class represents a specific error condition in the application that is an Exception (error) on the service and communicated as a fault in WCF to the client. It describes the specific error condition of a client trying to login with a username that is already logged in to the chat service. The fault is communicated to the client to handle and understand that it could not log in and why. The code is available on GitHub [here].

    /// <summary>
    /// DuplicateUserFault
    /// Holds data object definition for a 
    /// duplicate user fault generated on
    /// the service sent to the client
    /// </summary>
    [DataContract]
    public class DuplicateUserFault
    {
        [DataMember]
        public string Reason { get; set; }

    } // end of class

Chat Message Service Library

A class library describing the service implementation of the interfaces was added to my Visual Studio solution. Separating the service implementation from the host application project, provided one more level of abstraction and separation to better organize the solution. The code is available on GitHub [here].

ChatService (Code that Implements the Service Interface)

The service implementation code has service behaviors configured to allow for a single instance while each service call connects to the same instance. The service library references a shared class library called “SharedLibrary” that contains the interface definition for the service and the class definition for a file object (ChatMessage) holding the chat message information as well as the Fault (Exception) “DuplicateUserFault” class model.  The code is available on GitHub [here].

Fields

The fields section contains a few notable private fields. The first field “maximumMessages” limits the chat message history to a rolling of X number of messages. The chat messages (last X maximumMessages) are stored in a Queue container that represents the message history. Lastly, a Dictionary contains the logged in users (string) and their callback information (IChatServiceCallback).

 #region Fields

        // Set to a small number for testing purposes
        private const int maximumMessages = 40;

        // The Message History is a Queue
        private Queue<ChatMessage> chatMessages = new Queue<ChatMessage>();

        // The list of logged in users plus their callback information
        private Dictionary<string, IChatServiceCallback> loggedInUsers = new Dictionary<string, IChatServiceCallback>();

 #endregion Fields

Service Interface Implementation Methods

The service implementations are described below.

Get Message History

Returns a list of the most recent chat messages (List<ChatMessage>)

        /// <summary>
        /// GetMessageHistory()
        /// Returns a list of the most recent chat messages
        /// </summary>
        /// <returns>a list of all the chat messages List<ChatMessage></returns>
        public List<ChatMessage> GetMessageHistory()
        {
            return chatMessages.ToList();
        }
Login

Logins the user and registers the callback into a dictionary of logged in users. Sends a chat message that the user has logged on.

        /// <summary>
        /// Login
        /// Logins the user and registers the callback into a dictionary of
        /// logged in users.
        /// Sends a messgae that the user has logged on...
        /// </summary>
        /// <param name="userName">user name from the client (string)</param>
        public void Login(string userName)
        {
            // Trim the Username to 15 Characters
            if (userName.Length > 15)
            {
                userName = userName.Substring(0, 15);
            }

            // This is the caller and registers the callback for the service to 
            // communicate the new messages
            IChatServiceCallback callback = OperationContext.Current.GetCallbackChannel<IChatServiceCallback>();

            // Validates the User is Not Already Logged In
            if (!loggedInUsers.ContainsKey(userName))
            {
                // Add to the List of Logged on Users
                loggedInUsers.Add(userName, callback);

                // Send a Message that the new user is Logged In
                SendMessage("Admin", $"User {userName} logged in...");

                // Write to Console
                Console.WriteLine($"User {userName} logged in...");

            } // end of if

            // Duplicate User Fault - User Logged In Already
            else
            {
                DuplicateUserFault fault = new DuplicateUserFault()
                { Reason = "User '" + userName + "' already logged in!" };
                throw new FaultException<DuplicateUserFault>(fault);
            } // end of else

        } // end of method
Login State

Returns the login state (true if user is logged in, false otherwise) of a user name from the client (string).

        /// <summary>
        /// LogInState
        /// Returns the login state of a user
        /// </summary>
        /// <param name="userName">user name from the client (string)</param>
        /// <returns>true if user is logged in, false otherwise</returns>
        public bool LogInState(string userName)
        {
            // Trim the Username to 15 Characters
            if (userName.Length > 15)
            {
                userName = userName.Substring(0, 15);
            }

            if (loggedInUsers.ContainsKey(userName))
            {
                return true;
            }
            else
            {
                return false;
            }
        } // end of  method
Logoff

Logs off the user by removing the user from the logged in user list and sends a message that the user has logged off.

        /// <summary>
        /// Logoff
        /// Logs off the user by removing the user from the logged in user list
        /// Send a message that the user has logged off..
        /// </summary>
        /// <param name="userName">user name from the client (string)</param>
        public void Logoff(string userName)
        {
            // Trim the Username to 15 Characters
            if (userName.Length > 15)
            {
                userName = userName.Substring(0, 15);
            }

            try
            {
                if (loggedInUsers.ContainsKey(userName))
                {
                    // Remove from the List of Logged on Users
                    loggedInUsers.Remove(userName);

                    // Send a Message that the user is Logged Off
                    SendMessage("Admin", $"User {userName} logged off...");

                    // Send Message to Console
                    Console.WriteLine($"User {userName} logged off...");
                }
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.Message, System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace + "." + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name + "." + System.Reflection.MethodBase.GetCurrentMethod().Name);
            }

        } // end of method
Send Message

The service implementation that handles the client request to send a chat message

        /// <summary>
        /// SendMessage
        /// The Service Implementation that handles the client request
        /// to send it a message
        /// </summary>
        /// <param name="userName">user name from the client (string)</param>
        /// <param name="message">the message (string) from the client</param>
        public void SendMessage(string userName, string message)
        {
            // Trim the Username to 15 Characters
            if (userName.Length > 15)
            {
                userName = userName.Substring(0, 15);
            }

            try
            {
                // Verify the User is Logged On First
                if (userName != "Admin" && !loggedInUsers.ContainsKey(userName))
                {
                    // Try to Login User First
                    Login(userName);
                }

                // Create New Message Object
                ChatMessage chatmessage = new ChatMessage(userName, DateTime.Now , message);

                // Add to Message History
                AddMessage(chatmessage);

                // Transmitt to Connected Users
                SendMessageToUsers(chatmessage);

            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.Message, System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace + "." + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name + "." + System.Reflection.MethodBase.GetCurrentMethod().Name);
            }
        } // end of method

Helper Methods

The service helper method implementations are described below.

Add Message

Adds the chat message object (ChatMessage) to the queue collection

        /// <summary>
        /// AddMessage
        /// Add to Chat Messages
        /// </summary>
        /// <param name="message">the message object (ChatMessage) to add to the queue collection</param>
        private void AddMessage(ChatMessage message)
        {
            // There is a Message Limit
            // Dequeue then Enqueue
            if (chatMessages?.Count >= maximumMessages)
            {
                chatMessages.Dequeue();
                chatMessages.Enqueue(message);
            }
            // Enqueue
            else
            {
                chatMessages?.Enqueue(message);
            }
        } // end of method
Send Message to Users

Sends the chat message object (ChatMessage) to registered users & callbacks. Logs off any disconnected clients.

        /// <summary>
        /// SendMessageToUsers
        /// Transmit to Registered Users & Callbacks
        /// Logs off any disconnected clients
        /// </summary>
        /// <param name="message">The message to send to all clients (ChatMessage)</param>
        private void SendMessageToUsers(ChatMessage message)
        {
            // Inform all of the clients of the new message
            List<string> callbackKeys = loggedInUsers?.Keys.ToList();

            // Loops through each logged in user
            foreach (string key in callbackKeys)
            {
                try
                {
                    IChatServiceCallback callback = loggedInUsers[key];
                    callback.SendClientMessage(message);
                    Console.WriteLine($"Sending user {key} message {message}");
                }
                // catches an issue with a user disconnect and loggs off that user
                catch (Exception ex)
                {
                    System.Diagnostics.Debug.WriteLine(ex.Message, System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace + "." + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name + "." + System.Reflection.MethodBase.GetCurrentMethod().Name);
                    // Remove the disconnected client
                    Logoff(key);
                }
            } // end of foreach
        } // end of method

Application Configuration App.Config

The application configuration simple sets up the service model configuration including the endpoint, interface contract name, tcp protocol, and the base address for this demo. This configuration must be included where the application is hosted (run) (see next section) and is displayed in this service class library project for reference. The code is available on GitHub [here].

Chat Message Service Host (ChatServiceHost)

The ChatServiceHost project is a simple console application responsible for starting, managing, and stopping the Chat Message service. This demo hosts the service using tcp protocol on a custom port address as described in the application configuration file.  The code is available on GitHub [here].

Main Program

The main program is the entry point for the service host application. It creates a ServiceHost object of type described in the ChatServiceLibrary, opens the host up for connections, and waits for the user to manually close the service (pressing enter in the console window). Also it receives console commands from the service implementation that logs the activity going on the server (user login, logoff, messaging, etc.).

    class Program
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {

            try
            {
                Console.WriteLine("Starting Chat Service...");
                // Note: Do not put this service host constructor within a using clause.
                // Errors in Open will be trumped by errors from Close (implicitly called from ServiceHost.Dispose).
                ServiceHost host = new ServiceHost(typeof(ChatService));
                host.Open();

                Console.WriteLine("The Chat Service has started.");
                Console.WriteLine("Press <ENTER> to quit.");
                Console.ReadLine();
                host.Close();
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.Message, System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace + "." + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name + "." + System.Reflection.MethodBase.GetCurrentMethod().Name);
                Console.WriteLine("An error occurred: " + ex.Message);
                Console.WriteLine("Press <ENTER> to quit.");
                Console.ReadLine();
            }

        } // end of method
    } // end of class

Application Configuration App.Config

The application configuration simple sets up the service model configuration including the endpoint, interface contract name, tcp protocol, and the base address for this demo. The code is available on GitHub [here].

<?xml version="1.0"?>
<configuration>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="ServerBehavior">
          <serviceMetadata/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>

    <services>
      <service name="ChatServiceLibrary.ChatService" behaviorConfiguration="ServerBehavior">
        <host>
          <baseAddresses>
            <add baseAddress="net.tcp://localhost:31000/Chat"/>
            <!--<add baseAddress="http://localhost:31001/Chat"/>-->
          </baseAddresses>
        </host>

        <!--bindingConfiguration="default"-->
        <endpoint address="" binding="netTcpBinding" bindingConfiguration="myTcp" contract="SharedLibrary.IChatService"/>

        <!--<endpoint address="" binding="wsDualHttpBinding" bindingConfiguration="myHttp" contract="SharedLibrary.IChatService"/>-->
      </service>

    </services>

    <bindings>

      <netTcpBinding>
        <binding name="myTcp" maxBufferPoolSize="60000000" maxBufferSize="60000000" maxReceivedMessageSize="60000000">
          <security mode="None"/>
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647"/>
        </binding>
      </netTcpBinding>
      <wsDualHttpBinding>
        <binding name="myHttp" maxBufferPoolSize="60000000" maxReceivedMessageSize="60000000">
          <security mode="None"/>
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647"/>
        </binding>
      </wsDualHttpBinding>
    </bindings>

  </system.serviceModel>

  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>
  </startup>

</configuration>

Client “Tester to Service” Windows Form Application

The client “tester to service” is a simple windows form application project in the same solution that connects to the “Chat Service” by use of a ChannelFactory proxy to a specified service name that is defined in the client application configuration. The client program will use this proxy to test the OperationContract or methods available in the ServiceContract and return the results to the user on the form window.

The client application shares and references the “Shared Library” – a class library project. It has access to the Chat Message service interface to know exactly how it can consume the service with the help of a ChannelFactory proxy (discussed in the next section). The project code is available on GitHub [here].

Features

The client application was meant to only demonstrate consumption of the Chat Message service and has limited scope of features. Some of the client application features are:

Simple UI Design

The user interface is simplified to allow the log in and log off of the chat message service with one button. The connection action button turns green when connected and black when disconnected. The UI adapts to the connection state of the client to the service. When connected, the user has the ability to write and send messages to the chat service. When disconnected, those features are disabled.

Validation

The client validates the login state before sending messages or performing complex actions. The username is validated locally and at the service. If there is an existing user logged in, the service sends a detailed fault that the client then processes and alerts the user through a message dialog window. Additional validation to the right state works in tandem with the simple UI design features.

Chat History & Formatting

The chat history is retrieved from the chat message service and formatted specially to fit in a word-wrap text style in the Windows Form control list box. A special thanks to a stack-flow article [here] that I used to make sure the text was formatted correctly to wrap in the list box.

Form Code

The code behind file is for the client tester and manages the application in one file. A separate business logic/process layer could have been added but the focus of the demo was on the service and consumption, not a UI/UX design. The code behind the form is available on GitHub [here].

Fields

  • Private fields allow the client to work with the following:
  • Logged into service state
  • User name on the client (may be truncated on service)
  • If the chat message history is loaded into the client
        // Helps Keeps Track of the Client State to Service
        private bool LoggedIn;

        // Username (Autenticated with Service)
        private string userName = "";

        // Has the Chat History Been Loaded?
        private bool IsChatHistoryLoaded = false;

Constructors

When the form is instantiated, it does some house keeping on the form GUI (ex: buttons enable/disable). Send message capability is disabled until the user successfully connects to the service.

        /// <summary>
        /// frmMain()
        /// Constructor for the Main Form
        /// Initializes the GUI
        /// </summary>
        public frmMain()
        {
      
            // Initializes the Main Form
            InitializeComponent();

            // Initialize UI
            // Set the default loggedin state
            LoggedIn = false;

            // Enable the UI features
            btnConnection.Enabled = true;

            // Disable the Chat Features until user logs in
            UpdateChatGUI(false);

        } // end of main method

Events

Connection Button Click

Clicking on this button either connects or disconnects the client from the Chat message service. If the user is “locally” logged in (the field is set to true), the it proceeds to logoff the user and update the GUI accordingly. If the user is logged of, it proceeds to validate the client user name text entry, then try to login to the chat messenger service. If it successfully logs in, it will update the UI and retrieve the chat message history.

        /// <summary>
        /// btnConnection_Click
        /// Processes the Login/Logoff Button Control
        /// </summary>
        /// <param name="sender">not used</param>
        /// <param name="e">not used</param>
        private void btnConnection_Click(object sender, EventArgs e)
        {
            // If the User is Logged In, Log Them Off
            if (LoggedIn)
            {
                // Log Off
                LogOff();

                // Disable the Chat Features until user logs in
                UpdateChatGUI(false);

                // Reset the Chat History Flag
                IsChatHistoryLoaded = false;

                return;
            } 

            // Proceed to Login User...

            // Validate UserName
            if (!ValidateGUIUserName())
            {
                return;
            }

            // Validate Login
            if (!Login())
            {
                return;
            }

            // Retrieve the Message History & Update UI
            UpdateMessageHistoryUI();

            // Update the GUI for Chat
            UpdateChatGUI(true);

        } // end of method
Send Message Button Click

Before attempting to send the message, it will first validate the user is logged in before sending a chat message (string) to the chat service. It does not assemble the ChatMessage object locally but rather relies on the service to create the object with the server timestamp and formatting, then return the object through the callback implementation (discussed below).

        /// <summary>
        /// btnSendMessage_Click
        /// Implements the Send Message Button Click
        /// </summary>
        /// <param name="sender">not used</param>
        /// <param name="e">not used</param>
        private void btnSendMessage_Click(object sender, EventArgs e)
        {
            // Validate User LoggedIn
            if (txtMyMessage.Enabled == false || !IsUserLoggedIn(userName))
            {
                // User needs to login!
                MessageBox.Show("Please login before sending a message.", "Login Required", MessageBoxButtons.OK);
            }
            else
            {
                // Send Message to Service
                // Retrieve the New Messages
                using (DuplexChannelFactory<IChatService> cf = new DuplexChannelFactory<IChatService>(this, "NetTcpBinding_IChatService"))
                {
                    cf.Open();
                    IChatService proxy = cf.CreateChannel();

                    if (proxy != null)
                    {
                        try
                        {
                            // It is ok to send empty messages, I don't care lol
                            proxy.SendMessage(userName, txtMyMessage.Text);

                        } // end of try

                        catch (Exception ex)
                        {
                            System.Diagnostics.Debug.WriteLine(ex.Message, System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace + "." + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name + "." + System.Reflection.MethodBase.GetCurrentMethod().Name);
                            MessageBox.Show(ex.Message);
                        } // end of catch

                    } // end of if

                    else
                    {
                        // Cannot Connect to Server 
                        MessageBox.Show("Cannot Create a Channel to a Proxy. Check Your Configuration Settings.", "Proxy", MessageBoxButtons.OK);
                    } // end of else

                } // end of using

            } // end of if

        } // end of method
Chat List Box Formatting Events

Much thanks to the Internet of programmers for finding a solution to word-wrap a formatted string to the Windows Form ListBox control width without having to show the horizontal scrollbars. The code reference is [here].

        /// <summary>
        /// lstChatMessages_MeasureItem
        /// Helps Word Wrap the ListBox 
        /// Thanks to a Slack Article
        /// https://stackoverflow.com/questions/17613613/winforms-dotnet-listbox-items-to-word-wrap-if-content-string-width-is-bigger-tha
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void lstChatMessages_MeasureItem(object sender, MeasureItemEventArgs e)
        {
            e.ItemHeight = (int)e.Graphics.MeasureString(lstChatMessages.Items[e.Index].ToString(), lstChatMessages.Font, lstChatMessages.Width).Height;
        }

        /// <summary>
        /// lstChatMessages_DrawItem
        /// Helps Word Wrap the ListBox
        /// Thanks to a Slack Article
        /// https://stackoverflow.com/questions/17613613/winforms-dotnet-listbox-items-to-word-wrap-if-content-string-width-is-bigger-tha
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void lstChatMessages_DrawItem(object sender, DrawItemEventArgs e)
        {
            e.DrawBackground();
            e.DrawFocusRectangle();
            e.Graphics.DrawString(lstChatMessages.Items[e.Index].ToString(), e.Font, new SolidBrush(e.ForeColor), e.Bounds);
        }

IChatServiceCallback Implementation

The client must implement the IChatServiceCallback interface contract to be able to receive the instant chat messages from the service from all users, including the ones they sent. This method is called when a message is received from the server. It processes the incoming message from the service from the chat conversation and adds to the UI chat message list box. Also, it checks if UI thread synchronization is required before processing the UI update. If synchronization is required, it invokes a delegate on the UI thread to process the new ChatMessage object in the GUI.

        /// <summary>
        /// IChatServiceCallback Implementation
        /// SendClientMessage
        /// Called when a message is received from the server
        /// Processes the incoming message from the service
        /// from the chat conversation and adds to the UI
        /// chat message box
        /// </summary>
        /// <param name="message">the message (ChatMessage) object</param>
        public void SendClientMessage(ChatMessage message)
        {
            // Don't receive any messages until the chat history is loaded
            if (!IsChatHistoryLoaded)
            {
                return;
            }

            try
            {
                // UI Threading Sync
                if (lstChatMessages.InvokeRequired)
                {
                    // UI Thread Sync is Required, Invoke the Request on the
                    // UI thread
                    Action<ChatMessage> del = new Action<ChatMessage>(SendClientMessage);
                    lstChatMessages.BeginInvoke(del, message);
                }
                // Add the recent chat message to the listbox
                else
                {
                    lstChatMessages.Items.Add(message);
                }
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.Message, System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace + "." + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name + "." + System.Reflection.MethodBase.GetCurrentMethod().Name);
                MessageBox.Show("Error receiving message: " + ex.Message);
            }
        } // end of method

Methods

IsUserLoggedIn

Connects to the service to see if the user is logged in.

        /// <summary>
        /// IsUserLoggedIn
        /// Connects to the Service to See if the User is LoggedIn
        /// </summary>
        /// <param name="name">the user name (string)</param>
        /// <returns>a boolean, true if the user is logged in</returns>
        private bool IsUserLoggedIn(string name)
        {
            // If Locally Logged In
            if (LoggedIn)
            {
                // Check State on Service
                // Make a ChannelFactory Proxy to the Service
                DuplexChannelFactory<IChatService> cf = new DuplexChannelFactory<IChatService>(this, "NetTcpBinding_IChatService");
                    
                    cf.Open();
                    IChatService proxy = cf.CreateChannel();

                    if (proxy != null)
                    {
                        try
                        {
                            // Call the Service 
                            if (proxy.LogInState(name))
                            {
                                // The user is logged in
                                LoggedIn = true;

                                // Update the GUI to Enable Chat
                                UpdateChatGUI(true);

                                return true;
                            }
                            else
                            {
                            
                                // The user is not logged in
                                LoggedIn = false;

                                // Update the GUI to Disable Chat
                                UpdateChatGUI(false);

                                return false;
                            }
                        } // end of try

                        catch (Exception)
                        {
                            MessageBox.Show("Cannot verify logged in state on service.", "Service Issue", MessageBoxButtons.OK);
                            return false;
                        } // end of catch

                    } // end of if

                    else
                    {
                        // Cannot Connect to Server 
                        MessageBox.Show("Cannot Create a Channel to a Proxy. Check Your Configuration Settings.", "Proxy", MessageBoxButtons.OK);
                        return false;
                    } // end of else

            } // end of main if

            // User is Locally Logged Off
            else
            {
                // Update the GUI to Disable Chat
                UpdateChatGUI(false);

                return false;
            }

        } // end of method
ValidateGUIUserName

Validates the username text entry to be not empty and unique and not already logged in on the service.

        /// <summary>
        /// ValidateGUIUserName
        /// Validates the username text entry to be not empty
        /// and unique and not already logged in on the 
        /// service
        /// </summary>
        /// <returns>true if a valid user name</returns>
        private bool ValidateGUIUserName()
        {
            // User Text Entry is Valid
            if (!string.IsNullOrWhiteSpace(txtName.Text))
            {
                // Validate User Is Not Already Logged In
                if (!IsUserLoggedIn(txtName.Text))
                {
                    userName = txtName.Text;
                    return true;
                }
                else
                {
                    MessageBox.Show("Username already logged in to service. Choose a different name.", "Existing Username", MessageBoxButtons.OK);
                    return false;
                }
            }
            // User Text Entry is Invalid
            else
            {
                MessageBox.Show("Username cannot be empty.", "Invalid Username", MessageBoxButtons.OK);
                return false;
            }
        } // end of method
Login

Tries to login this client’s username to the service.

        /// <summary>
        /// Login
        /// Tries to login this client's username to the service
        /// </summary>
        /// <returns>true if successful, false otherwise</returns>
        private bool Login()
        {
            DuplexChannelFactory<IChatService> cf = new DuplexChannelFactory<IChatService>(this, "NetTcpBinding_IChatService");

                cf.Open();
                IChatService proxy = cf.CreateChannel();

                if (proxy != null)
                {
                    try
                    {
                        proxy.Login(userName);

                        // Change the Login State
                        LoggedIn = true;

                    return true;
                    } // end of try

                    catch (FaultException<DuplicateUserFault> ex)
                    {
                        System.Diagnostics.Debug.WriteLine(ex.Detail.Reason, System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace + "." + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name + "." + System.Reflection.MethodBase.GetCurrentMethod().Name);
                        MessageBox.Show(ex.Detail.Reason);
                        return false;
                    }
                    catch (Exception ex)
                    {
                        System.Diagnostics.Debug.WriteLine(ex.Message, System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace + "." + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name + "." + System.Reflection.MethodBase.GetCurrentMethod().Name);
                        MessageBox.Show("Error logging in user: " + ex.Message);
                        return false;
                    } 

                } // end of if

                else
                {
                    // Cannot Connect to Server 
                    MessageBox.Show("Cannot Create a Channel to a Proxy. Check Your Configuration Settings.", "Proxy", MessageBoxButtons.OK);
                    return false;
                } // end of else

        } // end of method
LogOff

Logs off the user from the service and updates the GUI locally

        /// <summary>
        /// LogOff
        /// Logs off the user from the service
        /// and updates the GUI locally
        /// </summary>
        private void LogOff()
        {
            // Call the Service to Log Off
            DuplexChannelFactory<IChatService> cf = new DuplexChannelFactory<IChatService>(this, "NetTcpBinding_IChatService");
                cf.Open();
                IChatService proxy = cf.CreateChannel();

                if (proxy != null)
                {
                    try
                    {
                        proxy.Logoff(userName);

                        // Update local field
                        LoggedIn = false;

                        // Disable the GUI for Chat
                        UpdateChatGUI(false);

                    } // end of try

                    catch (Exception ex)
                    {
                        System.Diagnostics.Debug.WriteLine(ex.Message, System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace + "." + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name + "." + System.Reflection.MethodBase.GetCurrentMethod().Name);
                        MessageBox.Show("Error logging off user: " + ex.Message);
                    }

                } // end of if

                else
                {
                    // Cannot Connect to Server 
                    MessageBox.Show("Cannot Create a Channel to a Proxy. Check Your Configuration Settings.", "Proxy", MessageBoxButtons.OK);
                } // end of else

        } // end of method
GetMessageHistory

Gets a list of ChatMessages from the service to update the UI list box

        /// <summary>
        /// GetMessageHistory
        /// Gets a list of ChatMessages to update
        /// the UI list box
        /// </summary>
        private void UpdateMessageHistoryUI()
        {
            // If User is Not Logged In
            if (!IsUserLoggedIn(userName))
            {
                return;
            }

            // Clear the List Box
            lstChatMessages.Items.Clear();

            // Temporary variable to hold the list of chat messages
            List<ChatMessage> historyChat;

            // Retrieve the New Messages
            DuplexChannelFactory<IChatService> cf = new DuplexChannelFactory<IChatService>(this, "NetTcpBinding_IChatService");

                cf.Open();
                IChatService proxy = cf.CreateChannel();

                if (proxy != null)
                {
                    try
                    {
                        // retrieve the chat history
                        historyChat = proxy.GetMessageHistory();

                        // Update the UI
                        foreach (ChatMessage item in historyChat)
                        {
                            lstChatMessages.Items.Add(item);
                        }

                    IsChatHistoryLoaded = true;

                    } // end of try

                    catch (Exception ex)
                    {
                        System.Diagnostics.Debug.WriteLine(ex.Message, System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace + "." + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name + "." + System.Reflection.MethodBase.GetCurrentMethod().Name);
                        MessageBox.Show(ex.Message);
                    }

                } // end of if

                else
                {
                    // Cannot Connect to Server 
                    MessageBox.Show("Cannot Create a Channel to a Proxy. Check Your Configuration Settings.", "Proxy", MessageBoxButtons.OK);
                } // end of else

        } // end of method
UpdateChatGUI

Cleans house on the GUI to make it in sync with being logged in, able to send messages, etc…

       /// <summary>
        /// UpdateChatGUI
        /// Cleans house on the GUI to make it in sync
        /// with being logged in, able to send messages, etc...
        /// </summary>
        /// <param name="enabled">true to enable chat, false otherwise</param>
        private void UpdateChatGUI(bool enabled)
        {
            if (enabled)
            {
                // Update the GUI to Enable Chat
                txtName.Enabled = false;
                btnSendMessage.Enabled = true;
                txtMyMessage.Enabled = true;
                btnConnection.Enabled = true;
                btnConnection.BackgroundImage = Properties.Resources.logon;
            }
            else
            {
                // Update the GUI to Disable Chat
                // Changes back to Login Screen
                txtName.Enabled = true;
                btnConnection.Enabled = true;
                btnSendMessage.Enabled = false;
                txtMyMessage.Enabled = false;
                btnConnection.BackgroundImage = Properties.Resources.logoff;
            }
        } // end of method

ChannelFactory Proxy and Application Configuration

The application configuration file (App.config) specifies the service endpoint address, binding, and contract information for the client to create a proxy and connect. The service contract information is in the Shared Class Library that has the service interface. The code is available on GitHub [here].

<?xml version="1.0"?>
<configuration>
  
  <system.serviceModel>
    <bindings>
      <netTcpBinding>
        <binding name="NetTcpBinding_IChatService" maxBufferPoolSize="60000000" maxBufferSize="60000000" maxReceivedMessageSize="60000000">
          <security mode="None"/>
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647"/>
          <!--<reliableSession ordered="true" inactivityTimeout="00:10:00" enabled="false"/>-->
          <!--<security mode="Transport">
            <transport clientCredentialType="Windows" protectionLevel="EncryptAndSign"/>
            <message clientCredentialType="Windows"/>
          </security>-->         
        </binding>
      </netTcpBinding>

      <!--<wsDualHttpBinding>
        <binding name="myHttp_IChatService" maxBufferPoolSize="60000000" maxReceivedMessageSize="60000000">
          <security mode="None"/>
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647"/>
        </binding>
      </wsDualHttpBinding>--> 
      
    </bindings>    
    <client>
      <endpoint address="net.tcp://localhost:31000/Chat" binding="netTcpBinding" bindingConfiguration="NetTcpBinding_IChatService" contract="SharedLibrary.IChatService" name="NetTcpBinding_IChatService">
      <!--<endpoint address="http://localhost:31001/Chat" binding="wsDualHttpBinding"
                bindingConfiguration="myHttp" contract="ChatServiceSharedLib.IChatService" name="myHttp">-->
      </endpoint>
    </client>
  </system.serviceModel>
  
<startup>
  <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>
</startup>
</configuration>

Demo

Code

The entire project code repository is available on GitHub [here].

Kathleen has 15 yrs. of experience analyzing business IT needs and engineering solutions in payments, electric utilities, pharmaceutical, financial, virtual reality, and internet industries with a variety of technologies. Kathleen's project experience has been diverse ranging from executing the business analysis “design phase” to hands-on development, implementation, and testing of the IT solutions. Kathleen and her husband reside in Indiana with no children, instead living along with their fur babies.