Developing screen pop applications

In my last post I introduced SuperToast and promised to discuss some of the internal workings. I’ve decided to broaden that out a bit, and discuss screen pops in general, and how they can be implemented using the Lync 2010 SDK.

For those not in the know, a screen pop is a notification window that appears in response to a call or message. The screen pop will often show details about the caller that are not available in the Lync client – this data is often pulled from a separate source, such as a CRM system.

To implement a screen pop solution, you need to be able to determine a) when a call or message is made or received, and b) who the remote participants are. That’s what this post is going to cover.

A typical screen pop solution will likely implement one or more of the following features:

  • An action in response to an incoming call. For example, in a call centre, an incoming call may trigger a screen pop that opens the callers account details in the customer database
  • An action in response to an outgoing call. For example, in a sales department, an outgoing call may trigger a new “Phone Call” activity in the CRM system
  • An action in response to an incoming instant message. For example, SuperToast

Although these sound pretty similar, the solution to each is different enough to make it worth discussing each separately, so that’s what I’ll do.

To start with, you’ll need Visual Studio 2010 and the Lync 2010 SDK  installed. You’ll then need to to create a desktop application in Visual Studio. This could be WPF or WinForms – my preference is for WPF, so that’s what I’m going for, using the C# Lync WPF Application template in Visual Studio.

You can download the complete, working sample application here.

Responding to Incoming Calls

I’ve created a class called IncomingCallNotifier to encapsulate this functionality. The class is shown in full here:

1 using System; 2 using Microsoft.Lync.Model; 3 using Microsoft.Lync.Model.Conversation; 4 5 namespace ScreenPops 6 { 7 class IncomingCallNotifier 8 { 9 // The event raised to signal a new call 10 internal event EventHandler<NewIncomingCallEventArgs> NewCall = delegate { }; 11 void OnNewCall(string remoteParticipant, bool hasSharingOnly, bool hasInstantMessaging, bool hasAudioVideo, bool isConference) 12 { 13 NewCall(this, new NewIncomingCallEventArgs(remoteParticipant, hasSharingOnly, hasInstantMessaging, hasAudioVideo, isConference)); 14 } 15 16 private LyncClient _lyncClient = null; 17 18 internal IncomingCallNotifier() 19 { 20 // Get a reference to the running Lync client, register for the ConversationAdded event. 21 // Note: This assumes that the Lync client is running 22 _lyncClient = LyncClient.GetClient(); 23 _lyncClient.ConversationManager.ConversationAdded += ConversationManager_ConversationAdded; 24 } 25 26 void ConversationManager_ConversationAdded(object sender, Microsoft.Lync.Model.Conversation.ConversationManagerEventArgs e) 27 { 28 var conversation = e.Conversation; 29 30 // Test conversation state. If inactive, then the new conversation window was opened by the user, not a remote participant 31 if (conversation.State == ConversationState.Inactive) 32 return; 33 34 // Get the URI of the "Inviter" contact 35 var remoteParticipant = ((Contact)conversation.Properties[ConversationProperty.Inviter]).Uri; 36 37 // Determine which modalities are available in the conversation 38 bool hasSharingOnly = true; 39 40 bool hasInstantMessaging = false; 41 if (ModalityIsNotified(conversation, ModalityTypes.InstantMessage)) 42 { 43 hasInstantMessaging = true; 44 hasSharingOnly = false; 45 } 46 47 bool hasAudioVideo = false; 48 if (ModalityIsNotified(conversation, ModalityTypes.AudioVideo)) 49 { 50 hasAudioVideo = true; 51 hasSharingOnly = false; 52 } 53 54 // Get whether this is a conference 55 bool isConference = conversation.Properties[ConversationProperty.ConferencingUri] != null; 56 57 // Raise the NewCall event 58 OnNewCall(remoteParticipant, hasSharingOnly, hasInstantMessaging, hasAudioVideo, isConference); 59 } 60 61 private bool ModalityIsNotified(Conversation conversation, ModalityTypes modalityType) 62 { 63 return conversation.Modalities.ContainsKey(modalityType) && 64 conversation.Modalities[modalityType].State == ModalityState.Notified; 65 } 66 } 67 }

 

This class uses a private field, _lyncClient, to hold a reference to the running instance of Lync. This is instantiated in the class constructor by calling the static method LyncClient.GetClient(), and a handler is created for the ConversationAdded event:

1 internal IncomingCallNotifier() 2 { 3 // Get a reference to the running Lync client, register for the ConversationAdded event. 4 // Note: This assumes that the Lync client is running 5 _lyncClient = LyncClient.GetClient(); 6 _lyncClient.ConversationManager.ConversationAdded += ConversationManager_ConversationAdded; 7 }

The ConversationAdded event tells us when a new conversation is created in the Lync client. This could be an outgoing conversation started by the logged-on user (e.g. by double-clicking a contact in the Lync client), or an incoming conversation started by a contact. As we’re only interested in incoming calls, we ignore outgoing calls by checking the conversation state.

When an incoming call is detected and the ConversationAdded event fires, the conversation is already active, as the conversation was initiated by the remote user. In this case, the conversation state will be “Active”. When an outgoing conversation is started in the Lync client, the ConversationAdded event will be fired as soon as the conversation windows opens, which is before the conversation has initiated. In this case, the conversation state will be “Inactive”.

1 var conversation = e.Conversation; 2 3 // Test conversation state. If inactive, then the new conversation window was opened by the user, not a remote participant 4 if (conversation.State == ConversationState.Inactive) 5 return;

To determine who initiated the call, the route that seems the most obvious is to check the Participants collection on the Conversation object. This suffers from a couple of problems. The first is fairly trivial – you need to check the IsSelf property of each participant in order to ensure you only pick up the remote participants. The second problem comes about when the incoming call is a multi-party call. The ConversationAdded event fires before the participants are added to the conversation, and so the Participants collection only contains the logged on user.

Instead, it is better to check the ConversationProperty.Inviter property in the Conversation.Properties collection. This property must be casted to a Contact object before using. For an incoming P2P (peer-to-peer) call, the property will represent the remote participant. For an incoming multi-party call, the property will represent the remote participant who invited the logged on user to the conversation.

1 // Get the URI of the "Inviter" contact 2 var remoteParticipant = ((Contact)conversation.Properties[ConversationProperty.Inviter]).Uri;

If you really must know the URI of every remote participant in the remote conversation, this can be achieved by listening for the ParticipantAdded event on the conversation object – although this will only fire after the user has accepted and joined the multi-party conversation.

For an incoming call, we can also test to determine which modalities are available – this tells us whether the call has audio/video and instant messaging. We’re looking for modalities that have a state of “Notified” (as in “the user has been notified that this modality is available, but has not yet joined it”).

There are 3 modalities that could potentially be available – Instant Messaging, Audio/Video and Sharing (e.g. desktop or application sharing). Unfortunately, we can only get information about the state of the IM and AV modalities – there is no property in the API that represents the sharing modality. This means that we can only infer whether the sharing modality is available when the IM and AV modalities are not available. If we have a conversation with neither, then it must have been initiated with a sharing request. If we do have an IM or AV modality in the “Notified” state, then we have no way of telling whether the sharing modality is available. Hopefully this will be fixed sometime in the future, in the meantime, here is the best I could do:

1 // Determine which modalities are available in the conversation 2 bool hasSharingOnly = true; 3 4 bool hasInstantMessaging = false; 5 if (ModalityIsNotified(conversation, ModalityTypes.InstantMessage)) 6 { 7 hasInstantMessaging = true; 8 hasSharingOnly = false; 9 } 10 11 bool hasAudioVideo = false; 12 if (ModalityIsNotified(conversation, ModalityTypes.AudioVideo)) 13 { 14 hasAudioVideo = true; 15 hasSharingOnly = false; 16 }

The ModalityIsNotified function simply checks that the modality exists on the conversation, and is in the “Notified” state:

1 private bool ModalityIsNotified(Conversation conversation, ModalityTypes modalityType) 2 { 3 return conversation.Modalities.ContainsKey(modalityType) && 4 conversation.Modalities[modalityType].State == ModalityState.Notified; 5 }

Finally, we can test to see if the incoming call is a multi-party conversation by checking the ConversationProperty.ConferencingUri property in the Conversation.Properties collection. If it is null, then we have a P2P conversation – otherwise, a multi-party conversation.

1 // Get whether this is a conference 2 bool isConference = conversation.Properties[ConversationProperty.ConferencingUri] != null;

Responding to Outgoing Calls

I’ve created a class called OutgoingCallNotifier to encapsulate this functionality. The class is shown in full here:

1 using System; 2 using System.Collections.Generic; 3 using Microsoft.Lync.Model; 4 using Microsoft.Lync.Model.Conversation; 5 6 namespace ScreenPops 7 { 8 class OutgoingCallNotifier 9 { 10 // The event raised to signal a new call 11 internal event EventHandler<NewOutgoingCallEventArgs> NewCall = delegate { }; 12 void OnNewCall(List<string> remoteParticipants, bool isConference) 13 { 14 NewCall(this, new NewOutgoingCallEventArgs(remoteParticipants, isConference)); 15 } 16 17 private LyncClient _lyncClient = null; 18 19 internal OutgoingCallNotifier() 20 { 21 // Get a reference to the running Lync client, register for the ConversationAdded event. 22 // Note: This assumes that the Lync client is running 23 _lyncClient = LyncClient.GetClient(); 24 _lyncClient.ConversationManager.ConversationAdded += ConversationManager_ConversationAdded; 25 } 26 27 void ConversationManager_ConversationAdded(object sender, Microsoft.Lync.Model.Conversation.ConversationManagerEventArgs e) 28 { 29 var conversation = e.Conversation; 30 31 // Test conversation state. If inactive, then the new conversation window was opened by the user, not a remote participant 32 if (conversation.State != ConversationState.Inactive) 33 return; 34 35 bool isConference = false; 36 var remoteParticipants = new List<string>(); 37 38 // Determine if the Inviter property is null - it will be null for a conference, in which case we need to look at the participants collection 39 var remoteParticipant = (Contact)conversation.Properties[ConversationProperty.Inviter]; 40 if (remoteParticipant == null) 41 { 42 isConference = true; 43 foreach (var participant in conversation.Participants) 44 if (!participant.IsSelf) remoteParticipants.Add(participant.Contact.Uri); 45 } 46 else 47 { 48 remoteParticipants.Add(remoteParticipant.Uri); 49 } 50 51 // Raise the NewCall event 52 OnNewCall(remoteParticipants, isConference); 53 } 54 } 55 } 56

 

Again, we’re interested in the ConversationAdded event – this time, we filter for conversations that only have a state of “Inactive”:

1 var conversation = e.Conversation; 2 3 // Test conversation state. If inactive, then the new conversation window was opened by the user, not a remote participant 4 if (conversation.State != ConversationState.Inactive) 5 return;

For an outgoing call, the ConversationAdded event fires as soon as the conversation window opens, and before any conversation modalities are initialised – this means it’s not possible to determine which modalities are active in this event handler. It is possible to register for the Modality.ModalityStateChanged event on the IM and AV modalities, but these will only fire when the relevant modalities are initiated, e.g. by the user sending the first IM, or clicking the Call button.

The method to determine who the remote participants are on the conversation depends on whether the conversation is P2P or multi-party. To determine that, the ConversationProperty.Inviter property in the Conversation.Properties collection can be checked. If this is null, the conversation is multi-party, and the Conversation.Participants collection contains all participants in the conversation. If the Inviter property is non-null, then the conversation is P2P, and the Inviter property contains a Contact object representing the remote participant.

1 bool isConference = false; 2 var remoteParticipants = new List<string>(); 3 4 // Determine if the Inviter property is null - it will be null for a conference, in which case we need to look at the participants collection 5 var remoteParticipant = (Contact)conversation.Properties[ConversationProperty.Inviter]; 6 if (remoteParticipant == null) 7 { 8 isConference = true; 9 foreach (var participant in conversation.Participants) 10 if (!participant.IsSelf) remoteParticipants.Add(participant.Contact.Uri); 11 } 12 else 13 { 14 remoteParticipants.Add(remoteParticipant.Uri); 15 }

Responding to Incoming Instant Messages

I’ve created a class called InstantMessageNotifier to encapsulate this functionality. The class is shown in full here:

1 using System; 2 using Microsoft.Lync.Model; 3 using Microsoft.Lync.Model.Conversation; 4 5 namespace ScreenPops 6 { 7 class InstantMessageNotifier 8 { 9 // The event raised to signal a new instant message 10 internal event EventHandler<NewInstantMessageEventArgs> NewInstantMessage = delegate { }; 11 void OnNewInstantMessage(string senderUri, string text) 12 { 13 NewInstantMessage(this, new NewInstantMessageEventArgs(senderUri, text)); 14 } 15 16 private LyncClient _lyncClient = null; 17 18 internal InstantMessageNotifier() 19 { 20 // Get a reference to the running Lync client, register for the ConversationAdded event. 21 // Note: This assumes that the Lync client is running 22 _lyncClient = LyncClient.GetClient(); 23 _lyncClient.ConversationManager.ConversationAdded += ConversationManager_ConversationAdded; 24 } 25 26 void ConversationManager_ConversationAdded(object sender, Microsoft.Lync.Model.Conversation.ConversationManagerEventArgs e) 27 { 28 var conversation = e.Conversation; 29 30 // Register for the ParticipantAdded event on the conversation 31 conversation.ParticipantAdded += Conversation_ParticipantAdded; 32 } 33 34 void Conversation_ParticipantAdded(object sender, ParticipantCollectionChangedEventArgs e) 35 { 36 var participant = e.Participant; 37 38 // We're not interested in notifying on our own IM's, so return 39 if (participant.IsSelf) 40 return; 41 42 // Get the InstantMessage modality, and register to the InstantMessageReceived event 43 var imModality = (InstantMessageModality)e.Participant.Modalities[ModalityTypes.InstantMessage]; 44 imModality.InstantMessageReceived += ImModality_InstantMessageReceived; 45 } 46 47 void ImModality_InstantMessageReceived(object sender, MessageSentEventArgs e) 48 { 49 // Get the instant mesage modality that raised this event, and the contact that it belongs to 50 var imModality = (InstantMessageModality)sender; 51 var contact = imModality.Participant.Contact; 52 53 var instantMessageText = e.Text; 54 55 // Raise the NewInstantMessage event 56 OnNewInstantMessage(contact.Uri, instantMessageText); 57 } 58 } 59 }

Again, we’re interested in the ConversationAdded event – but this time we listen for both incoming and outgoing conversations, as we want to capture instant messages on each. In the ConversationAdded event handler, we need to register to the ParticipantAdded event on the conversation:

1 var conversation = e.Conversation; 2 3 // Register for the ParticipantAdded event on the conversation 4 conversation.ParticipantAdded += Conversation_ParticipantAdded;

When a participant is added, we then need to register for the InstantMessageReceived event on the participant’s instant messaging modality. Because we’re only interested in notifying on instant messages from remote participants, we can test the Participant.IsSelf property to ensure we don’t listen for the local participant’s instant messages:

1 var participant = e.Participant; 2 3 // We're not interested in notifying on our own IM's, so return 4 if (participant.IsSelf) 5 return; 6 7 // Get the InstantMessage modality, and register to the InstantMessageReceived event 8 var imModality = (InstantMessageModality)e.Participant.Modalities[ModalityTypes.InstantMessage]; 9 imModality.InstantMessageReceived += ImModality_InstantMessageReceived;

When InstantMessageReceived fires, we can cast the sender object to an InstantMessageModality, and determine the participant who sent the message by looking at the Participant property. The instant message text is contained in the MessageSentEventArgs.Text property.

Using the Notifier classes

Each Notifier class raises an event to inform interested parties that something has happened. A class interested in receiving these events simple need to create an instance of the class, and register for the event. The code below is in the main window in the sample project ():

1 using System.Windows; 2 3 namespace ScreenPops 4 { 5 /// <summary> 6 /// Interaction logic for Window1.xaml 7 /// </summary> 8 public partial class Window1 : Window 9 { 10 private IncomingCallNotifier _incomingCallNotifier = null; 11 private OutgoingCallNotifier _outgoingCallNotifier = null; 12 private InstantMessageNotifier _instantMessageNotifier = null; 13 14 public Window1() 15 { 16 InitializeComponent(); 17 18 // Create a new instance of the incoming call notifier, and register for the New Call event 19 _incomingCallNotifier = new IncomingCallNotifier(); 20 _incomingCallNotifier.NewCall += IncomingCallNotifier_NewCall; 21 22 // Create a new instance of the outgoing call notifier, and register for the New Call event 23 _outgoingCallNotifier = new OutgoingCallNotifier(); 24 _outgoingCallNotifier.NewCall += OutgoingCallNotifier_NewCall; 25 26 // Create a new instance of the instant message notifier, and register for the New Instant Message event 27 _instantMessageNotifier = new InstantMessageNotifier(); 28 _instantMessageNotifier.NewInstantMessage += InstantMessageNotifier_NewInstantMessage; 29 } 30 31 void IncomingCallNotifier_NewCall(object sender, NewIncomingCallEventArgs e) 32 { 33 // Just show a message box with the call details 34 MessageBox.Show(string.Format("Incoming Call\r\nCaller: {0}\r\nSharing Only: {1}\r\nHas Instant Messaging: {2}\r\nHas Audio/Video: {3}\r\nIs a Conference: {4}", 35 e.RemoteParticipant, 36 e.HasSharingOnly, 37 e.HasInstantMessaging, 38 e.HasAudioVideo, 39 e.IsConference 40 )); 41 } 42 43 void OutgoingCallNotifier_NewCall(object sender, NewOutgoingCallEventArgs e) 44 { 45 // Just show a message box with the call details 46 MessageBox.Show(string.Format("Outgoing Call\r\nCaller: {0}\r\nIs a Conference: {1}", 47 string.Join(", ", e.RemoteParticipants.ToArray()), 48 e.IsConference 49 )); 50 } 51 52 void InstantMessageNotifier_NewInstantMessage(object sender, NewInstantMessageEventArgs e) 53 { 54 // Just show a message box with the instant message details 55 MessageBox.Show(string.Format("Caller: {0}\r\nText: {1}", 56 e.SenderUri, 57 e.Text 58 )); 59 60 } 61 } 62 }

 

A real screen pop solution could take the information raised in the events, and use it to query a data source for further information to display to the user. That, as they say, is left as an exercise for the reader. Enjoy!

The sample project is available for download here.

18 thoughts on “Developing screen pop applications

  1. Pingback: Blog recommendation | Lync Development

  2. Thanks for your Lync wrapper! It helped me get up to speed rapidly with Lync API development.

    I have it working great with incoming and outgoing IM’s and outgoing phone calls. For someone unknown reason incoming calls do not trigger an event. I have Avaya One-X installed but not running.

    Do you have any advice as where to troubleshoot? Avaya does have an API to work with Lync. Is that necessary even when Avaya is not running? I will likely need the Avaya API to get the Lync API (via your wrapper) to receive incoming phone events when Avaya One-X is running.

    Avaya API link: http://support.avaya.com/css/P8/documents/100152872

    • Thanks Stephen, glad you found it useful. Are you saying you don’t receive the ConversationAdded event when Lync receives an incoming call?

      I’ve not come across this before. Is it possible that you already had an IM conversation window open with the user when the call came in? The ConversationAdded event wouldn’t fire in this case.

    • Hi Lync Guy,Nice article, I came acorss it while researching something weird, figured I throw it on here, maybe you or someone else knows whats up We have a federated partner, actually another division of our company, everything was working as intended, but now, they can not search for any of our Lync users by email\sip address when they type in & hit enter, nothing happens, not even Presence Unknown . However, if they add the contact to their local outlook contacts, it works just fine even if they just add them with an old email domain address that does not match the sip domain. It seems like it is matching the local outlook contact with the Exchange address book contact item. The can then see presence, send IMs, etc. with that person on our side.Of course they have no issues with Federation with any other partner, either dynamic or direct.Sorry for the rambling post, I know you aren’t my tech support, but if you have any clue, I appreciate it.Thanks,Sal

  3. Pingback: Answering the Call : accepting incoming calls in Lync Client SDK | Developing Lync

  4. Hi,
    We are trying to create an app with two features
    - Show Lync notification popup from an external app
    - Add a wpf control to Lync client
    Is it possible to to do this with Lync SDk. Any poinetrs would be appriciated
    Thanks,

    • Sure – this post should give you everything you need to meet the first requirement (unless i’ve misunderstood the requirement?). The second requirement isn’t possible in any supported by using the SDKs.

  5. Pingback: Introducing Lync 2012 Super Simple Auto Answer Video Kiosk with Full Screen - Scott Hanselman

  6. Pingback: Introducing Lync 2010 Super Simple Auto Answer Video Kiosk with Full Screen - Scott Hanselman

  7. This is very interesting, thanks for posting it.

    We want to use Lync with a custom CRM application and want Toast to pull up information from the CRM DB once a call comes in, then there should be a hyperlink from toast to the user if existing in CRM. Any help will be very much appreciated.

    Thanks.

    • You won’t be able to modify the standard Lync Toast message, but you could display your own screen containing the CRM data. This article should get you started on the Lync side, but obviously I can’t comment on how to pull data back from your CRM system :)

  8. Great class, however I have attempted to implement it into another class that is controlling the Lync connection from our software, when the delegation events are in the class the messagebox doesn’t appear, when they are on the main UI form they do, is there anything you can suggest?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>