Freigeben über


Understanding and Using CTI in UIICCA

CTI in UII is one of the more interesting and powerful additions to the UII system. 
The downside of that is that we don't have a lot of documentation on actually HOW to use it :)  yes.. I know DETAILS….

Yves Pitch, Program manager for UII, has posted a good walk though of creating a new CTI Adapter and connection on his blog,  you can find it here.
While the post is on CCF 2009 SP1 QFE.. the CTI Framework in UII is functionally identical to CCF 2009 Sp1 QFE, though several names have been changed.

What I'm going to cover here is a high-level overview of CTI in UII, How CTI events interact with the system, using the CTI demo that is shipped with the 32bit versions of UII’s install ( We don't support 64bit with our TSP demo provide at this time ( UII v1) ), and how to adapted and modify the CCA Project source code and UI.  I will also update the CCA Source to make things a bit more simple for our work.

Lets cover some High-level Concepts.

CTI in Uii ,for Voice,  is all about Call Control, not the media .. that means that CTI in UII is not a “Softphone”, rather is has the Keypad, Agent control pad and access to all the metadata that would be associated with a call.  Metadata would be things like:

  • Ani – the number that called me
  • Dnis – the number that was called
  • Agent configuration information
  • Possible Wrap codes
  • CTI or IVR Data – data that is captured up stream and attached to the call, for example.. a customer or account id

For Chat Applications, its all about Call Control and CAN support chat media if the upstream CTI system supports it. As well as similar metadata to what would be captured for Voice.

We have also used the UII CTI framework to control Video Conferences and Email exchanges,  both of which are variants of the 2 above approaches. 

CTI in Uii is dependant on the Desktop Shell SDK, meaning,  to use the CTI system in UII, you must implement your agent desktop using the Shell Framework SDK.  Also CTI in Uii leverages the same search system that a user would use to do a customer lookup from CTI data.

CTI in Uii has 3 major components,

  • CTI Connection Layer
    • Handles Communication with the Up-steam CTI Interface
  • CTI Manager Layer
    • Handles all the calls a desktop is currently connected too, setting up a new call, and Agent state
  • CTI Visualization Layer
    • Visualize the “State” of the call and the “State” of the agent

Diagram wise , using the CCA desktop… it looks like this:

image

Now to the details :)

Following the instructions that are part of the CCA install, you will set the the Sample CTI components that are shipped with UII and CCA.  To be clear,  UII has the Sample TAPI and TSAPI components in its quick start directory. The CCA project has the TAPI Connector, Manager and visualizers that are used in CCA.  You can find the install instructions in the CCA Deployment Doc - Importing the CTI Hosted Applications

Before reading on,  if you have not done so.. I would encourage you to read though the CTI Documentation in UII’s Development guide, and API guide.

Call Start up Event Flow

The CTI system is inherently multi-threaded and event driven, this is by design to support the a number of different scenarios and the low latency requirements of the agent desktop and CTI solutions, however it makes debugging and following what is doing a bit challenging.  With that said I am going to walk you though the flow of events that a typical CTI call would flow though during startup..

When properly configured,  the CTI system is Listening or Polling for events from the CTI server, in the case of our TAPI demo, its listening to the TAPI connection our Sample TSP setup.  The listening process is usually setup to operate “out of band” of the main Desktop UI thread.  In the case of the TAPI demo code.. the Listener is in Microsoft.Crm.Accelerator.Cca.Cti.Samples.CtiRoot project, CtiRoot.cs file around Ln 144..  and is called Cti_CallChanged.  This method is the heart of the CTI System to UII Event Determination process.  this method effectively translates what the Upstream CTI system said, to something UII can understand.

Most of the time, when a call is sent to the agent, your CTI system is first going to raise a “NewCallEvent” or in the case of the TAPI simulator "Incoming_Call”.  This event indicates that a the agent has been selected to receive a new call, and includes the CTI Call ID, and Initial data about the call.. this happens before the Agent’s phone starts to ring,  You can see this event in the CCA Source code, Microsoft.Crm.Accelerator.Cca.Cti.Samples.CtiRoot project, CtiRoot.cs file around Ln 178..

 case CallClassProvider.CallState.Incoming_Call:
if (CallNewCallEvent != null)
{
   CtiCoreEventArgs args = new CtiCoreEventArgs("NewCALL", e,
                e.Call.CallID.ToString(CultureInfo.CurrentUICulture));
   CallNewCallEvent(this, args);
}

This is part of the CTI_CallChanged Method.

As you can see the Event raised here is the “CallNewCallEvent”,  this is a UII event and is caught by the CtiCallStateManager, the CtiCallStateManager is a Core UII class that is the basis for a customized or extended version of the CtiCallStateManager for your solution.  You can see this class in the CCA Source code, Microsoft.Crm.Accelerator.Cca.Cti.Samples.StateManagers, CallStateManager.cs.  You will notice that there is not an override for CallNewCallEvent in this source code.  the reason is that the base class, CitCallStateManager, is wiring that event and listening to it.  UII captures this event and sets up a CallInfo Record for the new call, and registers the call in the CallersList.

I would encourage you to read though the API entry for the CtiCallStateManager in the UII API documentation file as there a number of built-in Methods and features to use.

When the CtiCallStateManager is done with the CallNewCallEvent, it calls an overridden method in CtiCallStateManager called OnNewCallEvent passing in a NewCallEventData data structure.  The NewCallEventData Structure has 2 objects in it.  The original event from the CTI interface and the newly created CallInfoData object.  This is a Required override for your CtiCallStateManger as its your responsibly to decompose the CTI Event data and load it into the CallInfoData object… You can see the CCA version of it here; Microsoft.Crm.Accelerator.Cca.Cti.Samples.StateManagers, CallStateManager.cs. around Ln 86

Around Ln 101,  you will see a call to RaiseNewCallEvent(args)..  The code looks like this:

 NewCallEventArgs args = new NewCallEventArgs(
        callEventData.CallInfo.GetCtiCallRefId, callEventData.CallInfo);
RaiseNewCallEvent(args);

RaiseNewCallEvent is raising an event to the Desktop Manager code, which will handle the actual interaction with the desktop.

The handler for this event in the Desktop Manager is tapCallStateMgr_call_CallMgrStateNewCall method. its wired to the Instance of the CallStateManager that the desktop creates in the method SetRootCtiInterface method call.   It is the responsibly of the desktop manager to create the search request that will be handed off to search system, and handle any conflicting situations that could occur,  for example, if you have the ability to handle multiple lines or chats,  how do you deal with a new request if another Call is currently active.  Additionally the Desktop Manager would handle session change events and close events as they relate to CTI.  You can think of the Desktop Manager the desktop level business logic component for dealing with CTI.

The code for the tapCallStateMgr_call_CallMgrStateNewCall Method can be found in Microsoft.Crm.Accelerator.Cca.Cti.Samples.DesktopManager, DesktopManager.cs, around Ln 77 and looks like this:

 private void tapCallStateMgr_call_CallMgrStateNewCall(
    object sender, 
    NewCallEventArgs e)
{
    CtiLookupRequest lookupReq = new CtiLookupRequest(
      e.CallInfo.GetCtiCallRefId,
      this.ApplicationName,
      e.CallInfo.CallType,
      e.CallInfo.Ani,
      e.CallInfo.Dnis);
    string sData = GeneralFunctions.Serialize<CtiLookupRequest>(lookupReq);

    SendCommandParams cmd = new SendCommandParams(
               Constants.CUSTOMERSEARCH_APP_NAME, 
               CtiLookupRequest.CTILOOKUPACTIONNAME, sData);
    Thread t = new Thread(new ParameterizedThreadStart(SendAction));
    t.Start(cmd);
}

This method is creating a CTI Lookup Request from the information in the CallInfoData Object, Serializing that, then sending it off to the Search Control for processing,  the search control is the component that actually deals with querying the data store, in the case of CCA that's CRM, for the customer information.

The typical process from here would either immediately answer the call, for example if “enable autoanswer” is set to true, or wait for the lookup and session creation to process then allow the agent to press the answer button.

** CCA’s v1 sample code is a bit odd here as it was built for a specific situation.. I will cover how to make it more generic in the next section…

The next event that occurs would typically be a “Ringing Event” from the CTI connector.  This would typically be handled by raising an event from the CTI Connection layer to the Call Manager using the CallStateChangeEvent. however the Sample TAPI TSP we provide with the demo app doesn't support a proper Ringing event, so we have done a bit of slight of hand to make it work in our sample.

From then on out a call would go though a series of state changes, each one raising a CallStateChangeEvent from the CTI Connection layer.  Those events will be caught by the CtiCallStateManager and preprocessed,  then handed to the extended version of the OnCallStateChanged method in the CtiCallStateManager derived class.  You can see what that looks like here:  Microsoft.Crm.Accelerator.Cca.Cti.Samples.StateManagers, CallStateManager.cs. around Ln 48.

The function of the OnCallStateChanged method is to allow you to decode what the CTI Connection Layer said into something that UII will understand and update that information in the CallInfoData object,  Then once you are done with it, You call “RaiseCallStateChangeEvent” which hands the event onto the Desktop Manager or any Connected Visualization Layer. 

The full list of possible events that the CTI Connection Layer can raise is listed here:

 public override event EventHandler<CtiCoreEventArgs> CallNewCallEvent;
public override event EventHandler<CtiCoreEventArgs> CallStateChangeEvent;
public override event EventHandler<CtiCoreEventArgs> CallNewCallInfoEvent;
public override event EventHandler<CtiCoreEventArgs> CallItemEvent;
public override event EventHandler<CtiCoreEventArgs> CallMediaConnectedEvent;
public override event EventHandler<CtiCoreEventArgs> CallDestructedEvent;

You can find more info about each of them in the UII CTI documentation in the UII developer guide.  

Altering the CCA (Initial release) CTI Samples to make them more generic.

The CCA sample CTI code was developed with a specific scenario in mind and as such, it does not well tolerate different situations,  for example, if you want to pop up a search box if a exact match is not found on a phone number, or want to enable auto answer, or even have a different name for the Search control in CCA.

What we are going to do here is alter the CCA Code a bit to make it a bit more generic.

To start with We need to make a change to the Desktop Manager,  Open up Microsoft.Crm.Accelerator.Cca.Cti.Samples.DesktopManager, DesktopManager.cs in the CCA Project

First we need to alter the Constructor a bit.  Replace the UII Constructor with this one.

 public DesktopManager(Guid appId, string appName, string appInit)
    : base(appId, appName, appInit)
{
    InitializeComponent();
    AddAction(1200, "REJECTCALL", string.Empty); 
}

This will register an Action Name for the Desktop Manager to allow us to handle a call Rejected call situation.

Next we need to add in the DoAction Method,  After the Constructor,  Add in this code :

 protected override void DoAction(RequestActionEventArgs args)
{
    if (args.Action.Equals("REJECTCALL"))
    {
        DoRejectCall(args.Data);
        return;
    }
    base.DoAction(args);
}

This will capture the Action REJECTCALL and route the data to the DoRejectCall Method.

Next the implementation for DoRejectCall

 private void DoRejectCall(string refCallID)
{
    if (CallStateManager != null)
    {
        try
        {
            CallStateManager.RejectCall(new Guid(refCallID), null);
        }
        catch
        { }
    }
}

This will try to convert the refCallId ( from the data part of the action request ) into a Guid and Call RejectCall off the CtiCallStateManager.

Next, alter the tapCallStateMgr_call_CallMgrStateNewCall  to match this:

 private void tapCallStateMgr_call_CallMgrStateNewCall(
          object sender, 
          NewCallEventArgs e)
{
    CtiLookupRequest lookupReq = new CtiLookupRequest(e.CallInfo.GetCtiCallRefId,
      this.ApplicationName,
      e.CallInfo.CallType,
      e.CallInfo.Ani,
      e.CallInfo.Dnis);
    string sData = GeneralFunctions.Serialize<CtiLookupRequest>(lookupReq);
    SendCommandParams cmd = new SendCommandParams ("*",  
              CtiLookupRequest.CTILOOKUPACTIONNAME, sData);
    Thread t = new Thread(new ParameterizedThreadStart(SendAction));
    t.Start(cmd);
}

The Big change.. , or minor change… as it is,  is in the Call to SendCommandParams, we are changing it from sending to a specifically named control in UII, to a Broadcast command. meaning, any control in UII that has registered the action “CtiLookupRequest.CTILOOKUPACTIONNAME” will get it and be able to handle it. 

Next we need to modify the WpfCustomerSearch Control, you can find it here : Microsoft.Crm.Accelerator.Cca.Samples.Wpf.Controls, WpfCustomerSearch.xaml.cs.  Open that file

You will need to add a new Using statement to the Usings block, Add this:

 using Microsoft.Uii.Desktop.Cti.Core;

Next in the Init() Method, around Ln 76, Add the following Line. :

 AddAction(1200, CtiLookupRequest.CTILOOKUPACTIONNAME, string.Empty); 

This will register an Action Name for the Search control, so it will grab the broadcast action from the Desktop Manager.

Next, around Line 104, between the Properties Region and the IContextManager Members Region,  place this code:

 protected override void DoAction(RequestActionEventArgs args)
{
    if (args.Action.Equals(CtiLookupRequest.CTILOOKUPACTIONNAME))
    {
        DoSearchFromCtiRequest(args.Data);
        return; 
    }

    base.DoAction(args);
}

What this is doing is catching the Action, Raised from the Desktop manager and Routing that Action to the DoSearchFromCtiRequest Method of the ICustomerSearch interface, implemented in the WpfCustomerSearch.xaml.cs file.

Now we need to alter the DoSearchFromCtiRequest method, Locate the Method and Replace it with the code here:

 public void DoSearchFromCtiRequest(string data)
{
    ICustomerService CustomerLookup =
        AifServiceContainer.Instance.GetService<ICustomerService>(); ;
    CustomerRecord[] results = null;
    CustomerEntity UiiSessionCustomer = null;
    if (CustomerLookup != null)
    {
        if (!string.IsNullOrEmpty(data))
        {
            CtiLookupRequest request = null;
            try
            {
                // Decode the CTI request. 
                request = GeneralFunctions.Deserialize<CtiLookupRequest>(data);
            }
            catch (Exception)
            { }

            if (request != null)
            {
                string ani = string.Empty,
                        callType = string.Empty,
                        uiiCallID = string.Empty,
                        desktopManager = string.Empty,
                        dnis = string.Empty;

                Guid callRefId = Guid.Empty;
                if (request.Items != null)
                {
                    request.GetRequiredLookupData(
                        out callRefId,
                        out desktopManager,
                        out callType,
                        out ani,
                        out dnis);

                    if (callRefId != Guid.Empty)
                        uiiCallID = callRefId.ToString();
                    // If you need to get additional properties
                    foreach (LookupRequestItem itm in request.Items)
                    {
                        //if (itm.Key.Equals("CustomerID",
                        //        StringComparison.CurrentCultureIgnoreCase))
                        //{
                        //    customerID = itm.Value;
                        //}
                    }
                }

                if (!string.IsNullOrEmpty(ani))
                {
                    // try to get the customer. 
                    results = CustomerLookup.GetCustomer(string.Empty,
                        string.Empty,
                        ani,
                        string.Empty,
                        string.Empty,
                        string.Empty, 10);
                }

                if (results != null)
                    if (results.Length == 1)
                    {
                        // found it... 
                        CustomerRecord cust = results[0];
                        UiiSessionCustomer = new CustomerEntity(cust.CustomerId,
                            cust,
                            GeneralFunctions.Serialize<CustomerRecord>(cust),
                            false,
                            this.ApplicationName);
                        if (CustomerSearchResult != null)
                        {
                            SearchResultEventArgs args =
                                   new SearchResultEventArgs(UiiSessionCustomer,
                                       false,
                                       callRefId);
                            CustomerSearchResult(this, args);
                            return; // Exit and done... 
                        }
                    }

                // Failed to find it, pop the Search window. 
                CustomerRecord cRec = new CustomerRecord();
                if (!string.IsNullOrEmpty(ani))
                    cRec.PhoneHome = ani;

                LookupDlg dlg = new LookupDlg(CustomerLookup, cRec);
                var dlgResult = dlg.ShowDialog();
                if (dlgResult.HasValue && dlgResult.Value)
                {
                    UiiSessionCustomer = null;
                    if (dlg.Result != null)
                    {
                        UiiSessionCustomer =
                            new CustomerEntity(dlg.Result.CustomerId,
                                dlg.Result,
                                GeneralFunctions.Serialize<CustomerRecord>(dlg.Result),
                                false,
                                this.ApplicationName);
                    }
                    else
                    {
                        dlg.Result = CreateCustomer();
                        UiiSessionCustomer =
                            new CustomerEntity(dlg.Result.CustomerId,
                                dlg.Result,
                                GeneralFunctions.Serialize<CustomerRecord>(dlg.Result),
                                false,
                                this.ApplicationName);
                    }
                    dlg.Close();

                    if (CustomerSearchResult != null)
                        CustomerSearchResult(this,
                            new SearchResultEventArgs(UiiSessionCustomer, false));
                }
                else
                    dlg.Close();
            }
        }
    }
}

In this change we are altering the way that CCA will treat a request from the Desktop manager. 
Uii provides a object, mentioned above, called the CtiLookupRequest.  this allows us to transport any CTI centric data to our search control,  Decode it, and then act on it.  Additional data can be stored in the CtiLookupRequest as necessary via the Items List. 

In this case we are accepting the request, pulling the ANI out of it, ANI is the number that called us, and using that to do a customer lookup.  If we don't get a result, or we get more then one Result then we need to pop the Lookup Dialog, otherwise we go straight to starting a session. 

Now We need to handle a call REJECT situation,  that means that the user didn't find them on a search and need to “reject” the call, this will link up with what we added to the Desktop Manager a bit earlier

To Support Reject with our TAPI Simulator, we also need to make a minor change to the CTI Connection Layer.  Open up the Microsoft.Crm.Accelerator.Cca.Cti.Samples.CtiRoot, CtiRoot.cs file and find and replace the method RejectCall with :

 public override bool RejectCall(CtiCommandRequest commandData)
{
    AnswerCall(commandData); 
    HangUpCall(commandData); 
    return true;
}

This will force the TAPI Sim to drop the call.

Now we need to update the Visualization Layer so its displaying states correctly.  Open up the Microsoft.Crm.Accelerator.Cca.Samples.Wpf.Controls, CtiCallState.xaml.cs file.

First we need to find and replace the Answer_Click method,  the code to replace it with is here:

 private void Answer_Click(object sender, RoutedEventArgs e)
{
    // Init Local Var. 
    Guid CallIDToActOn = RingingCallIDFromEvent;
    // Get a copy of the local IAD Session Object. 
    AgentDesktopSession IadSession = (AgentDesktopSession)LocalSessionManager.ActiveSession;
    // Check to see if the Session has a current call. 
    if (IadSession.CtiCallRefId != Guid.Empty)
    {
        CallIDToActOn = IadSession.CtiCallRefId;
    }
    if (CallIDToActOn != Guid.Empty)
    {
        CallInfoData calldata = ICallStateMgr.GetCallInfoData(CallIDToActOn);
        if (calldata != null) // it can happen..... 
        {
            // verify we are still Rining...
            if (calldata.CurrentCallState.Equals(CtiCallStates.RINGING.ToString()))
            {
                ICallStateMgr.AnswerCall(CallIDToActOn);
                ddlAgentState.SelectedIndex = 1;
            }
        }
    }
}

Next, we need to update state flags in a few places..

Find the Method ResetCallStateButtons, and add the line :

 Answer.IsEnabled = false;

just before the line LastCallIDFromEvent = Guid.Empty.

Find the Method ProcessOffHook, and add the line :

 Answer.IsEnabled = false;

just before the line Hangup.IsEnabled = true;

That will keep all the call state buttons working the way we want them too :)

A special note Regarding using the EnableAutoAnswer Switch on the CallStateManager
To use the Enable Auto Answer Switch with the TAPI demo, you need to make 2 additional changes to the code.

The best place to turn on Enable Auto Answer is in the Desktop Manager, in the SetRootCtiInterface Method. You also need to alter the CTI_CallChanged method in the CtiRoot.cs file.  The change you need to make deals with how the New call event is handled.

If you have Enable Auto Answer turned on.. update the case for Incomming_Call to read:

 case CallClassProvider.CallState.Incoming_Call:
// Dealing with Sillyness that is the TAPI Simulator... the Sim does not provide a "ringing" event.
if (CallNewCallEvent != null)
{
    CtiCoreEventArgs args = new CtiCoreEventArgs("NewCALL", e, e.Call.CallID.ToString(CultureInfo.CurrentUICulture));
    CallNewCallEvent(this, args);
}
//// Another workaround for the TAPI demo Code....
//e.State = CallClassProvider.CallState.Ringing;
//if (CallStateChangeEvent != null)
//{
//    CtiCoreEventArgs args = new CtiCoreEventArgs(e.State.ToString(), e, e.Call.CallID.ToString(CultureInfo.CurrentUICulture));
//    CallStateChangeEvent(this, args);
//}
break;

This will avoid creating a ringing event that will put the visual states out of whack ..

If you do NOT have Enable Auto Answer turned on,  Leave the code as is.

In Conclusion…

  • We walked though overall organization of the UII CTI system and its components inside UII,
  • We walked though the events and processes that occur when a New Call Comes into the UII CTI system,
  • And we went though altering the initial release CCA code to make it more flexible to customization, and via that highlighted how some of the parts interact.

Comments

  • Anonymous
    January 18, 2011
    The comment has been removed

  • Anonymous
    January 18, 2011
    What you need to do is add an action to the CTI DesktopManager Hosted Control, Call it “Reject”, and add a hander in the doAction override.   When the action “Reject” is received, call the method RejectCall on the call state manager for the active call.   Then in the CTI Root Hosted Control, handle the Reject call however the CTI system you’re working with requires you to do it.

  • Anonymous
    January 12, 2012
    Hi Matt, We are trying to modify the CCA code to integrate it with Skype. So, CCA would typically be using the Skype calling as the CTI. There are three basic questions -

  1. Would we require to rewrite the entire CTI related code of CCA for integrating it with the Skype or we can use most of what comes with the CCA download?
  2. Can the softphone emulator be replaced altogether with Skype without losing (rather extending) all the CTI related functionality of CCA? 3)We have been able to consume the SKYPE4COMLib for .NET API inside CCA and any call coming to the Skype is opening up the calling customer's record but all this is just at the cost of OOB CTI functionality present in the CCA? How to do it in the best possible way. Please instruct.
  • Anonymous
    May 08, 2012
    Hi, Matt How can i implement an OutBound Call  on CCA