Quantcast
Channel: Alxandr.me » c#
Viewing all articles
Browse latest Browse all 11

dotRant: Making an IRC library for .NET in C# – Part 4: Skeletal structure of the IrcClient and the DefaultDictionary

$
0
0

In the previous post in this series I talked about some errors I found in the dotRant code, and what had to be done to fix those, and I also talked a bit about taking an async approach to connecting to the server. This will allow for better responsiveness in applications using dotRant while still maintaining a simple way of using the library tanks to the System.Threading.Tasks namespace. In this post I will create the actual base structure of the IrcClient-class, and also construct a helper-class that I call DefaultDictionary, but first of all, let’s take a look at the problem we have at hand.

Irc consists of messages (or as I like to call them, commands) sent back and forth from client and server. These messages all contains a name which can be viewed as a string (though it might just be a number). We’ve already made functionality for sending and receiving these messages, however, we need to act upon the fact that there is a new command available, and depending upon how we decide to do this our class might end up as a mess, or it might end up being neat and tidy. I don’t think I have to tell you that I don’t want the class to end up as a mess, so let’s look at some of the alternative ways of handling commands sent from the server.

Handling commands

The first thing that comes to mind when you need to run different code depending on something is a regular if-else-test. Using an if-else-test we can write a function that handles the command like this:

private void HandleCommand(string cmdName, string[] cmdArgs)
{
	if(cmdName == "PRIVMSG")
	{
		// Perform handeling of the private message.
	}
	else if(cmdName == "JOIN")
	{
		// Perform handeling of a JOIN command.
	}
	else
	{
		// Default action, command not known.
	}
}

Needless to say, this is not very pretty, and does scale bad. You end up putting all the functionality into a single function, and there is a lot of code to do this simple matching. One way we can make this code better is by changing the if-else-test with a switch-test. Another thing we can do to further improve our code is to create functions for the commands, rather than just putting all the code in one function, then we end up with something like this:

private void HandleCommand(string cmdName, string[] cmdArgs)
{
	switch(cmdName)
	{
		case "PRIVMSG":
			HandlePrivmsg(cmdName, cmdArgs); break;
		case "JOIN":
			HandleJoin(cmdName, cmdArgs); break;
		default:
			HandleUnknown(cmdName, cmdArgs); break;
	}
}

This makes our code a lot better, but there is still one problem, whenever we add support for a new command we need to change code two places. First, we need to create a new function, than we need to add a clause in our switch-case statement, and if we remove something, or rename, it also needs to be changed both places. I personally don’t like that, and thus has found another way to solve this problem, by using Attributes. If you don’t know what attributes in C# is I recommend reading up on them, but essentially you can add info to functions or properties or even input-parameters and classes that you might use later to figure out how to use that class or method, and that’s precisely what we’re going to do. On our HandleJoin-method (which by the way is not necessarily a final name as I have yet to write the method) we just denote that we want this method to be called whenever a “JOIN” command is received, and then we handle the rest in our HandleCommand method (which too is just a temporarily name). Doing this enables us to add functionality much more simply, but how do we achieve this? Well, the actual implementation is something I’ll post in my next blog-post as I still need to do a bit more research about how efficient my current method of handling that task is. However, one of the things we do require for this functionality is to emulate the switch-case statement over a variable list of items containing a name and a function to call should there be a match, and also a default item, and for this I’ve created the DefaultDictionary. The DefaultDictionary is basically just a simple Dictionary that has a Default property that is returned should the dictionary not contain a match for the requested key, just like the switch-case does. Thus, you will never get a KeyNotFoundException cause if the key is not found the Default is returned instead. The DefaultDictionary is really easy to implement, and should be easy to understand. The code, fully commented with XML-comments where required, looks like this:

    /// <summary>
    /// A dictionary that also holds a default-value which it returns if
    /// you try to access a key that doesn't exist.
    /// </summary>
    /// <typeparam name="TKey">The type of the key.</typeparam>
    /// <typeparam name="TValue">The type of the values.</typeparam>
    public class DefaultDictionary<TKey, TValue> : IDictionary<TKey, TValue>
    {
        private TValue defaultValue = default(TValue);
        private Dictionary<TKey, TValue> dict = new Dictionary<TKey, TValue>();

        /// <summary>
        /// Gets or sets the default.
        /// </summary>
        /// <value>
        /// The default.
        /// </value>
        public TValue Default
        {
            get { return defaultValue; }
            set { defaultValue = value; }
        }

        #region IDictionary<TKey,TValue> Members

        /// <summary>
        /// Gets or sets the value associated with the specified key.
        /// </summary>
        /// <param name="key">The key of the value to get or set.</param>
        /// <returns>The value associated with the specified key.</returns>
        /// <exception cref="T:System.ArgumentNullException"><paramref name="key"/> is null.</exception>
        /// <exception cref="T:System.NotSupportedException">The property is set and the <see cref="T:System.Collections.Generic.IDictionary`2"/> is read-only.</exception>
        public TValue this[TKey key]
        {
            get
            {
                if (!dict.ContainsKey(key))
                    return defaultValue;
                return dict[key];
            }
            set
            {
                dict[key] = value;
            }
        }

        /// <summary>
        /// Adds an element with the provided key and value to the <see cref="T:System.Collections.Generic.IDictionary`2"/>.
        /// </summary>
        /// <param name="key">The object to use as the key of the element to add.</param>
        /// <param name="value">The object to use as the value of the element to add.</param>
        /// <exception cref="T:System.ArgumentNullException"><paramref name="key"/> is null.</exception>
        /// <exception cref="T:System.ArgumentException">An element with the same key already exists in the <see cref="T:System.Collections.Generic.IDictionary`2"/>.</exception>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.IDictionary`2"/> is read-only.</exception>
        public void Add(TKey key, TValue value)
        {
            dict.Add(key, value);
        }

        /// <summary>
        /// Determines whether the <see cref="T:System.Collections.Generic.IDictionary`2"/> contains an element with the specified key.
        /// </summary>
        /// <param name="key">The key to locate in the <see cref="T:System.Collections.Generic.IDictionary`2"/>.</param>
        /// <returns>
        /// true if the <see cref="T:System.Collections.Generic.IDictionary`2"/> contains an element with the key; otherwise, false.
        /// </returns>
        /// <exception cref="T:System.ArgumentNullException"><paramref name="key"/> is null.</exception>
        public bool ContainsKey(TKey key)
        {
            return dict.ContainsKey(key);
        }

        /// <summary>
        /// Gets an <see cref="T:System.Collections.Generic.ICollection`1"/> containing the keys of the <see cref="T:System.Collections.Generic.IDictionary`2"/>.
        /// </summary>
        /// <returns>An <see cref="T:System.Collections.Generic.ICollection`1"/> containing the keys of the object that implements <see cref="T:System.Collections.Generic.IDictionary`2"/>.</returns>
        public ICollection<TKey> Keys
        {
            get { return dict.Keys; }
        }

        /// <summary>
        /// Removes the element with the specified key from the <see cref="T:System.Collections.Generic.IDictionary`2"/>.
        /// </summary>
        /// <param name="key">The key of the element to remove.</param>
        /// <returns>
        /// true if the element is successfully removed; otherwise, false.  This method also returns false if <paramref name="key"/> was not found in the original <see cref="T:System.Collections.Generic.IDictionary`2"/>.
        /// </returns>
        /// <exception cref="T:System.ArgumentNullException"><paramref name="key"/> is null.</exception>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.IDictionary`2"/> is read-only.</exception>
        public bool Remove(TKey key)
        {
            return dict.Remove(key);
        }

        /// <summary>
        /// Gets the value associated with the specified key.
        /// </summary>
        /// <param name="key">The key whose value to get.</param>
        /// <param name="value">When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the type of the <paramref name="value"/> parameter. This parameter is passed uninitialized.</param>
        /// <returns>
        /// true if the object that implements <see cref="T:System.Collections.Generic.IDictionary`2"/> contains an element with the specified key; otherwise, false.
        /// </returns>
        /// <exception cref="T:System.ArgumentNullException"><paramref name="key"/> is null.</exception>
        public bool TryGetValue(TKey key, out TValue value)
        {
            return dict.TryGetValue(key, out value);
        }

        /// <summary>
        /// Gets an <see cref="T:System.Collections.Generic.ICollection`1"/> containing the values in the <see cref="T:System.Collections.Generic.IDictionary`2"/>.
        /// </summary>
        /// <returns>An <see cref="T:System.Collections.Generic.ICollection`1"/> containing the values in the object that implements <see cref="T:System.Collections.Generic.IDictionary`2"/>.</returns>
        public ICollection<TValue> Values
        {
            get { return dict.Values; }
        }

        /// <summary>
        /// Removes all items from the <see cref="T:System.Collections.Generic.ICollection`1"/>.
        /// </summary>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only. </exception>
        public void Clear()
        {
            dict.Clear();
        }

        #endregion

        #region ICollection<KeyValuePair<TKey,TValue>> Members

        void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
        {
            ((ICollection<KeyValuePair<TKey, TValue>>)dict).Add(item);
        }

        void ICollection<KeyValuePair<TKey, TValue>>.Clear()
        {
            ((ICollection<KeyValuePair<TKey, TValue>>)dict).Clear();
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
        {
            return ((ICollection<KeyValuePair<TKey, TValue>>)dict).Contains(item);
        }

        void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
        {
            ((ICollection<KeyValuePair<TKey, TValue>>)dict).CopyTo(array, arrayIndex);
        }

        int ICollection<KeyValuePair<TKey, TValue>>.Count
        {
            get { return ((ICollection<KeyValuePair<TKey, TValue>>)dict).Count; }
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly
        {
            get { return ((ICollection<KeyValuePair<TKey, TValue>>)dict).IsReadOnly; }
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
        {
            return ((ICollection<KeyValuePair<TKey, TValue>>)dict).Remove(item);
        }

        #endregion

        #region IEnumerable<KeyValuePair<TKey,TValue>> Members

        IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
        {
            return ((IEnumerable<KeyValuePair<TKey, TValue>>)dict).GetEnumerator();
        }

        #endregion

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable)dict).GetEnumerator();
        }

        #endregion
    }

As you can see it just delegates every call to the underlying dictionary, with the exception of the index-method where it returns the defaultValue if the key does not exist. The DefaultDictionary is used to store the function-delegates associated with their names, and a default-action to perform should the command not be known. As we get further on you will also see that the DefualtDictionary is useful for other aspects of the irc-library too.

The skeletal structure of the IrcClient

Now it’s about time we start writing the actual IrcClient-class. The IrcClass needs to delegate the events fired by the MessageConnection with the exception of the MessageEvent, it should contain Connect and ConnectAsync methods, and it must allow the user of the library to select Encoding to be used, and Nick and Fullname of the connected user (also, at a later point I will add the functionality to specify the client-name by subclassing the IrcClient-class. All in all it’s a simple class, so I’m just going ahead and posting the code as most of it’s been explained earlier.

    /// <summary>
    /// An IRC-client used to connect to and chat at a irc-network.
    /// </summary>
    public class IrcClient
    {
        private static Logger logger = LogManager.GetCurrentClassLogger();

        #region Private Members
        private readonly MessageConnection client;
        private Encoding encoding;
        private string nick;
        private string fullName;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="IrcClient"/> class.
        /// </summary>
        /// <param name="nick">The nick.</param>
        /// <param name="fullName">The full name.</param>
        /// <param name="encoding">The encoding.</param>
        public IrcClient(string nick, string fullName, Encoding encoding = null)
        {
            if (encoding == null)
                encoding = Encoding.UTF8;

            this.nick = nick;
            this.fullName = fullName;
            this.encoding = encoding;

            client = new MessageConnection();
            client.Disconnect += new EventHandler<EventArgs>(client_Disconnect);
            client.SslValidate += new EventHandler<SslValidateEventArgs>(client_SslValidate);
            client.Message += new EventHandler<MessageEventArgs>(client_Message);
        }
        #endregion

        #region Client Events
        void client_Message(object sender, MessageEventArgs e)
        {
            throw new NotImplementedException("Implement handling commands.");
        }

        void client_SslValidate(object sender, SslValidateEventArgs e)
        {
            e.Accept = OnSslValidate(e.Sender, e.Certificate, e.Chain, e.SslPolicyErrors);
        }

        void client_Disconnect(object sender, EventArgs e)
        {
            OnDisconnect();
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets a value indicating whether this <see cref="MessageConnection"/> is connected.
        /// </summary>
        /// <value>
        ///   <c>true</c> if connected; otherwise, <c>false</c>.
        /// </value>
        public bool Connected
        {
            get
            {
                return client != null && client.Connected;
            }
        }
        #endregion

        #region Events
        /// <summary>
        /// Occurs when a SSL-certificate should be validated.
        /// </summary>
        public event EventHandler<SslValidateEventArgs> SslValidate;

        /// <summary>
        /// Occurs when the connection is disconnected from the other end.
        /// </summary>
        public event EventHandler<EventArgs> Disconnect;
        #endregion

        #region Public Methods
        /// <summary>
        /// Connects the specified hostname.
        /// </summary>
        /// <param name="hostname">The hostname.</param>
        /// <param name="port">The port.</param>
        /// <param name="secureConnection">if set to <c>true</c> connects using ssl.</param>
        /// <returns>A task representing the async operation.</returns>
        public Task ConnectAsync(string hostname, int port, bool secureConnection)
        {
            return client.ConnectAsync(hostname, port, secureConnection).ContinueWith(task =>
            {
                throw new NotImplementedException("Implement sending of commands.");
                // PASS *
                // NICK <nick>
                // USER dotRant 8 * :<full name>
            });
        }
        /// <summary>
        /// Connects the specified hostname.
        /// </summary>
        /// <param name="hostname">The hostname.</param>
        /// <param name="port">The port.</param>
        /// <param name="secureConnection">if set to <c>true</c> connects using ssl.</param>
        /// <returns>A task representing the async operation.</returns>
        public void Connect(string hostname, int port, bool secureConnection)
        {
            ConnectAsync(hostname, port, secureConnection).Wait();
        }
        #endregion

        #region EventHelpers
        /// <summary>
        /// Called when a ssl-certificate should be validated.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="certificate">The certificate.</param>
        /// <param name="chain">The chain.</param>
        /// <param name="sslPolicyErrors">The SSL policy errors.</param>
        /// <returns>Whether or not to accept the sertificate.</returns>
        protected virtual bool OnSslValidate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            SslValidateEventArgs args = new SslValidateEventArgs(sender, certificate, chain, sslPolicyErrors);
            if (SslValidate != null)
                SslValidate(this, args);
            return args.Accept;
        }

        /// <summary>
        /// Called when the other end disconnects.
        /// </summary>
        protected virtual void OnDisconnect()
        {
            if (Disconnect != null)
                Disconnect(this, new EventArgs());
        }
        #endregion
    }

As always, if there are any questions, please leave a comment below. The code for this post can be found here: https://github.com/Alxandr/dotRant—Old-Edition/tree/part-4/src/dotRant.

Until then. Alxandr.



Viewing all articles
Browse latest Browse all 11

Trending Articles