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].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
/// <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].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/// <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].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
/// <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].
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/// <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).
1 2 3 4 5 6 7 8 9 10 11 12 |
#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>)
1 2 3 4 5 6 7 8 9 |
/// <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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
/// <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).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/// <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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
/// <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/// <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/// <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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/// <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.).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
<?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
1 2 3 4 5 6 7 8 |
// 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/// <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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
/// <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).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
/// <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].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/// <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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/// <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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
/// <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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/// <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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
/// <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
/// <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
/// <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…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
/// <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].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<?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].