Introduction to the UCMA API - Part 2 - Constructing our client

Today we will start discussing coding details of the UCMA API. First I should explain what the purpose of this API is. As I mentioned yesterday, Speech Server is built on top of UCMA. In essence, UCMA enables you to create a SIP endpoint. If you do not know what a SIP endpoint is, I suggest you do a search for the many SIP resources on the Internet before continuing with this series because knowledge of SIP is essential to understanding UCMA.

 

This SIP endpoint can be either a server or a client. In this series, we will create both. Since Speech Server is really just another SIP endpoint, we use UCMA under the covers to handle all of the SIP signaling that is necessary. So, when you are creating a new UCMA app you are creating an application similar to Speech Server – though of course it may have a completely different purpose. I’m sure some of you are wondering – “how can I get Speech Server and my app to play together”. This is not an easy question, but I hope to be able to cover it to some degree in this series. Before we can handle a tough issue like that though, we need to understand UCMA much better.

 

I find it’s easiest to learn an API by creating a sample application. During this series we will slowly add to an application called IDKStudio. IDK here stands for “I Don’t Know” which is the truth. I have absolutely no idea what this application will do except that it will contain a client and a server. Most likely, it will achieve nothing of value but will most likely wind up calling most of the methods in UCMA.

 

Note about code: Obviously there is a lot of code here. With each day I will post a VS solution containing the code up to and including that day. If you prefer to build things yourself to see how they work, feel free to follow along. If you are a bit more lazy (like me) feel free to download today’s code.

 

Today, we will start with our client. Note that we will not do much UCMA coding today. We have a lot of coding to do today to create the infrastructure for our client. To create the client, follow these steps.

 

1) In Visual Studio, create a new Windows Forms application called IDKStudioClient. We will use windows forms for debugging purposes as well as collect info that we may need.

2) Rename the form to IDKStudioClientForm. The next several steps create a framework that we will use for our application. Bear with me for the Windows Forms code that will enable us to add UCMA code soon. Also please note that this Windows Forms code is not representative of what you would do for a shipping app – it is meant to be ‘throwaway’ code.

3) Setup your form similar to the following. I have labeled my controls as follows. The list box will display messages for debugging purposes. The start button starts doing whatever we intend to do and the options button will bring up an options dialog that we will add to.

a. ListBox – lbxProgress

b. Buttons – btnStart, btnOptions

4) Before we add code to our form, lets create the options dialog. Add a new form called OptionsForm and design it as follows.

Most of these we will not make use of today. They are intended for future use. The following are the Ids I used.

        private System.Windows.Forms.ComboBox cbxCertificate;

        private System.Windows.Forms.Label lblCertificate;

        private System.Windows.Forms.TextBox txtOurServer;

        private System.Windows.Forms.Label lblOurServer;

        private System.Windows.Forms.Button btnApply;

        private System.Windows.Forms.Button btnCancel;

        private System.Windows.Forms.Button btnOK;

        private System.Windows.Forms.GroupBox groupBox1;

        private System.Windows.Forms.RadioButton rdbTcp;

        private System.Windows.Forms.RadioButton rdbTls;

        private System.Windows.Forms.Label lblOCSServer;

        private System.Windows.Forms.TextBox txtOCSServer;

        private System.Windows.Forms.Label lblUri;

  private System.Windows.Forms.TextBox txtUri;

5) We are about to reference our first bit of UCMA code so you will need to add a reference to Microsoft.Rtc.Collaboration.dll in your project.

6) We will now create a settings class that will allow us to store our data. Originally when I created this sample I stored everything in the registry. Of course, that doesn’t help when multiple clients are running on the same box so instead I store it in an XML file. The code for the new Settings.cs file is

 

using System;

using System.Collections.Generic;

using System.Text;

using System.Xml;

using System.Reflection;

using System.IO;

using Microsoft.Rtc.Signaling;

namespace IDKStudioClient

{

    public sealed class Settings

    {

        #region Private member variables

        private static string _certificateIssuerName;

        private static Byte[] _certificateSerialNumber;

        private static string _ourServerName;

        private static string _ocsServerName;

        private static string _uri;

        private static SipTransportType _transportType;

        private const string XmlFileName = "Settings.xml";

        private const string Key_CertificateIssuerName = "certificateissuername";

        private const string Key_CertificateSerialNumber = "certificateserialnumber";

        private const string Key_OurServerName = "ourserver";

        private const string Key_OCSServer = "ocsserver";

        private const string Key_Uri = "uri";

        private const string Key_TransportType = "transporttype";

        private const string Element_Settings = "settings";

        #endregion

        #region Static constructor

        /// <summary>

        /// Reads all settings the first time we are called

        /// </summary>

        static Settings()

        {

            ReadAll();

        }

        #endregion

        #region Properties

        /// <summary>

        /// The issuer of the certificate

        /// </summary>

        public static string CertificateIssuerName

        {

            get { return _certificateIssuerName; }

            set { _certificateIssuerName = value; }

     }

        /// <summary>

        /// The serial number of the certificate

        /// </summary>

        public static Byte[] CertificateSerialNumber

        {

            get { return _certificateSerialNumber; }

            set { _certificateSerialNumber = value; }

        }

        /// <summary>

        /// The name of the server we are building

        /// </summary>

        public static string OurServerName

        {

            get { return _ourServerName; }

            set { _ourServerName = value; }

        }

        /// <summary>

        /// The OCS server we will sign in to

        /// </summary>

        public static string OCSServerName

        {

            get { return _ocsServerName; }

            set { _ocsServerName = value; }

        }

        /// <summary>

        /// Our sip address

        /// </summary>

        public static string Uri

        {

            get { return _uri; }

            set { _uri = value; }

        }

        /// <summary>

        /// The transport type we will use

        /// </summary>

        public static SipTransportType TransportType

        {

            get { return _transportType; }

            set { _transportType = value; }

        }

        #endregion

        #region Helper methods

        /// <summary>

        /// Reads all properties

        /// </summary>

        private static void ReadAll()

        {

            // If the xml file does not exist, read nothing

            string xmlFilePath = XmlFilePath;

            if (false == File.Exists(xmlFilePath))

            {

                return;

            }

            // Close the input stream on closing the reader

            XmlReaderSettings settings = new XmlReaderSettings();

            settings.CloseInput = true;

            // Open the xml file - let the exception bubble up to the user

            XmlReader reader = XmlReader.Create(new StreamReader(xmlFilePath), settings);

            // Read our settings

            while (reader.Read())

            {

                if (true == reader.IsStartElement())

                {

                    switch (reader.Name)

                    {

                        case Key_CertificateIssuerName:

                            _certificateIssuerName = reader.ReadString();

                            break;

                        case Key_CertificateSerialNumber:

                            _certificateSerialNumber = new byte[10];

                    reader.ReadElementContentAsBinHex(__certificateSerialNumber, 0, 10);

                            break;

                        case Key_OCSServer:

                            _ocsServerName = reader.ReadString();

                            break;

                        case Key_OurServerName:

                            _ourServerName = reader.ReadString();

                            break;

                        case Key_TransportType:

                            // Let the parsing exception bubble up to the user

                            string transportType = reader.ReadString();

                            _transportType = (SipTransportType)Enum.Parse(typeof(SipTransportType), transportType, true);

                            break;

     case Key_Uri:

                            _uri = reader.ReadString();

                            break;

                    }

                }

            }

            // Clean up

            reader.Close();

        }

        /// <summary>

        /// Saves all properties

        /// </summary>

        public static void SaveAll()

        {

            // Open or create the XML file.

            XmlWriterSettings settings = new XmlWriterSettings();

            XmlWriter writer = XmlWriter.Create(XmlFileName);

            // <settings>

            writer.WriteStartElement(Element_Settings);

            // Write the individual properties

            writer.WriteStartElement(Key_CertificateIssuerName);

            writer.WriteString(_certificateIssuerName);

            writer.WriteEndElement();

            writer.WriteStartElement(Key_OCSServer);

            writer.WriteString(_ocsServerName);

            writer.WriteEndElement();

            writer.WriteStartElement(Key_OurServerName);

            writer.WriteString(_ourServerName);

            writer.WriteEndElement();

            writer.WriteStartElement(Key_TransportType);

            writer.WriteString(_transportType.ToString());

            writer.WriteEndElement();

            writer.WriteStartElement(Key_Uri);

            writer.WriteString(_uri);

            writer.WriteEndElement();

            writer.WriteStartElement(Key_CertificateSerialNumber);

            writer.WriteBinHex(_certificateSerialNumber, 0, _certificateSerialNumber.Length);

            writer.WriteEndElement();

            writer.Close();

        }

        /// <summary>

        /// Gets the path to the XML file

        /// </summary>

        private static string XmlFilePath

        {

            get

            {

                // Look in the same directory as our assembly

                string ourDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().FullName);

                string xmlFilePath = Path.Combine(ourDirectory, XmlFileName);

                return xmlFilePath;

            }

        }

        #endregion

    }

}

 

7) We now need to add a new CertificateEntry.cs that will handle the certificates for us. If you are still unsure why we need certificates – hang on. We will cover that later.

 

using System;

using System.Collections.Generic;

using System.Text;

using System.Security.Cryptography;

using System.Security.Cryptography.X509Certificates;

namespace IDKStudioClient

{

    /// <summary>

    /// Handles certificate entries

    /// </summary>

    public class CertificateEntry

    {

        private string _description;

        private string _issuerName;

        private Byte[] _serialNumber;

        /// <summary>

        /// Creates a new CertificateEntry instance

        /// </summary>

        /// <param name="description">The certificate</param>

        public CertificateEntry(X509Certificate2 certificate)

        {

            _description = certificate.GetNameInfo(X509NameType.SimpleName, true) + " " +

                                certificate.GetSerialNumberString();

            _issuerName = certificate.Issuer;

            _serialNumber = certificate.GetSerialNumber();

        }

        /// <summary>

        /// The issuer name

        /// </summary>

        public string IssuerName

        {

            get { return _issuerName; }

        }

        /// <summary>

        /// The serial number

        /// </summary>

        public Byte[] SerialNumber

        {

            get { return _serialNumber; }

        }

        public override string ToString()

        {

            return _description;

        }

        /// <summary>

        /// Determines whether two serial numbers are equal

        /// </summary>

        /// <param name="serial1">The first serial number</param>

        /// <param name="serial2">The second serial number</param>

        /// <returns>True if they are equal</returns>

        public static bool SerialNumbersEqual(Byte[] serial1, Byte[] serial2)

        {

            // If one is null we don't care but don't continue

            if (serial1 == null || serial2 == null)

            {

                return false;

            }

            // Make sure they are the same length

            if (serial1.Length != serial2.Length)

            {

                return false;

            }

            // Iterate through the arrays to see if they are the same

            int length = serial1.Length;

            for (int count = 0; count < length; count++)

            {

                if (serial1[count] != serial2[count])

                {

                    return false;

                }

            }

            return true;

        }

    }

}

 

8) OK, let’s get our options form to do something. Let’s start with some code to set the default values.

 

        /// <summary>

        /// Sets the default values of the form

        /// </summary>

        private void SetDefaultValues()

        {

            // Set the values of the controls

            txtOCSServer.Text = Settings.OCSServerName;

            txtOurServer.Text = Settings.OurServerName;

            txtUri.Text = Settings.Uri;

            if (Settings.TransportType == SipTransportType.Tls)

            {

                rdbTls.Checked = true;

            }

            else

            {

                rdbTcp.Checked = true;

            }

            // Load the certificates

            X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);

            try

            {

                store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

            }

            catch (System.Security.SecurityException)

            {

                MessageBox.Show("You don't have permissions to enumerate the certificates from the local computer store");

                return;

            }

            // Make sure we have certificates

            if (store.Certificates.Count < 1)

            {

                MessageBox.Show("Make sure to place certificates in the store.");

                return;

            }

            // Add the certificates to the combo box

            cbxCertificate.Items.Clear();

            Byte[] currentSerial = Settings.CertificateSerialNumber;

            foreach (X509Certificate2 certificate in store.Certificates)

            {

                CertificateEntry entry = new CertificateEntry(certificate);

                cbxCertificate.Items.Add(entry);

                // See if this is the selected certificate

                if (true == CertificateEntry.SerialNumbersEqual(entry.SerialNumber, currentSerial))

                {

                    cbxCertificate.SelectedIndex = cbxCertificate.Items.Count - 1;

                }

            }

            // Set the first certificate if we don't have one

            if (null == currentSerial)

            {

                cbxCertificate.SelectedIndex = 0;

            }

        }

 

9) Now let’s create the code to write the values back to the settings file.

 

        /// <summary>

        /// Records new values

        /// </summary>

        void RecordNewValues()

        {

            // Read the values

            string ourServer = txtOurServer.Text.Trim();

            string uri = txtUri.Text.Trim();

            string ocsServer = txtOCSServer.Text.Trim();

            // Make sure we have a server and uri

     if (true == String.IsNullOrEmpty(ourServer))

            {

                MessageBox.Show("Cannot save values - you must specify our server.");

                return;

            }

            if (true == String.IsNullOrEmpty(uri))

            {

                MessageBox.Show("Cannot save values - you must specify a uri.");

                return;

            }

            if (true == String.IsNullOrEmpty(ocsServer))

            {

                MessageBox.Show("Cannot save values - you must specify an ocs server.");

                return;

            }

            // Save the values

            Settings.OurServerName = ourServer;

            Settings.Uri = uri;

            Settings.OCSServerName = ocsServer;

            if (true == rdbTls.Checked)

            {

                Settings.TransportType = SipTransportType.Tls;

            }

            else

            {

                Settings.TransportType = SipTransportType.Tcp;

            }

            // Save the certificate

            CertificateEntry entry = (CertificateEntry)cbxCertificate.Items[cbxCertificate.SelectedIndex];

            Settings.CertificateIssuerName = entry.IssuerName;

            Settings.CertificateSerialNumber = entry.SerialNumber;

            // Save the settings

            Settings.SaveAll();

        }

10) To complete the options form, we just need to add the event handlers for the OK and apply buttons. We also need to set the default values in the constructor.

 

        public OptionsForm()

        {

            InitializeComponent();

            SetDefaultValues();

        }

        private void btnApply_Click(object sender, EventArgs e)

        {

            RecordNewValues();

        }

        private void btnOK_Click(object sender, EventArgs e)

        {

            RecordNewValues();

            this.DialogResult = DialogResult.OK;

            this.Close();

        }

11) Now we need to finish our main form. First let’s add an event handler for the options button.

 

        private void btnOptions_Click(object sender, EventArgs e)

        {

            OptionsForm options = new OptionsForm();

            options.ShowDialog();

        }

12) Now let’s add two delegates other objects will use to pass status back to our main form for display.

 

        private delegate void UpdateProgressDelegate(string Message);

        private delegate void EnableButtonDelegate(Button button, string label);

13) In the constructor, let’s handle the double click event so we can display more info about a particular progress item.

 

lbxProgress.DoubleClick += new EventHandler(lbxProgress_DoubleClick);

 

14) Now let’s implement that event.

 

        private void btnOptions_Click(object sender, EventArgs e)

        {

            OptionsForm options = new OptionsForm();

            options.ShowDialog();

        }

 

 

I think that’s enough code for today. OK, so we didn’t see much of the UCMA API today, but we will tomorrow as we get our client to sign in.