Developing screen pop applications

This article was imported from codelync.com

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.

Responding to Incoming Calls

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

using System;
using Microsoft.Lync.Model;
using Microsoft.Lync.Model.Conversation;

namespace ScreenPops
{
    class IncomingCallNotifier
    {
        // The event raised to signal a new call
        internal event EventHandler<NewIncomingCallEventArgs> NewCall = delegate { };
        void OnNewCall(string remoteParticipant, bool hasSharingOnly, bool hasInstantMessaging, bool hasAudioVideo, bool isConference)
        {
            NewCall(this, new NewIncomingCallEventArgs(remoteParticipant, hasSharingOnly, hasInstantMessaging, hasAudioVideo, isConference));
        }

        private LyncClient _lyncClient = null;

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

        void ConversationManager_ConversationAdded(object sender, Microsoft.Lync.Model.Conversation.ConversationManagerEventArgs e)
        {
            var conversation = e.Conversation;

            // Test conversation state. If inactive, then the new conversation window was opened by the user, not a remote participant
            if (conversation.State == ConversationState.Inactive)
                return;

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

            // Determine which modalities are available in the conversation
            bool hasSharingOnly = true;
            
            bool hasInstantMessaging = false;
            if (ModalityIsNotified(conversation, ModalityTypes.InstantMessage))
            {
                hasInstantMessaging = true;
                hasSharingOnly = false;
            }

            bool hasAudioVideo = false;
            if (ModalityIsNotified(conversation, ModalityTypes.AudioVideo))
            {
                hasAudioVideo = true;
                hasSharingOnly = false;
            }

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

            // Raise the NewCall event
            OnNewCall(remoteParticipant, hasSharingOnly, hasInstantMessaging, hasAudioVideo, isConference);
        }

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

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:

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

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”.

var conversation = e.Conversation;

// Test conversation state. If inactive, then the new conversation window was opened by the user, not a remote participant
if (conversation.State == ConversationState.Inactive)
    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.

// Get the URI of the "Inviter" contact
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:

// Determine which modalities are available in the conversation
bool hasSharingOnly = true;
 
bool hasInstantMessaging = false;
if (ModalityIsNotified(conversation, ModalityTypes.InstantMessage))
{
    hasInstantMessaging = true;
    hasSharingOnly = false;
}

bool hasAudioVideo = false;
if (ModalityIsNotified(conversation, ModalityTypes.AudioVideo))
{
    hasAudioVideo = true;
    hasSharingOnly = false;
}

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

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

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.

// Get whether this is a conference
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:

using System;
using System.Collections.Generic;
using Microsoft.Lync.Model;
using Microsoft.Lync.Model.Conversation;

namespace ScreenPops
{
    class OutgoingCallNotifier
    {
        // The event raised to signal a new call
        internal event EventHandler<NewOutgoingCallEventArgs> NewCall = delegate { };
        void OnNewCall(List<string> remoteParticipants, bool isConference)
        {
            NewCall(this, new NewOutgoingCallEventArgs(remoteParticipants, isConference));
        }

        private LyncClient _lyncClient = null;

        internal OutgoingCallNotifier()
        {
            // Get a reference to the running Lync client, register for the ConversationAdded event.
            // Note: This assumes that the Lync client is running
            _lyncClient = LyncClient.GetClient();
            _lyncClient.ConversationManager.ConversationAdded += ConversationManager_ConversationAdded;
        }

        void ConversationManager_ConversationAdded(object sender, Microsoft.Lync.Model.Conversation.ConversationManagerEventArgs e)
        {
            var conversation = e.Conversation;

            // Test conversation state. If inactive, then the new conversation window was opened by the user, not a remote participant
            if (conversation.State != ConversationState.Inactive)
                return;

            bool isConference = false;
            var remoteParticipants = new List<string>();

            // 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
            var remoteParticipant = (Contact)conversation.Properties[ConversationProperty.Inviter];
            if (remoteParticipant == null)
            {
                isConference = true;
                foreach (var participant in conversation.Participants)
                    if (!participant.IsSelf) remoteParticipants.Add(participant.Contact.Uri);
            }
            else
            {
                remoteParticipants.Add(remoteParticipant.Uri);
            }

            // Raise the NewCall event
            OnNewCall(remoteParticipants, isConference);
        }
    }
}

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

var conversation = e.Conversation;

// Test conversation state. If inactive, then the new conversation window was opened by the user, not a remote participant
if (conversation.State != ConversationState.Inactive)
    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.

bool isConference = false;
var remoteParticipants = new List<string>();

// 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
var remoteParticipant = (Contact)conversation.Properties[ConversationProperty.Inviter];
if (remoteParticipant == null)
{
    isConference = true;
    foreach (var participant in conversation.Participants)
        if (!participant.IsSelf) remoteParticipants.Add(participant.Contact.Uri);
}
else
{
    remoteParticipants.Add(remoteParticipant.Uri);
}

Responding to Incoming Instant Messages

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

using System;
using Microsoft.Lync.Model;
using Microsoft.Lync.Model.Conversation;

namespace ScreenPops
{
    class InstantMessageNotifier
    {
        // The event raised to signal a new instant message
        internal event EventHandler<NewInstantMessageEventArgs> NewInstantMessage = delegate { };
        void OnNewInstantMessage(string senderUri, string text)
        {
            NewInstantMessage(this, new NewInstantMessageEventArgs(senderUri, text));
        }

        private LyncClient _lyncClient = null;

        internal InstantMessageNotifier()
        {
            // Get a reference to the running Lync client, register for the ConversationAdded event.
            // Note: This assumes that the Lync client is running
            _lyncClient = LyncClient.GetClient();
            _lyncClient.ConversationManager.ConversationAdded += ConversationManager_ConversationAdded;
        }

        void ConversationManager_ConversationAdded(object sender, Microsoft.Lync.Model.Conversation.ConversationManagerEventArgs e)
        {
            var conversation = e.Conversation;

            // Register for the ParticipantAdded event on the conversation
            conversation.ParticipantAdded += Conversation_ParticipantAdded;
        }

        void Conversation_ParticipantAdded(object sender, ParticipantCollectionChangedEventArgs e)
        {
            var participant = e.Participant;

            // We're not interested in notifying on our own IM's, so return
            if (participant.IsSelf)
                return;

            // Get the InstantMessage modality, and register to the InstantMessageReceived event
            var imModality = (InstantMessageModality)e.Participant.Modalities[ModalityTypes.InstantMessage];
            imModality.InstantMessageReceived += ImModality_InstantMessageReceived;
        }

        void ImModality_InstantMessageReceived(object sender, MessageSentEventArgs e)
        {
            // Get the instant mesage modality that raised this event, and the contact that it belongs to
            var imModality = (InstantMessageModality)sender;
            var contact = imModality.Participant.Contact;
            
            var instantMessageText = e.Text;

            // Raise the NewInstantMessage event
            OnNewInstantMessage(contact.Uri, instantMessageText);
        }
    }
}

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:

var conversation = e.Conversation;

// Register for the ParticipantAdded event on the conversation
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:

var participant = e.Participant;

// We're not interested in notifying on our own IM's, so return
if (participant.IsSelf)
    return;

// Get the InstantMessage modality, and register to the InstantMessageReceived event
var imModality = (InstantMessageModality)e.Participant.Modalities[ModalityTypes.InstantMessage];
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 project:

using System.Windows;

namespace ScreenPops
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        private IncomingCallNotifier _incomingCallNotifier = null;
        private OutgoingCallNotifier _outgoingCallNotifier = null;
        private InstantMessageNotifier _instantMessageNotifier = null;

        public Window1()
        {
            InitializeComponent();

            // Create a new instance of the incoming call notifier, and register for the New Call event
            _incomingCallNotifier = new IncomingCallNotifier();
            _incomingCallNotifier.NewCall += IncomingCallNotifier_NewCall;

            // Create a new instance of the outgoing call notifier, and register for the New Call event
            _outgoingCallNotifier = new OutgoingCallNotifier();
            _outgoingCallNotifier.NewCall += OutgoingCallNotifier_NewCall;

            // Create a new instance of the instant message notifier, and register for the New Instant Message event
            _instantMessageNotifier = new InstantMessageNotifier();
            _instantMessageNotifier.NewInstantMessage += InstantMessageNotifier_NewInstantMessage;
        }

        void IncomingCallNotifier_NewCall(object sender, NewIncomingCallEventArgs e)
        {
            // Just show a message box with the call details
            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}",
                e.RemoteParticipant,
                e.HasSharingOnly,
                e.HasInstantMessaging,
                e.HasAudioVideo,
                e.IsConference
                ));
        }

        void OutgoingCallNotifier_NewCall(object sender, NewOutgoingCallEventArgs e)
        {
            // Just show a message box with the call details
            MessageBox.Show(string.Format("Outgoing Call\r\nCaller: {0}\r\nIs a Conference: {1}",
                string.Join(", ", e.RemoteParticipants.ToArray()),
                e.IsConference
                ));
        }

        void InstantMessageNotifier_NewInstantMessage(object sender, NewInstantMessageEventArgs e)
        {
            // Just show a message box with the instant message details
            MessageBox.Show(string.Format("Caller: {0}\r\nText: {1}",
                e.SenderUri,
                e.Text
                ));
            
        }
    }
}

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!

Paul Nearney Written by:

Paul is a software solution architect specialising in Microsoft Teams, and the founder of Chimu Software