Creating Automated Attendants in UCMA 3.0 and Lync 2010: The Lync Client Caller (Part 3 of 5)

Summary:   Learn how a Microsoft Unified Communications Managed API (UCMA) 3.0 application can process dual-tone multifrequency (DTMF) tones that are sent from a Microsoft Lync 2010 application. Part 3 discusses the Lync 2010 application.

Applies to:   Microsoft Unified Communications Managed API (UCMA) 3.0 Core SDK | Microsoft Lync 2010 SDK | Microsoft Speech Platform SDK

Published:   December 2011 | Provided by:   John Austin and Mark Parker, Microsoft | About the Authors

Contents

  • Introduction

  • NorthwindCashRegister Development Prerequisites

  • About the NorthwindCashRegister Application

  • Audio Call Application Logic

  • Part 4

  • Additional Resources

This is the third in a five-part series of articles about how to build client and middle-tier applications that interact by using DTMF tones in a Microsoft Lync 2010 API and using speech synthesis in a Microsoft Unified Communications Managed API (UCMA) 3.0 application.

Introduction

The NorthwindCashRegister application has a telephone dial pad component that lets a user dial external telephone numbers and extensions within Northwind Traders. If a store employee dials the extension of the Northwind Traders Microsoft Unified Communications Managed API (UCMA) 3.0 attendant middle-tier application, the NorthwindCashRegister application lets the employee respond to the audio menu prompts of the UCMA 3.0 attendant by sending DTMF tones.

This article discusses how you send DTMF tones in an audio conversation by using the Microsoft Lync 2010 API. Use the code in this article to create the dial pad feature of the Microsoft Lync 2010 conversation window in your own custom conversation window. To send DTMF tones, all that you need is an active conversation with a connected audio/video modality, and then you can send a DTMF tone by calling the BeginSendDtmf method. If you want to write an application with additional features, your code must handle the changing state of both the conversation and the audio/video modality in the life cycle of a conversation. The example code in this article shows you how to start a conversation, register for and respond to modality state change related events, and send DTMF tones.

Lync 2010 must be installed on a computer that runs the DTMF tone-sending application that you create by using the Lync 2010 API. All users must also be able to sign in to Lync 2010.

NorthwindCashRegister Development Prerequisites

To compile the sample that is used in this article, the following software is required.

  • Microsoft Visual Studio 2008 or Microsoft Visual Studio 2010 development system.

  • Microsoft Lync 2010 SDK.

  • Microsoft Lync 2010 API Runtime DLLs.

About the NorthwindCashRegister Application

The NorthwindCashRegister application dials a telephone number to start a Lync 2010 audio conversation and then lets a user send DTMF tones to respond to prompts. Figure 1 shows the components that are included in the NorthwindCashRegister application. There are two Windows forms, two user controls, and three utility classes.

Figure 1. The NorthwindCashRegister Solution Explorer view

DTMF Tone Sender application solution view

The components of the NorthwindCashRegister application appear in the following table.

Project Type name

Description

CashRegister

The main window of the cash register application. It hosts multiple user controls. To sign a user in to Lync 2010, the form shows the SignInControl. When an employee is signed in, the main window replaces the SignInControl with the PhonePad. Conversations are started from the cash register form when a user clicks the Connect button after dialing a number.

ConversationWindow

Displays conversation state, and connects or disconnects the audio modality in the conversation. Sends DTMF tones.

PhonePad

UserControl class that displays a telephone dial pad. Used by the main window to accept a telephone number to dial. Used by the conversation window to accept DTMF digits.

SignInControl

Collects user network credentials to be passed to Lync when an employee is signed in to Lync.

ConversationItem

Utility class that encapsulates a conversation and related conversation state.

ParticipantItem

Utility class that encapsulates a participant and related participant state.

LyncModelExceptionHelper

Takes a System.SystemException and returns true if the exception is a Lync 2010 API exception.

The following sections describe each item in the NorthwindCashRegister project.

PhonePadControl User Control

This control is a multi-use control in the NorthwindCashRegister application. On the CashRegister form, it is used to accept user input to form a dial string. As a part of the conversation window, it is used to get a character to be sent as DTMF. The logic in the telephone pad control raises a DialPadPressed event when a user clicks a button. The event argument provides a string that is the text of the button that is clicked.

The CashRegister form appends the button text to a dial string that is used as the argument to ContactManager.GetContactByUri. The Microsoft.Lync.Model.Contact instance that is returned is added to the conversation that is obtained with a call into the ConversationManager.AddConversation method.

The conversation window uses the DialPadPressed event to get a single character string whose value is the clicked button text. The string is passed as the first argument of the BeginSendDtmf method.

Figure 2. NorthwindCashRegister phone pad

DTMF tone dial pad

Tip

The phone pad plays a DTMF tone through the system speaker when a user clicks a button on the keypad. For information about how to play audio files together with the Lync 2010 API, see Playing Audio Files in Lync 2010 Client: Introduction (Part 1 of 2).

SignInControl User Control

This user control contains three text boxes that are used to obtain user network credentials, and include the following three credential elements.

  • SIP address

  • User name

  • Password

If the Lync 2010 UI is not suppressed on the computer and the user is not signed in to Lync 2010, then the CashRegister instance shows SignInControl. The user types in the required credentials, clicks the Sign In button, and then SignInControl signs the user in to Lync 2010.

The sign-in logic becomes more complex when the Lync 2010 UI is suppressed. If a user has not signed in to Lync 2010 since signing into Microsoft Windows, Lync 2010 must be initialized before the user can sign in to Lync 2010. Figure 3 shows the sequence of API calls and events for UI suppression sign in.

Figure 3. UI suppression sign-in sequence

UI suppression sign-in sequence

CashRegister Form

The CashRegister form is the main form of the NorthwindCashRegister application. A store employee can make Lync calls by using the embedded telephone pad control. If an employee is not signed in to Lync 2010 when the form is opened, the telephone pad control is replaced with the SignInControl. SignInControl collects the employee’s network credentials (sign-in address, user name, and password), and then signs in to Lync 2010.

The CashRegister constructor tries to obtain an instance of Microsoft.Lync.Model.LyncClient and then catches an exception if it cannot get an instance. In this case, Lync 2010 is not running. If Lync 2010 is running then the form logic signs a user in to Lync 2010, and registers for client and conversation-related events.

When the telephone Connect button is clicked, CashRegister completes the following steps:

Figure 4 shows the sequence of API method calls and events that start a Lync 2010 conversation.

Figure 4. Starting a Lync conversation sequence diagram

Starting a Lync conversation sequence diagram

If the called contact has forwarded audio calls to a mobile telephone or other PSTN (Public switched telephone network) telephone, that telephone starts to ring. Otherwise, the contact sees a Lync audio conversation invitation.

Figure 5. CashRegister form

Northwind Traders cash register form

ConversationWindow Form

The ConversationWindow form is opened while the audio/video modality is connecting and the remote telephone is ringing. The conversation roster shows both the local user denoted by “(self)” and the remote contact on the call. The keypad buttons and the Disconnect button are not enabled until the call is answered. At that time, the status bar on the bottom of the window shows that the conversation is active and the audio/video modality is connected.

The form constructor gets the current Microsoft.Lync.Model.Conversation.Conversation from the CashRegister form as a constructor argument. The window uses the conversation instance to register for events on the conversation, audio/video modality, and audio channel. The conversation roster is maintained by event handlers for the Conversation.ParticipantAdded and Conversation.ParticipantRemoved events. The conversation, modality, and audio channel status labels on the bottom of the form are maintained by the handlers for the Conversation.StateChanged, Modality.ModalityStateChanged, and Channel.StateChanged events.

The embedded telephone pad control raises an event when the user clicks a button on the pad. The event is handled by the parent conversation window and a DTMF tone representing the clicked button is sent on the audio channel.

Figure 6 shows the conversation form with a call to the telephone number (425) 555-0102. The person at that telephone number has answered the call, the conversation is active, and the audio/video modality is connected.

Figure 6. Conversation window form

Conversation window form

Figure 7 shows the sequence of API method calls and events that start when the conversation window is constructed while the call is connecting and complete with the sending of a DTMF tone.

Figure 7. The send DTMF sequence diagram

Send a DTMF tone sequence diagram

Audio Call Application Logic

To add the Lync calling feature, you must add using statements, a class field declaration, method calls, and event handlers. Each of these is discussed in the following sections.

Important

The code in the following examples make calls into utility classes that are not listed in this article. For a complete listing of the sample code that includes the utility classes, see Creating Automated Attendants in UCMA 3.0 and Lync 2010: Code Listing and Conclusion (Part 5 of 5).

Namespace Using Declarations

This example shows the using statements that are added to the cash register and conversation window classes.

using Microsoft.Lync.Model;
using Microsoft.Lync.Model.Conversation;
using Microsoft.Lync.Model.Conversation.AudioVideo;

Sample Application Logic

The examples in this section have been extracted from the download sample that accompanies this article. Each sample is taken from the methods that are listed in Creating Automated Attendants in UCMA 3.0 and Lync 2010: Code Listing and Conclusion (Part 5 of 5). Only the Lync 2010 API calls are shown.

CashRegister Class

The examples in this section are taken from the CashRegister form.

CashRegister Class Field Declarations

Declare an instance of Microsoft.Lync.Model.LyncClient to represent the process that calls into the Lync 2010 client that is running on the computer.

// Store the Lync client instance.
   private LyncClient client;

Get the LyncClient Instance

The following example initializes the Microsoft.Lync.Model.LyncClient instance that is declared in the previous example.

// Obtain the Lync client instance.
client = LyncClient.GetClient();

Register for Lync Client Events

In the main window load event, register for the Client.StateChanged and LyncClient.ClientDisconnected events.

// Register for state updates. Whenever the client changes its state, this
// event will be fired. For example, during Sign-In the state will likely move from:
// SignedOut -> SigningIn -> SignedIn
client.StateChanged += client_StateChanged;

// Register for the client exiting event.
// When/if the Lync process exits, it will fire this event.
client.ClientDisconnected += client_ClientDisconnected;

Initialize the Client

If the Lync client is configured to suppress its UI, then you must initialize the client before it can be used. For information about initializing the Lync client, see Walkthrough: Sign In to Lync with UI Suppressed.

In the main form load event, add the following code.

// If this client is in UISuppressionMode...
if (client.InSuppressedMode && client.State == ClientState.Uninitialized)
{
   client.BeginInitialize(ClientInitialized, null);
}

...
private void ClientInitialized(IAsyncResult result)
{
   // Register for conversation related events.
   // These events will occur when new conversations are created (incoming/outgoing) and removed.
   client.ConversationManager.ConversationAdded += ConversationManager_ConversationAdded;
   client.ConversationManager.ConversationRemoved += ConversationManager_ConversationRemoved;
}

Register for ConversationManager Events

The Client.ConversationManager property returns the Microsoft.Lync.Model.Conversation.ConversationManager instance. The conversation manager raises an event when a new conversation is added or removed. The NorthwindCashRegister application uses this event to open a conversation window. The conversation window represents a single conversation and lets the user see the status of the conversation, the conversation roster, a set of telephone pad buttons, and a conversation disconnect feature.

// Register for conversation-related events.
// These events will occur when new conversations are created (incoming/outgoing) and removed.
client.ConversationManager.ConversationAdded += ConversationManager_ConversationAdded;
client.ConversationManager.ConversationRemoved += ConversationManager_ConversationRemoved;

Handle the Connect Button Click Event

The following example gets a Microsoft.Lync.Model.Contact that represents the telephone number that is dialed, and then adds a new Microsoft.Lync.Model.Conversation.Conversation instance.

private void Connectbutton_Click(object sender, EventArgs e)
{
   contactToCall = client.ContactManager.GetContactByUri(dialString);
   // Create a new conversation.
   Conversation conversation = null;
   conversation = client.ConversationManager.AddConversation();
}

Handle the ConversationAdded Event

This event handler is where most of the action occurs. The Microsoft.Lync.Model.Contact instance that represents the dialed telephone number is added to the new conversation, the conversation audio/video modality is connected, and then a ConversationWindow is created and opened.

Important

The ConversationAdded event is raised on the API thread. Events that are related to the conversation, conversation modality, modality channel, or conversation participants may be invoked non-deterministically and after the ConversationAdded event. The conversation window must be loaded and these conversation-related events are registered before the Lync platform starts to invoke the events. If the conversation window registers for these events in time, then the events is caught as they are invoked. To ensure this occurs, the example calls the AutoResetEvent.WaitOne method to lock the API thread until the ConversationWindow Form.Load event is invoked. The API thread is signaled to continue by calling the AutoResetEvent.Set method in the conversation window’s Load event handler that is registered for in the CashRegister code.

void ConversationManager_ConversationAdded(object sender, ConversationManagerEventArgs e)
{
   // Add a participant to the conversation.
   // The window created for this conversation will handle ParticipantAdded events.
   if (contactToCall != null && e.Conversation != null)
   {
      e.Conversation.AddParticipant(contactToCall);
   }
  
  
   ConversationWindow window = new ConversationWindow(e.Conversation, client);
   // Register for window load events so that the lock of the Lync thread can be released.
   window.Load += window_Load;

   // Show the new window.
   window.Show(this);

   // Wait until the window is loaded to release the SDK thread.
   conversationWindowLock.WaitOne();
}

/// <summary>
/// Releases the lock from the Lync thread when the conversation window is done loading.
/// </summary>
void window_Load(object sender, EventArgs e)
{
   // Release the lock used to hold the Lync SDK thread.
   conversationWindowLock.Set();
}

ConversationWindow Class

The examples in this section are taken from the ConversationWindow class.

Constructing the Conversation Window

The following example constructs a conversation window.

public ConversationWindow(Microsoft.Lync.Model.Conversation.Conversation conversation)
{
   InitializeComponent();
   // Save the conversation reference.
   _conversation = conversation;

   // Save AVModality and AudioChannel, for the sake of readability.
   avModality = (AVModality) _conversation.Modalities[ModalityTypes.AudioVideo];
   audioChannel = avModality.AudioChannel;

   // Register for conversation state updates.
   _conversation.StateChanged += conversation_StateChanged;

   // Register for participant events.
   _conversation.ParticipantAdded += conversation_ParticipantAdded;
   _conversation.ParticipantRemoved += conversation_ParticipantRemoved;

   // Subscribe to the conversation action availability events (for the ability to add/remove participants).
   _conversation.ActionAvailabilityChanged += conversation_ActionAvailabilityChanged;

   // Subscribe to modality action availability events (all audio buttons except DTMF).
   avModality.ActionAvailabilityChanged += avModality_ActionAvailabilityChanged;

   // Subscribe to the modality state changes so that the status bar is updated with the new state.
   avModality.ModalityStateChanged += avModality_ModalityStateChanged;

   // Subscribe to audio channel action availability events (DTMF only).
   audioChannel.ActionAvailabilityChanged += audioChannel_ActionAvailabilityChanged;

   // Subscribe to audio channel state changes so that the status bar is updated with the new state.
   audioChannel.StateChanged += audioChannel_StateChanged;


   avModality = (AVModality)_conversation.Modalities[ModalityTypes.AudioVideo];
   AsyncCallback callback = new AsyncOperationHandler(avModality.EndConnect).Callback;
   avModality.BeginConnect(callback, null);

   // Create the phonePad control for DTMF dialing.
   phonePadControl = new PhonePadControl();
   phonePadControl.DialPadPressed += new DialPadPressed(dialpad_DialPadPressed);
   phonePadControl.Anchor = AnchorStyles.None;
   phonePadControl.Enabled = false;
   tableLayoutPanel1.Controls.Add(phonePadControl, 0, 1);
}

Handle the Conversation Participant Events

This example adds a call participant from the conversation roster.

void conversation_ParticipantAdded(object sender, ParticipantCollectionChangedEventArgs e)
{
   // Error case, the participant wasn't actually added.
   if (e.StatusCode < 0)
   {
      Console.Out.WriteLine("Participant was not added: code=" + e.StatusCode);
      return;
   }

   // ParticipantAdded event is raised on the platform thread. BeginInvoke posts the
   // execution of the invoked delegate into the UI thread. The state
   // of UI controls cannot be changed by any thread that does not "own" the control,
   // making it necessary to invoke this delegate on the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {
      // Create a new item and adds to the roster listbox.
      listBoxRosterContacts.Items.Add(new ParticipantItem(e.Participant));
   }));

}

This example removes a participant from the roster.

void conversation_ParticipantRemoved(object sender, ParticipantCollectionChangedEventArgs e)
{
   // Error case, the participant wasn't actually removed.
   if (e.StatusCode < 0)
   {
      Console.Out.WriteLine("Participant was not removed: code=" + e.StatusCode);
      return;
   }

   // ParticipantRemoved event is raised on the platform thread. BeginInvoke posts the
   // execution of the invoked delegate into the UI thread. The state
   // of UI controls cannot be changed by any thread that does not "own" the control,
   // making it necessary to invoke this delegate on the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {

      // Find the position of the participant to be removed in the roster listbox.
      int removePosition = -1;
      for (int i = 0; i < listBoxRosterContacts.Items.Count; i++)
      {
         ParticipantItem item = listBoxRosterContacts.Items[i] as ParticipantItem;
         if (item != null && item.Participant.Equals(e.Participant))
         {
            removePosition = i;
            break;
         }
      }

      // Remove the participant from the roster listbox.
      if (removePosition > 0)
      {
         listBoxRosterContacts.Items.RemoveAt(removePosition);
      }
   }));
}

Handle the Conversation StateChanged Event

This example updates the text of the conversation status label when the state of the conversation changes.

void conversation_StateChanged(object sender, ConversationStateChangedEventArgs e)
{
   // StateChanged event is raised on the platform thread. BeginInvoke posts the
   // execution of the invoked delegate into the UI thread. The state
   // of UI controls cannot be changed by any thread that does not "own" the control,
   // making it necessary to invoke this delegate on the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {
      // Show the current conversation state.
      toolStripStatusLabelConvesation.Text = e.NewState.ToString();
   }));
}

Handle the Modality StateChanged Event

This example updates the text of the modality state label and enables the telephone pad if the modality is connected.

void avModality_ModalityStateChanged(object sender, ModalityStateChangedEventArgs e)
{
   // ModalityStateChanged event is raised on the platform thread. BeginInvoke posts the
   // execution of the invoked delegate into the UI thread. The state
   // of UI controls cannot be changed by any thread that does not "own" the control,
   // making it necessary to invoke this delegate on the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {
     // Update the status bar with the video channel state.
     toolStripStatusLabelModality.Text = e.NewState.ToString();
   }));
   if (e.NewState == ModalityState.Connected)
   {
      // Invoke UI state update delegate on UI thread.
      this.BeginInvoke(new MethodInvoker(delegate()
      {
         phonePadControl.Enabled = audioChannel.CanInvoke(ChannelAction.SendDtmf);
      }));
   }
}

Handle the Audio Channel StateChanged Event

This example updates the text of the audio channel state label.

/// <summary>
/// Updates the status bar when the new Audio Channel state.
/// </summary>
void audioChannel_StateChanged(object sender, ChannelStateChangedEventArgs e)
{
   // StateChanged event is raised on the platform thread. BeginInvoke posts the
   // execution of the invoked delegate into the UI thread. The state
   // of UI controls cannot be changed by any thread that does not "own" the control,
   // making it necessary to invoke this delegate on the UI thread.
   this.BeginInvoke(new MethodInvoker(delegate()
   {
      // Update the status bar with the audio channel state.
      toolStripStatusLabelAudioChannel.Text = e.NewState.ToString();       
   }));
}

Handle the DialPad Button Pressed Event

The following example is a conversation window method that handles the dial pad event that is raised when a user clicks a button on the dial pad. The example verifies that the send DTMF action is available, and then sends a tone represented by the method parameter.

/// <summary>
/// Called when a button is pressed in the dial pad dialog.
/// 
/// Sends the DTMF tone using AudioChannel.BeginSendDtmf().
/// </summary>
void dialpad_DialPadPressed(string tone)
{
   if (audioChannel.CanInvoke(ChannelAction.SendDtmf))
   {
      // Sends a set of characters (in this case one) as a dial tone.
      AsyncCallback callback = new AsyncOperationHandler(audioChannel.EndSendDtmf).Callback;
      audioChannel.BeginSendDtmf(tone, callback, null);
   }
}

Part 4

Creating Automated Attendants in UCMA 3.0 and Lync 2010: The UCMA Attendant (Part 4 of 5)

Additional Resources

For more information, see the following resources:

About the Authors

John Austin, Microsoft, is a programmer/writer in the Lync client SDK documentation team. He has been writing Microsoft technical documentation for four years. Prior to working for Microsoft, John spent two decades as a software developer. Mark Parker is a programming writer at Microsoft whose current responsibility is the UCMA SDK documentation. Mark previously worked on the Microsoft Speech Server 2007 documentation.