How to save IM conversations using Office Communicator SDK

Archiving the contents of an IM conversation using the Office Communicator SDK is something that I get asked about pretty routinely.  For example, let’s say that you want to launch an IM call from your application using the OC SDK and archive that conversation to be retrieved later (displaying all the application specific conversations in your application or on a SharePoint site, etc.).

The OC SDK provides the IMessengerConversationWndAdvanced.History property to access the body of an IM conversation.  For example, the following console application launches an IM call and then writes out the IM history to the console:

    1:  namespace History
    2:  {
    3:      class Program
    4:      {
    5:          private static Messenger _messenger;
    6:          private static IMessengerAdvanced _messengerAdv;
    7:   
    8:          private static long _myConvHWND = 0;
    9:          private static IMessengerConversationWndAdvanced _myConv;
   10:   
   11:          static void Main(string[] args)
   12:          {
   13:              _messenger = new Messenger();
   14:              _messengerAdv = (IMessengerAdvanced)_messenger;
   15:   
   16:              _messenger.OnIMWindowCreated +=
   17:                  new DMessengerEvents_OnIMWindowCreatedEventHandler(
   18:                  _messenger_OnIMWindowCreated);
   19:              _messenger.OnIMWindowDestroyed += 
   20:                  new DMessengerEvents_OnIMWindowDestroyedEventHandler(
   21:                  _messenger_OnIMWindowDestroyed);
   22:   
   23:              object[] sipUris = new object[] { "kf@fabrikam.com", "cb@fabrikam.com"  };
   24:   
   25:              object obj = _messengerAdv.StartConversation(
   26:                  // The call media.            
   27:                  CONVERSATION_TYPE.CONVERSATION_TYPE_IM,
   28:                  // The participants.
   29:                  sipUris,
   30:                  // Not supported.
   31:                  null,
   32:                  // The conversation window title as as string.
   33:                  "Account: Johnson (ID:12345)",
   34:                  // Not supported.  Pass "1".
   35:                  "1",
   36:                  // Not supported.
   37:                  null);
   38:   
   39:              _myConvHWND = long.Parse(obj.ToString());
   40:   
   41:              Console.WriteLine(
   42:                  "Press the Enter key to see IM conversation History.");
   43:              Console.ReadLine();
   44:   
   45:              if (_myConv != null)
   46:              {
   47:                  try
   48:                  {
   49:                      Console.WriteLine(_myConv.History);
   50:                  }
   51:                  catch (COMException ce)
   52:                  {
   53:                      Console.WriteLine(
   54:                          "COM Exception " + ce.ErrorCode.ToString());
   55:                  }
   56:              }
   57:   
   58:              Console.WriteLine("Press the Enter key to exit the application.");
   59:              Console.ReadLine();
   60:   
   61:              _messenger.OnIMWindowCreated -=
   62:                  new DMessengerEvents_OnIMWindowCreatedEventHandler(
   63:                  _messenger_OnIMWindowCreated);
   64:              _messenger.OnIMWindowDestroyed -=
   65:                  new DMessengerEvents_OnIMWindowDestroyedEventHandler(
   66:                  _messenger_OnIMWindowDestroyed);
   67:   
   68:              Marshal.ReleaseComObject(_messenger);
   69:              _messenger = null;
   70:              Marshal.ReleaseComObject(_messengerAdv);
   71:              _messengerAdv = null;
   72:   
   73:              if (_myConv != null)
   74:              {
   75:                  Marshal.ReleaseComObject(_myConv);
   76:                  _myConv = null;
   77:              }
   78:          }

In the code above, I’m registering for the OnIMWindowCreated and OnIMWindowDestroyed events to get and release my reference to the conversation window (_myConv) so I can access the History property.  The code below shows how I manage the _myConv reference:

    1:          static void _messenger_OnIMWindowCreated(object pIMWindow)
    2:          {
    3:              IMessengerConversationWndAdvanced newConv =
    4:                  (IMessengerConversationWndAdvanced)pIMWindow;
    5:   
    6:              if (newConv.HWND == _myConvHWND)
    7:              {
    8:                  newConv.SendText("This is a ongoing conversation " +
    9:                      "about the Account: Johnson (ID:12345) " +
   10:                      "and will be saved on the account site.");
   11:                  
   12:                  _myConv = newConv;
   13:              }
   14:          }
   15:   
   16:          static void _messenger_OnIMWindowDestroyed(object pIMWindow)
   17:          {   
   18:              if (object.ReferenceEquals((object)_myConv, pIMWindow))
   19:              {
   20:                  Marshal.ReleaseComObject(_myConv);
   21:                  _myConv = null;
   22:              }
   23:          }

In OnIMWindowCreated, I set _myConv using the pIMWindow object passed as an event argument after confirming it’s the conversation I started in code (by comparing the HWNDs).  When OnIMWindowDestroyed fires, I check to make sure the window being destroyed is mine by using object.ReferenceEquals and release the reference.  Note that I can’t use the HWND property in OnIMWindowDestroyed since conversation window no longer exists and _myConv is no longer a valid reference.

While this code works, it’s not an ideal solution.  I can’t rely on the user to do something (click a button, etc.) in my app before the conversation is closed.  I’d rather just write the IM conversation history out when the conversation is completed.  This leads a lot of developers to try the following:

    1:          static void _messenger_OnIMWindowDestroyed(object pIMWindow)
    2:          {   
    3:              if (object.ReferenceEquals((object)_myConv, pIMWindow))
    4:              {
    5:                  Console.WriteLine(_myConv.History);
    6:   
    7:                  Marshal.ReleaseComObject(_myConv);
    8:                  _myConv = null;
    9:              }
   10:          }

But, the call to IMessengerConversationWndAdvanced.History throws an exception since _myConv is no longer a valid reference when OnIMWindowDestroyed fires.

Another solution I’ve seen is polling History on another thread through the life of the conversation and writing out the last value when the conversation window is destroyed.  While this works, it’s less than ideal due to the resource you consume.

So what does provide a good working scenario?  Creating a UCMA 2.0 IM robot that archives conversations and inviting that bot to all the conversations you want archived is a great solution (and a great future blog post).

Details on how to start conversations using the Office Communicator SDK can be found in the How to make calls via Office Communicator SDK and How to send IM text using Office Communicator SDK posts on this blog.

If you’d like to try this code out for yourself, you’ll need to download the Office Communicator 2007 SDK and setup a UC development environment.

More details, tips and tricks on UC development can be found in the Programming for Unified Communications book.

Thanks,

Chris