Using UCMA 3.0 and Lync 2010 for Contextual Communication: Code Walkthrough (Part 5 of 6)

Summary:   This article is the fifth in a series of six articles that describe how to create a Microsoft Unified Communications Managed API (UCMA) 3.0 Core application that sets up a two-way contextual data channel with a Microsoft Lync 2010 application that uses Microsoft Silverlight. The Lync 2010 application runs in the Lync 2010 Conversation Window Extension.

The code for the UCMA 3.0 application and the Lync 2010 SDK Silverlight application appears in this article.

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

Published:   February 2011 | Provided by:   Mark Parker and John Clarkson, Microsoft | About the Authors

Contents

  • UCMA 3.0 Code

  • Lync API Code

  • Part 6

  • Additional Resources

Code Gallery  Download code

This article is the fifth in a six-part series of articles on using UCMA 3.0 and Lync 2010 for contextual communications.

UCMA 3.0 Code

The UCMA 3.0 application code appears in this section together with helper code that is used to prepare the basic objects that are used in UCMA 3.0 applications.

Configuration Code

The App.Config application configuration file is used to configure settings for the Microsoft Lync Server 2010 computer, the local endpoint (the UCMA 3.0 application), and the remote endpoint (the Lync 2010 application). When the appropriate parameters are added to the add elements and the XML comment delimiters are removed, you do not have to manually type the parameter values during run time.

App.Config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <!-- Provide parameters necessary for the sample to run without prompting for user input. -->
    <!-- Provide the FQDN of the Microsoft Lync 2010 Server -->
    <!-- <add key="ServerFQDN1" value="" />  -->
    <!-- The user sign-in name that is used to sign in to the application. -->
    <!-- To use credentials used by the currently signed-in user, do not add a value. -->
    <!-- <add key="UserName1" value="" /> -->
    <!-- The user domain name that is used to sign in to the application. -->
    <!-- To use credentials used by the currently signed-in user, do not add a value. -->
    <!-- <add key="UserDomain1" value="" /> -->
    <!-- The user URI that is used to sign in to the application, in the format user@host. -->
    <!-- <add key="UserURI1" value="" /> -->
    <!-- The URI of the remote endpoint, in the format sip:user@host -->
    <!-- <add key="UserURI2" value="" /> -->

  </appSettings>
  <system.web>
    <membership defaultProvider="ClientAuthenticationMembershipProvider">
      <providers>
        <add name="ClientAuthenticationMembershipProvider" type="System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" />
      </providers>
    </membership>
    <roleManager defaultProvider="ClientRoleProvider" enabled="true">
      <providers>
        <add name="ClientRoleProvider" type="System.Web.ClientServices.Providers.ClientRoleProvider, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" cacheTimeout="86400" />
      </providers>
    </roleManager>
  </system.web>
</configuration>

Application Code

The following code is needed to create and establish a ConversationContextChannel instance that is used to exchange context data with the Lync 2010 application that runs on the Lync 2010 remote client. The code includes a Run method, in which most of the action occurs. It also includes handlers for the various events that are raised during application run time, callback methods that are used in the asynchronous method calls, and a utility method that parses an input string and then formats the appropriate string that is sent to the remote client.

ContextualCommunication.cs

using System;
using System.Threading;
using System.Text;
using Microsoft.Rtc.Collaboration;
using Microsoft.Rtc.Collaboration.AudioVideo;
using Microsoft.Rtc.Signaling;
using Microsoft.Rtc.Collaboration.Sample.Common;
using System.Net.Mime;
using System.Collections.Generic;


namespace Microsoft.Rtc.Collaboration.Sample.ContextChannelSample
{
  // This application represents a simple outbound InstantMessaging call to a remote user.
  // This application signs in as the specified user, and places an IM call to the targeted user URI.
  // After establishing a conversation with a remote user, the application creates and establishes a 
  // ConversationContextChannel instance. After the context channel is established, the application 
  // receives contextual data from the remote user, and then sends additional contextual data
  // to the remote user.
  // After this application ends the call, it closes the platform, and then pauses the console to allow logs to be viewed.

  // This application requires the credentials of two Microsoft Lync Server 2010 users.
  // Ensure that the users in question can sign in to Lync Server 2010 with the Lync 2010 credentials provided.
  // Sign in must occur on the computer that runs this code.

  public class UCMABasicIMCall
  {
    // Some necessary instance variables.
    private UCMASampleHelper _helper;
    private InstantMessagingCall _IMCall;
    private InstantMessagingFlow _IMFlow;
    private UserEndpoint _userEndpoint;

    // The information for the conversation and the remote participant.
    // The target of the call in the format sip:user@host (and user should be signed in to the application when the application runs). 
    // Alternative format: tel:+1XXXYYYZZZZ.
    private static String _calledParty;

    // Subject of the conversation that appears in the heading of the conversation window if Lync 2010 is the far-end client.
    private static String _conversationSubject = "Conversation between UCMA and Lync 2010"; 
    private static String _conversationPriority = ConversationPriority.Normal;

    // The conversation.
    private Conversation _conversation;
 
    // The conversation context channel.
    private ConversationContextChannel _channel;
       
  
    // Wait handles are present only to keep things synchronous and easy to read.
    private AutoResetEvent _waitForConversationToTerminate = new AutoResetEvent(false);
    private AutoResetEvent _waitForCallToEstablish = new AutoResetEvent(false);
    private AutoResetEvent _waitForChannelToEstablish = new AutoResetEvent(false);
    private AutoResetEvent _waitForChannelDataToBeSent = new AutoResetEvent(false);
    private AutoResetEvent _waitForCallToTerminate = new AutoResetEvent(false);
    private AutoResetEvent _waitForChannelDataToBeReceived = new AutoResetEvent(false);


    static void Main(string[] args)
    {
      UCMABasicIMCall _basicIMCall= new UCMABasicIMCall();
      _basicIMCall.Run();
    }

    public void Run()
    {
                 
      // Initialize and register the endpoint, using the credentials of the user that the application represents.
      _helper = new UCMASampleHelper();

      _userEndpoint = _helper.CreateEstablishedUserEndpoint("Contextual Communication Sample User" /*endpointFriendlyName*/);
           
      // Set up the conversation.
      ConversationSettings _convSettings = new ConversationSettings();
      _convSettings.Priority = _conversationPriority;
      _convSettings.Subject = _conversationSubject;

      // Conversation represents a collection of modalities in the context of a dialog with one or more call recipients.
      _conversation = new Conversation(_userEndpoint, _convSettings);
            
      _IMCall = new InstantMessagingCall(_conversation);
            
      // Call: StateChanged: Only hooked up for logging.
      _IMCall.StateChanged += new EventHandler<CallStateChangedEventArgs>(IMCall_StateChanged);

      // Subscribe for the flow configuration requested event. The flow is used to send the media.
      // Ultimately, as a part of the callback, the media is sent/received.
      _IMCall.InstantMessagingFlowConfigurationRequested += this.IMCall_FlowConfigurationRequested;

      // Get the URI of the called party.            
      _calledParty = _helper.GetRemoteUserURI();
            
      // Place the call to the remote party.
      _IMCall.BeginEstablish(_calledParty, null, EndCallEstablish, _IMCall);
      Console.WriteLine("\nCalling the remote user...");
      Console.WriteLine("\nConversation ID = {0}", _conversation.Id);


      // Wait for the call to become established.
      _waitForCallToEstablish.WaitOne();

      // Establish the context channel. 
      _channel = new ConversationContextChannel(_conversation, _IMCall.RemoteEndpoint);

      // Register handlers for the DataReceived and StateChanged events on ConversationContextChannel.
      _channel.DataReceived += new EventHandler<ConversationContextChannelDataReceivedEventArgs>(channel_DataReceived);
      _channel.StateChanged += new EventHandler<ConversationContextChannelStateChangedEventArgs>(channel_StateChanged);

      // Configure a set of options for a context channel.
      ConversationContextChannelEstablishOptions channelOptions = new ConversationContextChannelEstablishOptions();
            
      channelOptions.ApplicationName = "Blue Yonder Airlines";
      // Normally used for custom initialization of the remote application.
      channelOptions.ContextualData = "Context channel is open.";  
      // The text that appears in the “toast”.
      channelOptions.Toast = "Get ready for incoming contextual data.";

      // The application ID.
      Guid guid = new Guid("3271E259-E508-4D39-B044-445855591E79");
     
      // Establish a context channel to the remote endpoint.
      _channel.BeginEstablish(guid, channelOptions, BeginEstablishCB, null);
      _waitForChannelDataToBeReceived.WaitOne();

      _channel.BeginTerminate(BeginTerminateCB, null);

      // Terminate the call, and then the conversation.
      _IMCall.BeginTerminate(EndTerminateCall, _IMCall);
      Console.WriteLine("\nWaiting for the call to be terminated...");
      _waitForCallToTerminate.WaitOne();

      _IMCall.Conversation.BeginTerminate(EndTerminateConversation, _IMCall.Conversation);
      Console.WriteLine("\nWaiting for the conversation to be terminated...");
      _waitForConversationToTerminate.WaitOne();

      // Close the platform.
      Console.WriteLine("\nShutting down the platform...");
      _helper.ShutdownPlatform();

      // Pause the console to allow user to to view logs.
      Console.WriteLine("\nPress any key to end the sample.");
      Console.ReadKey(); 
    }

 
    #region EVENT HANDLERS
  
    // Event handler to record the call state transitions in the console.
    void IMCall_StateChanged(object sender, CallStateChangedEventArgs e)
    {
      Console.WriteLine("\nCall has changed state. The previous call state was: " + e.PreviousState + " and the current state is: " + e.State);
    }

    // Event handler for the StateChanged event on the channel. 
    void channel_StateChanged(object sender, ConversationContextChannelStateChangedEventArgs e)
    {
      Console.WriteLine("\nChannel state change reason is {0}", e.TransitionReason.ToString());
      Console.WriteLine("\nChannel state is {0}", _channel.State.ToString());
    }

    // Flow created indicates that there is a flow present to begin media operations with, and that it is no longer null.
    public void IMCall_FlowConfigurationRequested(object sender, InstantMessagingFlowConfigurationRequestedEventArgs e)
    {
      Console.WriteLine("\nFlow Created.");
      _IMFlow = e.Flow;

      // Now that the flow is non-null, bind the event handler for StateChanged.
      // When the flow goes active, (as indicated by the state changed event) the application can take media-related actions on the flow.
      _IMFlow.StateChanged += new EventHandler<MediaFlowStateChangedEventArgs>(IMFlow_StateChanged);
    }

    private void IMFlow_StateChanged(object sender, MediaFlowStateChangedEventArgs e)
    {
      Console.WriteLine("\nFlow state changed from " + e.PreviousState + " to " + e.State);

      // When flow is active, media operations can begin.
      if (e.State == MediaFlowState.Active)
      {
        // Other samples demonstrate uses for an active flow.
      }
    }

    // Event handler for the DataReceived event on ConversationContextChannel. 
    void channel_DataReceived(object sender, ConversationContextChannelDataReceivedEventArgs e)
    {
      Console.WriteLine("\nRequest data: {0}", e.RequestData.MessageType);
      Console.WriteLine("\nContent type: {0}", e.ContentDescription.ToString());

      // Assume that no more than 100 bytes are sent at a time.
      Byte[] body_byte = new Byte[100];
      body_byte = e.ContentDescription.GetBody();
      SendDataToRemote(body_byte);

      String body_UTF8 = null;
      body_UTF8 = Converter.ConvertByteArrayToString(body_byte, EncodingType.UTF8);

      Console.WriteLine("\nContent body: {0}", body_UTF8);
      _waitForChannelDataToBeReceived.Set();
    }
    #endregion 


    #region HELPER METHODS
    /// <summary>
    /// Parses the input parameter and sends the appropriate data to the remote side of the channel.
    /// </summary>
    /// <param name="data">An array of bytes received from the context channel.</param>
    private void SendDataToRemote(Byte[] data)
    {
      // Each string to be sent consists of three fields, separated by commas or semicolons.
      String strCamera = "SLR X1000,$199.95,In stock";
      String strPhone = "ABC Smartphone,$149.50, In stock";
      String strGPS = "Gourmand XYZ,$210.00, On backorder";
      String strError = "Error,No item chosen,";
      String temp;

      // Convert data to String.
      String tempString = Converter.ConvertByteArrayToString(data, EncodingType.ASCII);

      if (tempString.Equals("Camera")) temp = strCamera;
      else if (tempString.Equals("Smartphone")) temp = strPhone;
      else if (tempString.Equals("GPS")) temp = strGPS;
      else temp = strError;

      // Convert temp to Byte[].
      Byte[] data_bytes = new byte[temp.Length];

      ASCIIEncoding data_ASCII = new ASCIIEncoding();
      data_bytes = data_ASCII.GetBytes(temp);

      ContentType contentType = new ContentType("text/plain; charset=us-ascii");

      _channel.BeginSendData(contentType, data_bytes, BeginSendDataCB, null);
      _waitForChannelDataToBeSent.WaitOne();
    }
    #endregion


    #region CALLBACK METHODS

    private void EndCallEstablish(IAsyncResult ar)
    {
      Call call = ar.AsyncState as Call;
      try
      {
        call.EndEstablish(ar);
        Console.WriteLine("\nThe call with Local Participant: " + 
            call.Conversation.LocalParticipant + 
            " and Remote Participant: " + 
            call.RemoteEndpoint.Participant +
            " is now in the established state.");
        Console.WriteLine("\nConversation ID (in EndCallEstablish) = ", _conversation.Id);
      }
      catch (OperationFailureException opFailEx)
      {
        // OperationFailureException: Indicates failure to connect the call to the remote party.
        // It is left to the application to perform real error-handling here.
        Console.WriteLine(opFailEx.ToString());
      }
      catch (RealTimeException exception)
      {
        // RealTimeException may be thrown on media or link-layer failures.
        // It is left to the application to perform real error-handling here.
        Console.WriteLine(exception.ToString());
      }
      finally
      {
        // Synchronize threads.
        _waitForCallToEstablish.Set();
      }
    }

    // Callback for BeginEstablish on ConversationContextChannel.
    private void BeginEstablishCB(IAsyncResult result)
    {
      if (_channel.State == ConversationContextChannelState.Establishing)
      {
        Console.WriteLine("\nChannel is in Establishing state.");
        _channel.EndEstablish(result);
      }
    }

    // Callback for BeginSendData on ConversationContextChannel.
    private void BeginSendDataCB(IAsyncResult ar)
    {
      _channel.EndSendData(ar);
      Console.WriteLine("\nEndSendData() called on channel.");
      _waitForChannelDataToBeSent.Set();
    }

    // Callback for BeginTerminate on ConversationContextChannel.
    private void BeginTerminateCB(IAsyncResult ar)
    {
      _channel.EndTerminate(ar);
    }

    private void EndTerminateCall(IAsyncResult ar)
    {
      InstantMessagingCall IMCall = ar.AsyncState as InstantMessagingCall;

      IMCall.EndTerminate(ar);

      // Synchronize threads.
      _waitForCallToTerminate.Set();
    }

    private void EndTerminateConversation(IAsyncResult ar)
    {
      Conversation conv = ar.AsyncState as Conversation;

      conv.EndTerminate(ar);

      // Synchronize threads.
      _waitForConversationToTerminate.Set();
    }

    #endregion

  }
}

Helper Class

The next code consists of a class whose member methods create and start a CollaborationPlatform instance, and then create and establish the UserEndpoint instance that is used in the sample. The code appearing in this article is an abbreviated version of the code that appears in the UCMA 3.0 SDK UCMASampleCode.cs file.

The following code differs from UCMASampleCode.cs in four ways:

  1. The sample presented here is shorter. Methods related to creating and establishing an ApplicationEndpoint instance are removed. The following methods are removed.

    • CreateUserEndpointWithServerPlatform

    • ReadGenericApplicationContactConfiguration

    • ReadApplicationContactConfiguration

    • CreateApplicationEndpoint

    • CreateAndStartServerPlatform

  2. A number of unused private fields are excluded from the following code. Code that uses one of these fields, _serverCollabPlatform, is also excluded from the code sample.

  3. Two variables are added: _remoteUserURIPrompt and _remoteUserURI.

  4. The GetRemoteUserURI method is added to the following code.

UCMASampleHelper.cs (abbreviated)

using System;
using System.Configuration;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;

using Microsoft.Rtc.Collaboration;
using Microsoft.Rtc.Signaling;

namespace Microsoft.Rtc.Collaboration.Sample.Common
{
  class UCMASampleHelper
  {
    private static ManualResetEvent _sampleFinished = new ManualResetEvent(false);

    // The name of this application, to be used as the outgoing user agent string.
    // The user agent string is put in outgoing message headers to identify the application that is used.
    private static string _applicationName = "UCMASampleCode";

    const string _sipPrefix = "sip:";

    // These strings are used as keys into the App.Config file to get information to avoid prompting. For most of these strings,
    // suffixes 1-N are used on each subsequent call. For example, UserName1 is used for the first user and UserName2 for the second user.
    private static String _serverFQDNPrompt = "ServerFQDN";
    private static String _userNamePrompt = "UserName";
    private static String _userDomainPrompt = "UserDomain";
    private static String _userURIPrompt = "UserURI";
    private static String _remoteUserURIPrompt = "UserURI";

    // Construct the network credential that the UserEndpoint will use for authentication by the Microsoft Lync Server 2010 computer.
    private string _userName; // User name and password for a user who is authorized to access Lync Server 2010. 
    private string _userPassword;
    private string _userDomain; // Domain that this user signs in to. Note: This is the Active Directory domain, not the portion of the SIP URI following the ’@’ sign.
    private System.Net.NetworkCredential _credential;

    // The user URI and connection server.
    private string _userURI; // This should be the URI of the user specified earlier.
    // Note: Made public to be visible in external files.
    public string _remoteUserURI; // This should be the URI of the remote user specified earlier.
    // The server FQDN.
    private static string _LyncServerFQDN;// The FQDN of the Microsoft Lync Server 2010 computer.

    // Transport type used to communicate with the Lync Server 2010 computer.
    private Microsoft.Rtc.Signaling.SipTransportType _transportType = Microsoft.Rtc.Signaling.SipTransportType.Tls;

    private static CollaborationPlatform _collabPlatform;
    private static bool _isPlatformStarted;
    // private static CollaborationPlatform _serverCollabPlatform;

    private AutoResetEvent _platformStartupCompleted = new AutoResetEvent(false);
    private AutoResetEvent _endpointInitCompletedEvent = new AutoResetEvent(false);
    private AutoResetEvent _platformShutdownCompletedEvent = new AutoResetEvent(false);
    private UserEndpoint _userEndpoint;

    private bool _useSuppliedCredentials;
    // private static int _appContactCount;
    private static int _userCount = 1;

    // This method attempts to read user settings from the App.Config file. If the settings are not
    // present in the configuration file, this method prompts the user for them in the console.
    // This method returns a UserEndpointSettings object. If you do not want to monitor LocalOwnerPresence, you can 
    // call the CreateEstablishedUserEndpoint method directly. Otherwise, you can call the ReadUserSettings, 
    // CreateUserEndpoint, and EstablishUserEndpoint methods, in that order.
    public UserEndpointSettings ReadUserSettings(string userFriendlyName)
    {
      UserEndpointSettings userEndpointSettings = null;
      string prompt = string.Empty;
      if (string.IsNullOrEmpty(userFriendlyName))
      {
        userFriendlyName = "Default User";
      }

      try
      {
        Console.WriteLine(string.Empty);
        Console.WriteLine("Creating User Endpoint for {0}...", userFriendlyName);
        Console.WriteLine();

        if (ConfigurationManager.AppSettings[_serverFQDNPrompt + _userCount] != null)
        {
          _LyncServerFQDN = ConfigurationManager.AppSettings[_serverFQDNPrompt + _userCount];
          Console.WriteLine("Using {0} as Lync Server", _LyncServerFQDN);
        }
        else
        {
          // Prompt user for server FQDN. If server FQDN was entered previously, then use the saved value.
          string localServer;
          StringBuilder promptBuilder = new StringBuilder();
          if (!string.IsNullOrEmpty(_LyncServerFQDN))
          {
            promptBuilder.Append("Current Microsoft Communication Server = ");
            promptBuilder.Append(_LyncServerFQDN);
            promptBuilder.AppendLine(". Please hit ENTER to retain this setting - OR - ");
          }

          promptBuilder.Append("Please enter the FQDN of the Lync Server that the ");
          promptBuilder.Append(userFriendlyName);
          promptBuilder.Append(" endpoint is homed on => ");
          localServer = PromptUser(promptBuilder.ToString(), null);

          if (!String.IsNullOrEmpty(localServer))
          {
            _LyncServerFQDN = localServer;
          }
        }

        // Prompt user for user name.
        prompt = String.Concat("Please enter the User Name for ",
                                        userFriendlyName,
                                        " (or hit the ENTER key to use current credentials)\r\n" +
                                        "Please enter the User Name => ");
        _userName = PromptUser(prompt, _userNamePrompt + _userCount);

        // If user name is empty, use current credentials.
        if (string.IsNullOrEmpty(_userName))
        {
          Console.WriteLine("Username was empty - using current credentials...");
          _useSuppliedCredentials = true;
        }
        else
        {
          // Prompt for password.
          prompt = String.Concat("Enter the User Password for ", userFriendlyName, " => ");
          _userPassword = PromptUser(prompt, null);

          prompt = String.Concat("Please enter the User Domain for ", userFriendlyName, " => ");
          _userDomain = PromptUser(prompt, _userDomainPrompt + _userCount);
        }

        // Prompt user for user URI.
        prompt = String.Concat("Please enter the User URI for ", userFriendlyName, " in the User@Host format => ");
        _userURI = PromptUser(prompt, _userURIPrompt + _userCount);
        if (!(_userURI.ToLower().StartsWith("sip:") || _userURI.ToLower().StartsWith("tel:")))
          _userURI = "sip:" + _userURI;

        // Increment the last user number.
        _userCount++;

        // Initialize and register the endpoint, using the credentials of the user that the application represents.
        // NOTE: the _userURI should always use the sip:user@host format.
        userEndpointSettings = new UserEndpointSettings(_userURI, _LyncServerFQDN);

        if (!_useSuppliedCredentials)
        {
          _credential = new System.Net.NetworkCredential(_userName, _userPassword, _userDomain);
          userEndpointSettings.Credential = _credential;
        }
        else
        {
          userEndpointSettings.Credential = System.Net.CredentialCache.DefaultNetworkCredentials;
        }
      }
      catch (InvalidOperationException iOpEx)
      {
        // Invalid Operation Exception should only be thrown on poorly-entered input.
        Console.WriteLine("Invalid Operation Exception: " + iOpEx.ToString());
      }

      return userEndpointSettings;
    }

    // Returns the remote user URI.
    public String GetRemoteUserURI()
    {
      String str = "";
      try
      {
        if (ConfigurationManager.AppSettings[_remoteUserURIPrompt + _userCount] != null)
        {
          _remoteUserURI = ConfigurationManager.AppSettings[_remoteUserURIPrompt + _userCount];
          Console.WriteLine("\nUsing {0} as remote user", _remoteUserURI);
          return _remoteUserURI;
        }
        else
        {
          // Prompt user for remote user URI.
          _remoteUserURI = UCMASampleHelper.PromptUser("Enter the URI for the remote user logged onto Communicator, in the form sip:User@Host format or tel:+1XXXYYYZZZZ => ", "RemoteUserURI");
          return str;
        }
      }
      catch (InvalidOperationException iOpEx)
      {
        // Invalid Operation Exception should only be thrown on poorly-entered input.
        Console.WriteLine("Invalid Operation Exception: " + iOpEx.ToString());
        return str;
      }
    }

    /// <summary>
    /// If the 'key' is not found in App.Config, prompt the user in the console, using the specified prompt text.
    /// </summary>
    /// <param name="promptText">The text to be displayed in the console if ‘key’ is not found in App.Config.</param>
    /// <param name="key">Searches for this key in App.Config and returns a value if found. Pass null to always display a message that requests user input.</param>
    /// <returns>String value either from App.Config or user input.</returns>
    public static string PromptUser(string promptText, string key)
    {
      String value;
      if (String.IsNullOrEmpty(key) || ConfigurationManager.AppSettings[key] == null)
      {
        Console.WriteLine(string.Empty);
        Console.Write(promptText);
        value = Console.ReadLine();
      }
      else
      {
        value = ConfigurationManager.AppSettings[key];
        Console.WriteLine("Using keypair {0} - {1} from AppSettings...", key, value);
      }

      return value;
    }
    
    // This method creates an endpoint, using the specified UserEndpointSettings object.
    // This method returns a UserEndpoint object so that you can register endpoint-specific event handlers. 
    // If you do not want to get endpoint-specific event information at the time the endpoint is established, you can 
    // call the CreateEstablishedUserEndpoint method directly. Otherwise, you can call the ReadUserSettings,
    // CreateUserEndpoint, and EstablishUserEndpoint methods, in that order.
    public UserEndpoint CreateUserEndpoint(UserEndpointSettings userEndpointSettings)
    {
      // Reuse the platform instance so that all endpoints share the same platform.
      if (_collabPlatform == null)
      {
        // Initialize and start the platform.
        ClientPlatformSettings clientPlatformSettings = new ClientPlatformSettings(_applicationName, _transportType);
        _collabPlatform = new CollaborationPlatform(clientPlatformSettings);
      }

      _userEndpoint = new UserEndpoint(_collabPlatform, userEndpointSettings);
      return _userEndpoint;
    }

    // This method establishes a previously created UserEndpoint.
    // This method returns an established UserEndpoint object. If you do not want to monitor LocalOwnerPresence, you might 
    // want to call the CreateEstablishedUserEndpoint method directly. Otherwise, you can call the ReadUserSettings,
    // CreateUserEndpoint, and EstablishUserEndpoint methods, in that order.
    public bool EstablishUserEndpoint(UserEndpoint userEndpoint)
    {
      // Start the platform, if not already started.
      if (_isPlatformStarted == false)
      {
        userEndpoint.Platform.BeginStartup(EndPlatformStartup, userEndpoint.Platform);

        // Wait for platform startup to be completed.
        _platformStartupCompleted.WaitOne();
        Console.WriteLine("\nPlatform started...");
        _isPlatformStarted = true;
      }
      // Establish the user endpoint.
      userEndpoint.BeginEstablish(EndEndpointEstablish, userEndpoint);

      // Wait until the endpoint is established.
      _endpointInitCompletedEvent.WaitOne();
      Console.WriteLine("\nEndpoint established...");
      return true;
    }

    // This method creates an established UserEndpoint.
    // This method returns an established UserEndpoint object. If you do not want to monitor LocalOwnerPresence, you might 
    // want to call this CreateEstablishedUserEndpoint method directly. Otherwise, you can call the ReadUserSettings,
    // CreateUserEndpoint, and EstablishUserEndpoint methods, in that order.
    public UserEndpoint CreateEstablishedUserEndpoint(string endpointFriendlyName)
    {
      UserEndpointSettings userEndpointSettings;
      UserEndpoint userEndpoint = null;
      try
      {
        // Read user settings.
        userEndpointSettings = ReadUserSettings(endpointFriendlyName);

        // Create User Endpoint.
        userEndpoint = CreateUserEndpoint(userEndpointSettings);

        // Establish the user endpoint.
        EstablishUserEndpoint(userEndpoint);
      }
      catch (InvalidOperationException iOpEx)
      {
        // Invalid Operation Exception should be thrown only for poorly-entered input.
        Console.WriteLine("Invalid Operation Exception: " + iOpEx.ToString());
      }

      return userEndpoint;
    }
    


    private void EndPlatformStartup(IAsyncResult ar)
    {
      CollaborationPlatform collabPlatform = ar.AsyncState as CollaborationPlatform;
      try
      {
        // The platform should now be started.
        collabPlatform.EndStartup(ar);
        // Note that all the re-thrown exceptions will crash the application. This is intentional.
        // Ideal exception handling will report the error and force the application to shut down in an orderly manner. 
        // In production code, consider using an IAsyncResult implementation to report the error
        // instead of throwing. Alternatively, put the implementation in this try block.
      }
      catch (OperationFailureException opFailEx)
      {
        // OperationFailureException is thrown when the platform cannot establish, here, usually due to invalid data.
        Console.WriteLine(opFailEx.Message);
        throw;
      }
      catch (ConnectionFailureException connFailEx)
      {
        // ConnectionFailureException is thrown when the platform cannot connect.
        // ClientPlatforms will not throw this exception during startup.
        Console.WriteLine(connFailEx.Message);
        throw;
      }
      catch (RealTimeException realTimeEx)
      {
        // RealTimeException may be thrown as a result of any UCMA operation.
        Console.WriteLine(realTimeEx.Message);
        throw;
      }
      finally
      {
        // Synchronize threads.
        _platformStartupCompleted.Set();
      }

    }

    private void EndEndpointEstablish(IAsyncResult ar)
    {
      LocalEndpoint currentEndpoint = ar.AsyncState as LocalEndpoint;
      try
      {
        currentEndpoint.EndEstablish(ar);
      }
      catch (AuthenticationException authEx)
      {
        // AuthenticationException is thrown when the credentials are not valid.
        Console.WriteLine(authEx.Message);
        throw;
      }
      catch (ConnectionFailureException connFailEx)
      {
        // ConnectionFailureException is thrown when the endpoint cannot connect to the server, or the credentials are invalid.
        Console.WriteLine(connFailEx.Message);
        throw;
      }
      catch (InvalidOperationException iOpEx)
      {
        // InvalidOperationException is thrown when the endpoint is not in a valid connection state. 
        // To connect, the platform must be started and the endpoint must be in the Idle state.
        Console.WriteLine(iOpEx.Message);
        throw;
      }
      finally
      {
        // Synchronize threads.
        _endpointInitCompletedEvent.Set();
      }
    }

    internal void ShutdownPlatform()
    {
      if (_collabPlatform != null)
      {
        _collabPlatform.BeginShutdown(EndPlatformShutdown, _collabPlatform);
      }


      // Synchronize threads.
      _platformShutdownCompletedEvent.WaitOne();
    }

    private void EndPlatformShutdown(IAsyncResult ar)
    {
      CollaborationPlatform collabPlatform = ar.AsyncState as CollaborationPlatform;

      try
      {
        // Shutdown actions do not throw.
        collabPlatform.EndShutdown(ar);
        Console.WriteLine("The platform is now shut down.");
      }
      finally
      {
        _platformShutdownCompletedEvent.Set();
      }
    }

    /// <summary>
    /// Read the local store for the certificate that is used to create the platform. This process is necessary to establish a connection to the server
    /// </summary>
    /// <param name="friendlyName">The friendly name of the certificate to use.</param>
    /// <returns>The certificate instance.</returns>
    public static X509Certificate2 GetLocalCertificate(string friendlyName)
    {
      X509Store store = new X509Store(StoreLocation.LocalMachine);

      store.Open(OpenFlags.ReadOnly);
      X509Certificate2Collection certificates = store.Certificates;
      store.Close();

      foreach (X509Certificate2 certificate in certificates)
      {
        if (certificate.FriendlyName.Equals(friendlyName, StringComparison.OrdinalIgnoreCase))
        {
          return certificate;
        }
      }
      return null;
    }

    public static void WriteLine(string line)
    {
      Console.WriteLine(line);
    }

    public static void WriteErrorLine(string line)
    {
      Console.ForegroundColor = ConsoleColor.Red;
      Console.WriteLine(line);
      Console.ResetColor();
    }

    public static void WriteException(Exception ex)
    {
      WriteErrorLine(ex.ToString());
    }

    /// <summary>
    /// Prompts the user to press a key, unblocking any waiting calls to the
    /// <code>WaitForSampleFinish</code> method
    /// </summary>
    public static void FinishSample()
    {
      Console.WriteLine("Please hit any key to end the sample.");
      Console.ReadKey();
      _sampleFinished.Set();
    }

    


    public static void WaitForSampleFinish()
    {
      _sampleFinished.WaitOne();
    }
  }
}

Byte Array Conversion Helper Class

The following code is for a small class that has a single method that converts a Byte array into a string that uses the specified encoding. The contextual data that the UCMA 3.0 application receives from the context channel or sends through it must be formatted as a Byte array. The method in this class handles the conversion of Byte arrays to strings.

Converter.cs

using System;

namespace Microsoft.Rtc.Collaboration.Sample.CommonLite
{
  // Encoding types enumeration. 

  public enum EncodingType
  {
    ASCII,
    Unicode,
    UTF7,
    UTF8,
    UTF32
  }
  class Converter
  {
     
    /// <summary> 
    /// Converts a byte array to a string using the specified encoding. 
    /// </summary> 
    /// <param name="byteArray">The byte array to be converted.</param> 
    /// <param name="encodingType">A member of the EncodingType enumeration.</param> 
    /// <returns>string</returns> 
    public static System.String ConvertByteArrayToString(byte[] byteArray, EncodingType encodingType) 
    { 
      System.Text.Encoding encoding = null; 
      if (encodingType == EncodingType.ASCII)
        encoding = new System.Text.ASCIIEncoding();
      else if (encodingType == EncodingType.Unicode)
        encoding = new System.Text.UnicodeEncoding();
      else if (encodingType == EncodingType.UTF7)
        encoding = new System.Text.UTF7Encoding();
      else if (encodingType == EncodingType.UTF8)
        encoding = new System.Text.UTF8Encoding();
      else if (encodingType == EncodingType.UTF32)
        encoding = new System.Text.UTF32Encoding();

      if (!(encoding == null))
      {
        return encoding.GetString(byteArray);
      }
      else return "Bad encoding type.";
    }
  }
}

Lync API Code

This section includes the XAML code that defines the form that is displayed to the Lync 2010 user. The XAML code is also known as the “code-behind” Silverlight code. This section also includes the HTML code for the web page that appears in the Lync 2010 Conversation Window Extension.

The Lync 2010 application is created by using the Lync Silverlight Application template in Microsoft Visual Studio development system. Some auto-generated files that are not revised, such as App.xaml and App.xaml.cs, are not presented here.

XAML Form Code

The following XAML code defines the form that is presented to the Lync 2010 user.

Page.xaml

<UserControl x:Class="ContextCommunication.MainPage"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="500">

    <Grid x:Name="LayoutRoot" Background="White" Width="490">
        <TextBlock Height="23" HorizontalAlignment="Left" Margin="23,15,0,0" Name="textBlock6" Text="Channel status" VerticalAlignment="Top" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="113,12,0,0" Name="channelStatus" Text="Not ready" Foreground="red" VerticalAlignment="Top" Width="66" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="318,0,0,178" Name="PriceBox" VerticalAlignment="Bottom" Width="120" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="319,70,0,0" Name="ModelBox" VerticalAlignment="Top" Width="120" />
        <TextBlock Height="23" HorizontalAlignment="Left" Margin="255,106,0,0" Name="textBlock1" Text="Price" VerticalAlignment="Top" Width="38" />
        <TextBlock Height="23" HorizontalAlignment="Left" Margin="255,76,0,0" Name="textBlock2" Text="Model" VerticalAlignment="Top" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="318,127,0,0" Name="AvailabilityBox" VerticalAlignment="Top" Width="120" />
        <TextBlock Height="23" HorizontalAlignment="Left" Margin="255,0,0,142" Name="textBlock4" Text="Availability" VerticalAlignment="Bottom" />
        <TextBlock Height="23" HorizontalAlignment="Left" Margin="22,48,0,0" Name="textBlock5" Text="Get information about..." VerticalAlignment="Top" Width="131" />
        <Button Content="SendData" Height="23" HorizontalAlignment="Right" Margin="0,137,390,0" Name="SendAdditionalData" Click="SendAdditionalData_Click" VerticalAlignment="Top" Width="75" />
        <RadioButton Content="Camera" Height="16" HorizontalAlignment="Left" Margin="31,64,0,0" Name="radioButton1" Click ="radioButton_Click1" VerticalAlignment="Top" />
        <RadioButton Content="Smartphone" Height="16" HorizontalAlignment="Left" Margin="30,88,0,0" Name="radioButton2" Click ="radioButton_Click2" VerticalAlignment="Top" />
        <RadioButton Content="GPS" Height="16" HorizontalAlignment="Left" Margin="30,111,0,0" Name="radioButton3" Click ="radioButton_Click3" VerticalAlignment="Top" />
        <TextBox Height="106" HorizontalAlignment="Left" Margin="74,182,0,0" Name="LoggerTextBox" VerticalAlignment="Top" Width="410" />
        <TextBlock Height="23" HorizontalAlignment="Left" Margin="30,217,0,0" Name="textBlock3" Text="Logger" VerticalAlignment="Top" />
    </Grid>
</UserControl>

Silverlight Code for the Form

The following C# code is what makes the Silverlight application work. Except for the initialization and logging functionality, most of this code consists of event handlers that respond to various events.

Page.xaml.cs

using System;
using System.Collections.Generic;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using Microsoft.Lync.Model.Extensibility;
using Microsoft.Lync.Model.Conversation;

namespace ContextCommunication
{
  public partial class MainPage : UserControl
  {
    string AppId = "{3271E259-E508-4D39-B044-445855591E79}";
    Conversation _conversation;
    String _itemChosen = "No item chosen";

    public MainPage()
    {
      InitializeComponent();
      Initialize();
    }

    // Get the hosting Conversation object and application data, and then register for event notification.
    private void Initialize()
    {
      String appData;
      try
      {
        _conversation = (Conversation)Microsoft.Lync.Model.LyncClient.GetHostingConversation();
      }
      catch (LyncClientException ex)
      {
        Logger("LyncClientException error: " + ex);
      }

      catch (Exception ex)
      {
        Logger("Other conversation initialization error: " + ex);
      }

      _conversation.ContextDataReceived += OnContextDataReceived;
      _conversation.InitialContextReceived += OnInitialContextReceived;
      _conversation.ContextDataSent += OnContextDataSent;
      
      appData = _conversation.GetApplicationData(_appId);
      Logger("Application data: " + appData);
      
      if (appData.Contains("open"))
      {
        channelStatus.Foreground = new SolidColorBrush(Colors.Green);
        channelStatus.Text = "Ready";
      }
    }
    
    // Display a string in the Logger textbox.
    private void Logger(string text)
    {
      LoggerTextBox.Text += text + "\n";
    }

    // Handler for InitialContextReceived event.
    void OnInitialContextReceived(object sender, InitialContextEventArgs args)
    {
      channelStatus.Foreground = new SolidColorBrush(Colors.Green);
      channelStatus.Text = "Ready";
      Logger("InitialContextReceived event raised. Data received: " + args.ApplicationData);
    }

    // Handler for the ContextDataReceived event on the Conversation object.
    // This handler displays the data that is sent to it from the UCMA application.
    public void OnContextDataReceived(object sender, ContextEventArgs args)
    {
      if ((args != null) && (args.ContextData.Length != 0))
      {
        Logger("OnContextDataReceived:" + args.ContextData);
        string str = args.ContextData;
        string[] substr = str.Split(new char[] { ',' });

        // Populate the three text boxes.
        ModelBox.Text = substr[0];
        PriceBox.Text = substr[1];
        AvailabilityBox.Text = substr[2];
      }
      else
      {
        Logger("OnContextDataReceived called with no data.");
      }
    }
    
    // Handler for the ContextDataSent event on the Conversation object. 
    public void OnContextDataSent(object sender, ContextEventArgs args)
    {
      try
      {
        Logger("OnContextDataSent: AppId = " + args.ApplicationId +
                 " DataType = " + args.ContextDataType +
                 " Data ++oncontextSent++ = " + args.ContextData);
      }
      catch (Exception ex)
      {
        Logger("OnContextDataSent error: " + ex);
      }
    }

    // Handler for the Click event on the SendData button.
    private void SendAdditionalData_Click(object sender, RoutedEventArgs e)
    {
      try
      {
        Logger("Sending additional context: Query item = " + _itemChosen);

        _conversation.BeginSendContextData(AppId, @"plain/text", _itemChosen, SendAdditionalDataCallBack, null);
      }

      catch (Exception ex)
      {
        Logger("SendAdditionalData error: " + ex);
      }

    }

    // Callback for the BeginSendContextData method.
    private void SendAdditionalDataCallBack(IAsyncResult asyncResult)
    {
      if (asyncResult.IsCompleted)
      {
        _conversation.EndSendContextData(asyncResult);

        Logger("Additional context sent successfully.");
      }

      else
      {
        Logger("Could not send additional context : " + asyncResult.AsyncState);
      }
    }

    // Handlers for the Click event on the three option buttons.
    // Each handler sets a global variable with a string that identifies the selected option button.
    private void radioButton_Click1(object sender, RoutedEventArgs e)
    {
      _itemChosen = "Camera";
    }

    private void radioButton_Click2(object sender, RoutedEventArgs e)
    {
      _itemChosen = "Smartphone";
    }

    private void radioButton_Click3(object sender, RoutedEventArgs e)
    {
      _itemChosen = "GPS";
    }
  }

}

HTML Code

The following HTML code is used to generate the web page that the Silverlight application opens. The code includes the IMG block that is used for the logo of the fictitious Contoso Corporation, and the object block that loads the application binary, ContextualCommunication.xap.

sample.html

<html>
<head>
</head>
<body>
<IMG src="Logo.jpg">
<object width="500" height="300" 
  data="data:application/x-silverlight-2," 
  type="application/x-silverlight-2" >
  <param name="source" value="ContextualCommunication.xap"/>
</object>
</body>
</html>

Part 6

Using UCMA 3.0 and Lync 2010 for Contextual Communication: Summary (Part 6 of 6)

Additional Resources

For more information, see the following resources:

About the Authors

Mark Parker and John Clarkson are programming writers with the Microsoft Lync product team.