Using UCMA 3.0 BackToBackCall: Code Listing (Part 3 of 4)

Summary:   The BackToBackCall class is an important addition to Microsoft Unified Communications Managed API (UCMA) 3.0. Part 3 contains code listings for the application discussed in this series of articles.

Applies to:   Microsoft UCMA 3.0 SDK | Microsoft Lync Server 2010

Published:   May 2011 | Provided by:   Mark Parker, Microsoft | About the Author

Contents

  • Application Configuration

  • Application Code

  • Helper Class

  • Part 4

  • Additional Resources

Download code  Download code

This article is the third in a four-part series of articles on how to use the BackToBackCall class.

Application Configuration

App.Config, the application configuration file, is used to configure settings for the computer that is hosting the application, and the remote endpoint. When the appropriate parameters are entered in the add elements (and the XML comment delimiters are removed), they do not have to be entered from the keyboard when the application is running.

The following example shows the App.Config file.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>

    <!-- Provide parameters necessary for the sample to run without prompting for user input. -->

    <!-- Provide the FQDN of the Microsoft Lync Server 2010 computer.-->
    <!-- <add key="ServerFQDN1" value=""/> -->

    <!-- The user sign-in name that is used to sign in to the application. -->
    <!-- To use credentials used by the currently signed-in user, do not add a value. -->
    <!-- <add key="UserName1" value=""/> -->

    <!-- The user domain name that is used to sign in to the application. -->
    <!-- To use credentials used by the currently signed-in user, do not add a value. -->
    <!-- <add key="UserDomain1" value=""/> -->

    <!-- The user URI that is used to sign in to the application, in the format user@host. -->
    <!-- <add key="UserURI1" value=""/> -->

    <!-- The URI of the remote endpoint, in the format sip:user@host. -->
    <!--This is the URI of the agent. -->
    <!-- <add key="RemoteUserURI1" value=""/> -->
  </appSettings>
</configuration>

Application Code

The following example creates the BackToBackCall object and defines handlers for events of interest.

using System;
using System.Threading;
using Microsoft.Rtc.Collaboration;
using Microsoft.Rtc.Collaboration.AudioVideo;
using Microsoft.Rtc.Signaling;
using Microsoft.Rtc.Collaboration.Sample.Common;

namespace Microsoft.Rtc.Collaboration.Sample.B2BCall
{
  // This application sets up a BackToBackCall instance that connects an incoming AudioVideoCall from a customer 
  // to an outgoing AudioVideoCall to an agent.
  // This application logs in as the user listed in UserName1, in App.Config.  
  // When this application starts, it creates, but does not establish, an AudioVideoCall instance.
  // When a remote user (the customer) calls this application, a delegate creates and establishes 
  // the BackToBackCall instance that is used.
  // At this point the customer and agent are connected, and can carry on a conversation.
  // At the conclusion of the conversation, the application tears down the platform and ends, and then pauses
  // to allow the log messages to be viewed in the console.
  // 
  // This application requires the credentials of three users who can sign in to  
  // Microsoft Lync Server 2010, and who are enabled for voice.

  public class UCMAB2BCallExample
  {
    // Some necessary instance variables
    private UCMASampleHelper _helper;
    private AudioVideoCall _incomingAVCall;
    private AudioVideoCall _outgoingAVCall;
    private UserEndpoint _userEndpoint;

    // The information for the conversation and the far end participant.
    // The target of the call (agent) in the format sip:user@host (should be logged on when the application is run). This could also be in the format tel:+1XXXYYYZZZZ
    private static String _calledParty;

    // Conversation subject that appears at the top of the Lync 2010 conversation window for the incoming caller.
    private static String _outConversationSubject = "UCMA to agent conversation leg"; 
    
    private static String _conversationPriority = ConversationPriority.Urgent;

    // The conversations.
    // The conversation between the customer and the UCMA application.
    private Conversation _incomingConversation;
    // The conversation between the UCMA application and the agent.
    private Conversation _outgoingConversation;

    // BackToBackCall and associated fields.
    private BackToBackCall _b2bCall;
    // _outgoingCallLeg contains the settings for the leg from the UCMA application to the agent.
    private BackToBackCallSettings _outgoingCallLeg;
    // _incomingCallLeg contains the settings for the leg from the customer to the UCMA application.
    private BackToBackCallSettings _incomingCallLeg;

    
    // Wait handles are used to synchronize the main thread and the worker thread that is
    // used for callbacks and event handlers.
    private AutoResetEvent _waitForConversationToTerminate = new AutoResetEvent(false);
    private AutoResetEvent _waitForCallToEstablish = new AutoResetEvent(false);
    private AutoResetEvent _waitForB2BCallToEstablish = new AutoResetEvent(false);
    private AutoResetEvent _waitUntilOneUserHangsUp = new AutoResetEvent(false);
    private AutoResetEvent _waitForB2BCallToTerminate = new AutoResetEvent(false);
    
    
    static void Main(string[] args)
    {
      UCMAB2BCallExample BasicB2BCallCall = new UCMAB2BCallExample();
      BasicB2BCallCall.Run();
    }

    public void Run()
    {
      
      // Initialize and register the endpoint, using the credentials of the user the application will be acting as.
      _helper = new UCMASampleHelper();
      _userEndpoint = _helper.CreateEstablishedUserEndpoint("B2BCall Sample User" /*endpointFriendlyName*/);
      _userEndpoint.RegisterForIncomingCall<AudioVideoCall>(incomingAVCall_CallReceived);

      // Conversation settings for the outgoing call (to the agent).
      ConversationSettings outConvSettings = new ConversationSettings();
      outConvSettings.Priority = _conversationPriority;
      outConvSettings.Subject = _outConversationSubject;
 
      // Create the Conversation instance between the application and the agent.
      _outgoingConversation = new Conversation(_userEndpoint, outConvSettings);

      // Create the outgoing call between the application and the agent.
      _outgoingAVCall = new AudioVideoCall(_outgoingConversation);

      // Register for notification of the StateChanged event on the outgoing call. 
      _outgoingAVCall.StateChanged += new EventHandler<CallStateChangedEventArgs>(outgoingAVCall_StateChanged);

      // Prompt for called party - the agent.
      _calledParty = UCMASampleHelper.PromptUser("Enter the URI of the called party, in sip:User@Host form or tel:+1XXXYYYZZZZ form => ", "RemoteUserURI1");

      _outgoingCallLeg = new BackToBackCallSettings(_outgoingAVCall, _calledParty);

      // Pause the main thread until both calls, the BackToBackCall, both conversations,
      // and the platform are shut down.
      _waitUntilOneUserHangsUp.WaitOne();

      // Pause the console to allow for easier viewing of logs.
      Console.WriteLine("Press any key to end the sample.");
      Console.ReadKey();
    }

    // Shut down the BackToBackCall, both Conversations, and the platform.
    void FinishShutdown()
    {
      _b2bCall.BeginTerminate(B2BTerminateCB, _b2bCall);
      _waitForB2BCallToTerminate.WaitOne();

      _outgoingAVCall.Conversation.BeginTerminate(TerminateConversationCB, _outgoingAVCall.Conversation);
      _incomingAVCall.Conversation.BeginTerminate(TerminateConversationCB, _incomingAVCall.Conversation);

      Console.WriteLine("Waiting for the conversation to be terminated...");
      _waitForConversationToTerminate.WaitOne();

      // Now, clean up by shutting down the platform.
      Console.WriteLine("Shutting down the platform...");
      _helper.ShutdownPlatform();
      _waitUntilOneUserHangsUp.Set();
    }

    #region EVENT HANDLERS

    // Handler for the StateChanged event on the incoming call. 
    void incomingAVCall_StateChanged(object sender, CallStateChangedEventArgs e)
    {
      Console.WriteLine("Incoming call - state change.\nPrevious state: "
              + e.PreviousState + "\nCurrent state: " + e.State
              + "\nTransitionReason: " + e.TransitionReason + "\n"); 
      if (e.TransitionReason == CallStateTransitionReason.TerminatedRemotely)
      {
        // If one call has been terminated remotely, unregister both calls for 
        // notification of the StateChanged event.
        _outgoingAVCall.StateChanged -= outgoingAVCall_StateChanged;
        _incomingAVCall.StateChanged -= incomingAVCall_StateChanged;
        _incomingAVCall.BeginTerminate(TerminateCallCB, _incomingAVCall);
        
        if (_outgoingAVCall != null)
        {
          Console.WriteLine("Terminating the incoming call...");
          _outgoingAVCall.BeginTerminate(TerminateCallCB, _outgoingAVCall);
        }

        FinishShutdown(); 
      }
    }

    // Handler for the StateChanged event on the outgoing call.
    void outgoingAVCall_StateChanged(object sender, CallStateChangedEventArgs e)
    {
      Console.WriteLine("Outgoing call - state change.\nPrevious state: " 
              + e.PreviousState + "\nCurrent state: " + e.State
              + "\nTransitionReason: " + e.TransitionReason + "\n");
      if (e.TransitionReason == CallStateTransitionReason.TerminatedRemotely)
      {
        // If one call has been terminated remotely, unregister both calls for 
        // notification of the StateChanged event.
        _incomingAVCall.StateChanged -= incomingAVCall_StateChanged;
        _outgoingAVCall.StateChanged -= outgoingAVCall_StateChanged;
        _outgoingAVCall.BeginTerminate(TerminateCallCB, _outgoingAVCall);
        
        if (_incomingAVCall != null)
        {
          Console.WriteLine("Terminating the incoming call...");
          _incomingAVCall.BeginTerminate(TerminateCallCB, _incomingAVCall);
        }

        FinishShutdown(); 
      }
    }


    // The delegate to be called when the incoming call arrives (the call from a customer). 
    private void incomingAVCall_CallReceived(object sender, CallReceivedEventArgs<AudioVideoCall> e)
    {
      _incomingAVCall = e.Call;
      // Register for notification of the StateChanged event on the incoming call.
      _incomingAVCall.StateChanged += new EventHandler<CallStateChangedEventArgs>(incomingAVCall_StateChanged);

      // Create a new conversation for the incoming call leg.
      _incomingConversation = new Conversation(_userEndpoint);
      _incomingCallLeg = new BackToBackCallSettings(_incomingAVCall);
      
      // Create the back-to-back call instance.
      // Note that you need a Destination URI for the outgoing call leg, but not for the incoming call leg.
      _b2bCall = new BackToBackCall(_incomingCallLeg, _outgoingCallLeg);
      
      // Begin the back-to-back session; provide a destination.
      try
      {
        IAsyncResult result = _b2bCall.BeginEstablish(BeginEstablishCB, _b2bCall);
      }
      catch (InvalidOperationException ioe)
      {
        Console.WriteLine("_b2bCall must be in the Idle state." + ioe.Message.ToString());
      }
      _waitForB2BCallToEstablish.WaitOne();
    }

    #endregion

    #region CALLBACKs

    // Callback for BeginTerminate on a call.
    private void TerminateCallCB(IAsyncResult ar)
    {
      AudioVideoCall audioVideoCall = ar.AsyncState as AudioVideoCall;

      // Finish terminating the call.
      audioVideoCall.EndTerminate(ar);
    }

    // Callback for BeginTerminate on the Conversation object.
    private void TerminateConversationCB(IAsyncResult ar)
    {
      Conversation conv = ar.AsyncState as Conversation;

      // Finish terminating the conversation.
      conv.EndTerminate(ar);

      // Synchronize the main and worker threads.
      _waitForConversationToTerminate.Set();
    }
    

    // Callback for BeginEstablish on the BackToBackCall instance.
    private void BeginEstablishCB(IAsyncResult ar)
    {
      _b2bCall.EndEstablish(ar);
      _waitForB2BCallToEstablish.Set();
    }

    // Callback for BeginTerminate on the BackToBackCall instance.
    private void B2BTerminateCB(IAsyncResult ar)
    {
      BackToBackCall b2bCall = ar as BackToBackCall;
      if (b2bCall != null)
      {
        b2bCall.EndEstablish(ar);
      }
      _waitForB2BCallToTerminate.Set();
    }

    #endregion

  }
}

Helper Class

The next example consists of a class whose member methods are used to create and start a CollaborationPlatform instance, and then create and establish the UserEndpoint instance that is used in the application. The code appearing in this article is an abbreviated version of the code that appears in the UCMA 3.0 SDK UCMASampleCode.cs file.

The following code differs from UCMASampleCode.cs in the following ways:

  1. Methods that are related to creating and establishing an ApplicationEndpoint instance are removed:

    • CreateUserEndpointWithServerPlatform

    • ReadGenericApplicationContactConfiguration

    • ReadApplicationContactConfiguration

    • CreateApplicationEndpoint

    • CreateAndStartServerPlatform

  2. A number of unused private fields are excluded. Code that uses the _serverCollabPlatform field is excluded.

  3. The _remoteUserURIPrompt and _remoteUserURI variables have been added to the code.

  4. The GetRemoteUserURI method is added to the code.

UCMASampleHelper.cs (abbreviated)

The following example contains a class that has helper methods that are used to create an established endpoint.

using System;
using System.Configuration;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;

using Microsoft.Rtc.Collaboration;
using Microsoft.Rtc.Signaling;

namespace Microsoft.Rtc.Collaboration.Sample.Common
{
  class UCMASampleHelper
  {
    private static ManualResetEvent _sampleFinished = new ManualResetEvent(false);

    // The name of this application, to be used as the outgoing user agent string.
    // The user agent string is put in outgoing message headers to indicate the application that is used.
    private static string _applicationName = "UCMASampleCode";

    const string _sipPrefix = "sip:";

    // These strings are used as keys into the App.Config file to get information to avoid prompting. For most of these strings,
    // suffixes 1-N are used on each subsequent call. For example, UserName1 is used for the first user and UserName2 for the second user.
    private static String _serverFQDNPrompt = "ServerFQDN";
    private static String _userNamePrompt = "UserName";
    private static String _userDomainPrompt = "UserDomain";
    private static String _userURIPrompt = "UserURI";
    private static String _remoteUserURIPrompt = "UserURI";

    // Construct the network credential that the UserEndpoint will use for authentication by the Microsoft Lync Server 2010 computer.
    private string _userName; // User name and password for pair of a user who is authorized to access Lync Server 2010. 
    private string _userPassword;
    private string _userDomain; // Domain that this user signs in to. Note: This is the Active Directory domain, not the portion of the SIP URI following the ‘@’ sign.
    private System.Net.NetworkCredential _credential;

    // The user URI and connection server of the user used.
    private string _userURI; // This should be the URI of the user specified earlier.
    private string _remoteUserURI; // The URI of the remote endpoint.

    // The Server FQDN.
    private static string _serverFqdn;// The FQDN of the Microsoft Lync Server 2010 computer.

    // Transport type used to communicate with Microsoft Lync Server 2010 computere.
    private Microsoft.Rtc.Signaling.SipTransportType _transportType = Microsoft.Rtc.Signaling.SipTransportType.Tls;

    private static CollaborationPlatform _collabPlatform;
    private static bool _isPlatformStarted;
    private AutoResetEvent _platformStartupCompleted = new AutoResetEvent(false);
    private AutoResetEvent _endpointInitCompletedEvent = new AutoResetEvent(false);
    private AutoResetEvent _platformShutdownCompletedEvent = new AutoResetEvent(false);
    private UserEndpoint _userEndpoint;

    private bool _useSuppliedCredentials;
    private static int _appContactCount;
    private static int _userCount = 1;

    // This method attempts to read user settings from the App.Config file. If the settings are not
    // present in the configuration file, this method prompts the user for them in the console.
    // This method returns a UserEndpointSettings object. If you do not want to monitor LocalOwnerPresence, you can 
    // call the CreateEstablishedUserEndpoint method directly. Otherwise, you can call the ReadUserSettings,
    // CreateUserEndpoint, and EstablishUserEndpoint methods, in that order.
    public UserEndpointSettings ReadUserSettings(string userFriendlyName)
    {
      UserEndpointSettings userEndpointSettings = null;
      string prompt = string.Empty;
      if (string.IsNullOrEmpty(userFriendlyName))
      {
        userFriendlyName = "Default User";
      }

      try
      {
        Console.WriteLine(string.Empty);
        Console.WriteLine("Creating User Endpoint for {0}...", userFriendlyName);
        Console.WriteLine();

        if (ConfigurationManager.AppSettings[_serverFQDNPrompt + _userCount] != null)
        {
          _serverFqdn = ConfigurationManager.AppSettings[_serverFQDNPrompt + _userCount];
          Console.WriteLine("Using {0} as Microsoft Lync Server", _serverFqdn);
        }
        else
        {
          // Prompt user for server FQDN. If server FQDN was entered previously, then use the saved value.
          string localServer;
          StringBuilder promptBuilder = new StringBuilder();
          if (!string.IsNullOrEmpty(_serverFqdn))
          {
            promptBuilder.Append("Current Microsoft Lync Server = ");
            promptBuilder.Append(_serverFqdn);
            promptBuilder.AppendLine(". Please hit ENTER to retain this setting - OR - ");
          }

          promptBuilder.Append("Please enter the FQDN of the Microsoft Lync Server that the ");
          promptBuilder.Append(userFriendlyName);
          promptBuilder.Append(" endpoint is homed on => ");
          localServer = PromptUser(promptBuilder.ToString(), null);

          if (!String.IsNullOrEmpty(localServer))
          {
            _serverFqdn = localServer;
          }
        }

        // Prompt user for user name
        prompt = String.Concat("Please enter the User Name for ",
                                    userFriendlyName,
                                    " (or hit the ENTER key to use current credentials)\r\n" +
                                    "Please enter the User Name => ");
        _userName = PromptUser(prompt, _userNamePrompt + _userCount);

        // If user name is empty, use current credentials
        if (string.IsNullOrEmpty(_userName))
        {
          Console.WriteLine("Username was empty - using current credentials...");
          _useSuppliedCredentials = true;
        }
        else
        {
          // Prompt for password
          prompt = String.Concat("Enter the User Password for ", userFriendlyName, " => ");
          _userPassword = PromptUser(prompt, null);

          prompt = String.Concat("Please enter the User Domain for ", userFriendlyName, " => ");
          _userDomain = PromptUser(prompt, _userDomainPrompt + _userCount);
        }

        // Prompt user for user URI
        prompt = String.Concat("Please enter the User URI for ", userFriendlyName, " in the User@Host format => ");
        _userURI = PromptUser(prompt, _userURIPrompt + _userCount);
        if (!(_userURI.ToLower().StartsWith("sip:") || _userURI.ToLower().StartsWith("tel:")))
        {
          _userURI = "sip:" + _userURI;
        }
        // Increment the last user number
        _userCount++;

        // Initialize and register the endpoint, using the credentials of the user that the application represents.
        // NOTE: the _userURI should always use the "sip:user@host" format.
        userEndpointSettings = new UserEndpointSettings(_userURI, _serverFqdn);

        if (!_useSuppliedCredentials)
        {
          _credential = new System.Net.NetworkCredential(_userName, _userPassword, _userDomain);
          userEndpointSettings.Credential = _credential;
        }
        else
        {
          userEndpointSettings.Credential = System.Net.CredentialCache.DefaultNetworkCredentials;
        }
      }
      catch (InvalidOperationException iOpEx)
      {
        // InvalidOperationException should be thrown only on poorly-entered input.
        Console.WriteLine("Invalid Operation Exception: " + iOpEx.ToString());
      }

      return userEndpointSettings;
    }

    // This method creates an endpoint, using the specified UserEndpointSettings object.
    // This method returns a UserEndpoint object so that you can register endpoint-specific event handlers. 
    // If you do not want to get endpoint-specific event information at the time the endpoint is established, you can 
    // call the CreateEstablishedUserEndpoint method directly. Otherwise, you can call the ReadUserSettings,
    // CreateUserEndpoint, and EstablishUserEndpoint methods, in that order.
    public UserEndpoint CreateUserEndpoint(UserEndpointSettings userEndpointSettings)
    {
      // Reuse the platform instance so that all endpoints share the same platform.
      if (_collabPlatform == null)
      {
        // Initialize and start the platform.
        ClientPlatformSettings clientPlatformSettings = new ClientPlatformSettings(_applicationName, _transportType);
        _collabPlatform = new CollaborationPlatform(clientPlatformSettings);
      }

      _userEndpoint = new UserEndpoint(_collabPlatform, userEndpointSettings);
      return _userEndpoint;
    }
    

    // This method establishes a previously created UserEndpoint.
    // This method returns an established UserEndpoint object. If you do not want to monitor LocalOwnerPresence, you can 
    // call the CreateEstablishedUserEndpoint method directly. Otherwise, you can call the ReadUserSettings,
    // CreateUserEndpoint, and EstablishUserEndpoint methods, in that order.
    public bool EstablishUserEndpoint(UserEndpoint userEndpoint)
    {
       // Start the platform, if not already started.
       if (_isPlatformStarted == false)
       {
         userEndpoint.Platform.BeginStartup(EndPlatformStartup, userEndpoint.Platform);

         // Wait for the platform startup to be completed.
         _platformStartupCompleted.WaitOne();
         Console.WriteLine("Platform started...");
         _isPlatformStarted = true;
       }
       // Establish the user endpoint.
       userEndpoint.BeginEstablish(EndEndpointEstablish, userEndpoint);

      // Wait until the endpoint is established.
      _endpointInitCompletedEvent.WaitOne();
      Console.WriteLine("Endpoint established...");
      return true;
    }

    

    // This method creates an established UserEndpoint.
    // This method returns an established UserEndpoint object. If you do not want to monitor LocalOwnerPresence, you can 
    // call this CreateEstablishedUserEndpoint method directly. Otherwise, you can call the ReadUserSettings,
    // CreateUserEndpoint, and EstablishUserEndpoint methods, in that order.
    public UserEndpoint CreateEstablishedUserEndpoint(string endpointFriendlyName)
    {
      UserEndpointSettings userEndpointSettings;
      UserEndpoint userEndpoint = null;
      try
      {
        // Read user settings
        userEndpointSettings = ReadUserSettings(endpointFriendlyName);

        // Create User Endpoint
        userEndpoint = CreateUserEndpoint(userEndpointSettings);

        // Establish the user endpoint
        EstablishUserEndpoint(userEndpoint);
      }
      catch (InvalidOperationException iOpEx)
      {
        // InvalidOperationException should be thrown only on poorly-entered input.
        Console.WriteLine("Invalid Operation Exception: " + iOpEx.ToString());
      }

      return userEndpoint;
    }

    // Returns the remote user URI.
    // This method is not present in the original UCMASampleHelper.cs
    public String GetRemoteUserURI()
    {
      String str = "";
      try
      {
        if (ConfigurationManager.AppSettings[_remoteUserURIPrompt + _userCount] != null)
        {
          _remoteUserURI = ConfigurationManager.AppSettings[_remoteUserURIPrompt + _userCount];
          Console.WriteLine("\nUsing {0} as remote user", _remoteUserURI);
          return _remoteUserURI;
        }
        else
        {
          // Prompt user for remote user URI
          _remoteUserURI = UCMASampleHelper.PromptUser("Enter the URI for the remote user logged onto Communicator, in the sip:User@Host format or tel:+1XXXYYYZZZZ format => ", "RemoteUserURI");
          return str;
        }
      }
      catch (InvalidOperationException iOpEx)
      {
        // Invalid Operation Exception should only be thrown on poorly-entered input.
        Console.WriteLine("Invalid Operation Exception: " + iOpEx.ToString());
        return str;
      }
    }

    /// <summary>
    /// If the 'key' is not found in App.Config, prompt the user in the console, using the specified prompt text.
    /// </summary>
    /// <param name="promptText"> The text to be displayed in the console if ‘key’ is not found in App.Config.</param>
    /// <param name="key"> Searches for this key in App.Config and returns a value if found. Pass null to always display a message that requests user input.</param>
    /// <returns>String value either from App.Config or user input.</returns>
    public static string PromptUser(string promptText, string key)
    {
      String value;
      if (String.IsNullOrEmpty(key) || ConfigurationManager.AppSettings[key] == null)
      {
        Console.WriteLine(string.Empty);
        Console.Write(promptText);
        value = Console.ReadLine();
      }
      else
      {
        value = ConfigurationManager.AppSettings[key];
        Console.WriteLine("Using keypair {0} - {1} from AppSettings...", key, value);
      }

      return value;
    }


    private void EndPlatformStartup(IAsyncResult ar)
    {
      CollaborationPlatform collabPlatform = ar.AsyncState as CollaborationPlatform;
      try
      {
        // The platform should now be started.
        collabPlatform.EndStartup(ar);
        // It should be noted that all the re-thrown exceptions will crash the application. This is intentional.
        // Ideal exception handling will report the error and force the application to shut down in an orderly manner. 
        // In production code, consider using an IAsyncResult implementation to report the error 
        // instead of throwing. Alternatively, put the implementation in this try block.
      }
      catch (OperationFailureException opFailEx)
      {
        // OperationFailureException is thrown when the platform cannot establish, usually due to invalid data.
        Console.WriteLine(opFailEx.Message);
        throw;
      }
      catch (ConnectionFailureException connFailEx)
      {
        // ConnectionFailureException is thrown when the platform cannot connect.
        // ClientPlatforms will not throw this exception during startup.
        Console.WriteLine(connFailEx.Message);
        throw;
      }
      catch (RealTimeException realTimeEx)
      {
        // RealTimeException may be thrown as a result of any UCMA operation.
        Console.WriteLine(realTimeEx.Message);
        throw;
      }
      finally
      {
        // Synchronize threads.
        _platformStartupCompleted.Set();
      }

    }

    private void EndEndpointEstablish(IAsyncResult ar)
    {
      LocalEndpoint currentEndpoint = ar.AsyncState as LocalEndpoint;
      try
      {
        currentEndpoint.EndEstablish(ar);
      }
      catch (AuthenticationException authEx)
      {
        // AuthenticationException is thrown when the credentials are not valid.
        Console.WriteLine(authEx.Message);
        throw;
      }
      catch (ConnectionFailureException connFailEx)
      {
        // ConnectionFailureException is thrown when the endpoint cannot connect to the server, or the credentials are not valid.
        Console.WriteLine(connFailEx.Message);
        throw;
      }
      catch (InvalidOperationException iOpEx)
      {
        // InvalidOperationException is thrown when the endpoint is not in a valid connection state to. To connect, the platform must be started and the endpoint must be in the Idle state.
        Console.WriteLine(iOpEx.Message);
        throw;
      }
      finally
      {
        // Synchronize threads.
        _endpointInitCompletedEvent.Set();
      }
    }

    internal void ShutdownPlatform()
    {
      if (_collabPlatform != null)
      {
        _collabPlatform.BeginShutdown(EndPlatformShutdown, _collabPlatform);
      }

      // if (_serverCollabPlatform != null)
      // {
      //   _serverCollabPlatform.BeginShutdown(EndPlatformShutdown, _serverCollabPlatform);
      //}

      // Synchronize threads.
      _platformShutdownCompletedEvent.WaitOne();
    }

    private void EndPlatformShutdown(IAsyncResult ar)
    {
      CollaborationPlatform collabPlatform = ar.AsyncState as CollaborationPlatform;

      try
      {
        // Shutdown actions do not throw.
        collabPlatform.EndShutdown(ar);
        Console.WriteLine("The platform is now shut down.");
      }
      finally
      {
        _platformShutdownCompletedEvent.Set();
      }
    }

    /// <summary>
    /// Read the local store for the certificate that is used to create the platform. This is necessary to establish a connection to the server.
    /// </summary>
    /// <param name="friendlyName">The friendly name of the certificate to use.</param>
    /// <returns>The certificate instance.</returns>
    public static X509Certificate2 GetLocalCertificate(string friendlyName)
    {
      X509Store store = new X509Store(StoreLocation.LocalMachine);

      store.Open(OpenFlags.ReadOnly);
      X509Certificate2Collection certificates = store.Certificates;
      store.Close();

      foreach (X509Certificate2 certificate in certificates)
      {
        if (certificate.FriendlyName.Equals(friendlyName, StringComparison.OrdinalIgnoreCase))
        {
          return certificate;
        }
      }
      return null;
    }

    public static void WriteLine(string line)
    {
      Console.WriteLine(line);
    }

    public static void WriteErrorLine(string line)
    {
      Console.ForegroundColor = ConsoleColor.Red;
      Console.WriteLine(line);
      Console.ResetColor();
    }

    public static void WriteException(Exception ex)
    {
      WriteErrorLine(ex.ToString());
    }

    /// <summary>
    /// Prompts the user to press a key, unblocking any waiting calls to the
    /// <code>WaitForSampleFinish</code> method
    /// </summary>
    public static void FinishSample()
    {
      Console.WriteLine("Please hit any key to end the sample.");
      Console.ReadKey();
      _sampleFinished.Set();
    }

    /// <summary>
    /// 
    /// </summary>
    public static void WaitForSampleFinish()
    {
      _sampleFinished.WaitOne();
    }
      
  }
}

Part 4

Using UCMA 3.0 BackToBackCall: Tips and Conclusion (Part 4 of 4)

Additional Resources

For more information, see the following resources:

About the Author

Mark Parker is a programming writer at Microsoft Corporation. His principal responsibility is the UCMA SDK documentation.