Creating a custom activity to make a supervised transfer

Important March 2007 Update:

Please do not use the code below in the latest OCS Beta. If you would like to create a custom activity using a supervised transfer there is now sample code installed with the SDK that provides a better example than this code.  

I suspect this will be one of my longer posts, but I have decided to throw it all together in one post vs dividing it up into several days so you are not left with a portion of an app, waiting for the next day to finish it.  This post took me awhile to put together, so I hope it is as useful as I expect it will be.

A number of questions have come up concerning creating custom activities for Speech Server workflow. In this article, I will guide you through the steps necessary to create a custom activity that performs a supervised transfer.  Note that these steps are most relevant for the Beta, and may change signficantly when we release.

1) The first thing you need to do is create a new Speech Workflow project. Use the default settings and let's call it CustomActivityHost.  If you want, drag over a statement and set the prompt to something like "Transferring you now.".

2) We are now ready to create our custom activity.  The project type we will add to the solution is a Workflow Activity Library.  Call it CustomActivities.  Future custom activities will also be added to this project.

3) Rename the default Activity1 activity to something more descriptive, like SupervisedTransferActivity.

4) We need to add a reference to the Speech Server assemblies in this new activity library project.  The assemblies we need are located at %SYSTEMDRIVE%\Microsoft Speech Server\PublicAssemblies and are called Microsoft.SpeechServer.Dialog.dll and Microsoft.SpeechServer.dll.

5) In SupervisedTransferActivity.cs, add the following lines at the top

using Microsoft.SpeechServer.Dialog;

using Microsoft.SpeechServer;

6) We now need to derive our SupervisedTransferActivity from the SpeechCompositeActivity class.  This class can be used as a base class for almost all of your custom activities.  It has one limitation though - the call must already be in a connected state.  If your custom activity cannot assume this (such as a specialized activity for making outbound calls) then the procedure is a bit more involved and will be covered in a later post.

7) Remove the constructor that was automatically generated for the activity.

8) Remove the code automatically added for you in InitializeComponent.

9) Override the ExecuteCore method.  This is the method that does the work within your activity, and is where we will call the Core API.

        /// <summary>

        /// Performs the main body of work for the activity

        /// </summary>

        /// <param name="executionContext"></param>

        protected override void ExecuteCore(ActivityExecutionContext executionContext)

        {

        }

10) We are going to allow any workflow that hosts our activity to interact with our supervised transfer activity.  To do this, we will add properties so the app can set the called party and calling party.  This is a bit more complex though than it seems.  The problem is that for each activity instance can only be run once in workflow.  Workflow clones each activity before running it.  To allow properties to be shared between clones we need to make use of the InstanceDependencyProperty class.  The first thing we need to do is add the following member variables to our activity.

private static InstanceDependencyProperty CalledNumberProperty =

InstanceDependencyProperty.Register("CalledNumber", typeof(string), typeof(SupervisedTransferActivity));

private static InstanceDependencyProperty CallingNumberProperty =

InstanceDependencyProperty.Register("CallingNumber", typeof(string), typeof(SupervisedTransferActivity));

private static InstanceDependencyProperty ErrorProperty =

InstanceDependencyProperty.Register("Error", typeof(Exception), typeof(SupervisedTransferActivity));

I added a separate Error property to allow the workflow to retrieve any error that occurs.  The assumption here is that if the call fails to connect or the transfer fails, the app should have the opportunity to handle this and is not an exceptional event.  Therefore any code that uses this should check the Error property to see if the transfer succeeded.

Now lets add code to set and retrieve our properties.

        /// <summary>

        /// The number that we will call

        /// </summary>

        public string CalledNumber

        {

            get { return (string)GetValue(CalledNumberProperty); }

            set { SetValue(CalledNumberProperty, value); }

        }

        /// <summary>

        /// The number that is calling

        /// </summary>

        public string CallingNumber

        {

            get { return (string)GetValue(CallingNumberProperty); }

            set { SetValue(CallingNumberProperty, value); }

        }

        /// <summary>

        /// The last error that occurred

        /// </summary>

        public Exception Error

        {

            get { return (Exception)GetValue(ErrorProperty); }

        }

 

Finally, lets add an event handler called TurnStarting that the app can override.  This will allow the app to retrieve numbers dynamically and set them in our activity before the call is transfered.  First, let's add our event handler.

public

EventHandler TurnStarting;

Now, let's add some code at the beginning of ExecuteCore

// Allow the workflow that uses this activity to do something when

// the activity starts.

if (null != TurnStarting)

{

TurnStarting(this, EventArgs.Empty);

}

This will call the TurnStarting event handler if it is defined. Next, we should make sure we have a number to call before executing. In this case I do pass the exception because failure to set the called number is a coding error.

// Make sure we have a number to call

string calledNumber = (string)GetValue(CalledNumberProperty);

if (true == string.IsNullOrEmpty(calledNumber))

{

base.OnSpeechActivityClosed(new Exception("The value of 'called' cannot be null or empty"));

return;

}

The last thing we need to do is set our error property to null.

// Set the error to null

SetValue(ErrorProperty, null);

11) We are now ready to start with making the supervised transfer. For this we will make use of the Core API because workflow elements currently do not exist that help us with this. There does exist a BlindTransferActivity but not one for supervised transfers (that is why we are building this one!). To perform a supervised transfer, we will follow these steps.

1) Put the original party on hold

2) Create a new telephony session

3) Open the new session with the called number

4) Transfer the call

As I mentioned in a previous post, most methods in the Core API are asynchronous, so for each step in this process I will do the following.

1) Set the event handler

2) Call the asynchronous method

3) (in event handler) Disconnect the event handler

4) Check if any errors occurred

The following code will put the call on hold.

// Put the current session on hold

Workflow.TelephonySession.HoldCompleted += new EventHandler<Microsoft.SpeechServer.AsyncCompletedEventArgs>(TelephonySession_HoldCompleted);

Workflow.TelephonySession.HoldAsync();

The following is the event handler for HoldCompleted.

void TelephonySession_HoldCompleted(object sender,

Microsoft.SpeechServer.AsyncCompletedEventArgs e)

{

Workflow.TelephonySession.HoldCompleted -= TelephonySession_HoldCompleted;

// Make sure the hold was successful

if (null != e.Error)

{

SetValue(ErrorProperty, e.Error);

base.OnSpeechActivityClosed(null);

return;

}

}

If we run across any error when the hold completes, we set the value of our error property and call base.OnSpeechActivityClosed to inform Workflow that our activity has finished. This method takes the error that occurred. Since we do not want to generate a fault here, we pass null, indicating success. We have now put the caller on hold. The next step is to create a new TelephonySession and open it. Add the following code to the HoldCompleted event handler.

// Retrieve the ANI and DNIS

string calledNumber = (string)GetValue(CalledNumberProperty);

string callingNumber = (string)GetValue(CallingNumberProperty);

// Create a new session

_newSession = Workflow.TelephonySession.CreateSession(TelephonySessionType.SupervisedTransfer);

_newSession.OpenCompleted += new

EventHandler<Microsoft.SpeechServer.AsyncCompletedEventArgs>(_newSession_OpenCompleted);

// Open the session

if (null == callingNumber)

{

_newSession.OpenAsync(calledNumber);

}

else

{

_newSession.OpenAsync(calledNumber, callingNumber);

}

First we retrieve the values of the called number and calling number properties (also called the DNIS and ANI respectively). To create a new ITelephonySession object, we call the CreateSession method of the existing TelephonySession object. We then call OpenAsync on the object using the appropriate overloaded method depending on whether we have an ANI or not. Make sure to add the following line in your member variables region.

private ITelephonySession _newSession;

To handle the OpenCompleted event, add the following code.

void _newSession_OpenCompleted(object sender, Microsoft.SpeechServer.AsyncCompletedEventArgs e)

{

_newSession.OpenCompleted -= _newSession_OpenCompleted;

// Make sure the session was opened

if (null != e.Error)

{

// The session failed to open, resume the call

SetValue(ErrorProperty, e.Error);

Workflow.TelephonySession.ResumeCompleted += new

EventHandler<Microsoft.SpeechServer.AsyncCompletedEventArgs>(

TelephonySession_ResumeCompleted);

Workflow.TelephonySession.ResumeAsync();

}

}

Here, our error handling needs to be a bit more complex. If we fail to open the new session, we have already put the user on hold. To allow the application to recover from this, we must resume the original call. A production quality component here may have some code to analyze why the call failed and set a value from an enumeration as to whether the call is busy or was not answered. Code to do this can be found in one of my previous posts on making outbound calls using the Core API (the logic is the same). You could also add answering machine detection here - see a previous post on a possible way to do that.

Add the following message to stop the activity when the call has been resumed.

void TelephonySession_ResumeCompleted(object sender,

Microsoft.SpeechServer.AsyncCompletedEventArgs e)

{

base.OnSpeechActivityClosed(null);

}

Now that we have handled the error conditions, we can assume the call was successful. The next step is to transfer the call. Add the following code to the OpenCompleted event handler.

// Transfer the call

Workflow.TelephonySession.TransferCompleted += new

EventHandler<Microsoft.SpeechServer.AsyncCompletedEventArgs>(

TelephonySession_TransferCompleted);

Workflow.TelephonySession.TransferAsync();

Be very careful when calling TransferAsync to call it on the telephony session you wish to transfer from - not on the one you wish to transfer to. The latter will not work. Add the following code to handle the TransferCompleted event.

void TelephonySession_TransferCompleted(object sender,

Microsoft.SpeechServer.AsyncCompletedEventArgs e)

{

Workflow.TelephonySession.TransferCompleted -= TelephonySession_TransferCompleted;

if (null != e.Error)

{

// The transfer failed, resume the call

SetValue(ErrorProperty, e.Error);

Workflow.TelephonySession.ResumeCompleted += new

EventHandler<Microsoft.SpeechServer.AsyncCompletedEventArgs>(

TelephonySession_ResumeCompleted);

Workflow.TelephonySession.ResumeAsync();

}

_newSession.Close();

_newSession = null;

if (null == e.Error)

{

base.OnSpeechActivityClosed(null);

}

}

Here again we must resume the call if an error occurred and pass the error back to the application. In all cases, we close the consultation session because the call has now tranferred and we need to clean up. Finally, if no error occurred we notify Workflow that our activity has finished. If an error does occur, we do this notification in the ResumeCompleted event handler.

12) Once all of the code has been entered, build the solution and you should see the new activity in the list of choices in the toolbox for the CustomActivityHost project. Drag a SupervisedTransferActivity to the host project's workflow. Make sure to set the called number to an arbitrary value.

You should now be able to build your project and run through the supervised transfer. I hope this has also helped you learn how to code custom activities for Microsoft Speech Server. In future posts, I hope to cover more advanced topics related to this - such as adding activities within our custom activity and creating activities where we do not assume we already have a connection.