Using Web Services Helpers to Access Events in Groove 2007

Summary: Reduce code complexity when accessing Groove events by creating or using custom wrappers around the Microsoft Office Groove 2007 Web services API. (12 printed pages)

Bob Novas, Microsoft Corporation

August 2007

Applies to: Microsoft Office Groove 2007, Microsoft Office Groove 2007 Server, Groove Web Services Helpers, Microsoft Office Groove 2007 SDK

Contents:

  • Overview of Events

  • Establishing a Simple Subscription and Processing Events

  • Establishing an Aggregated Subscription and Processing Events

  • Implementing IGrooveWebServicesAdvancedEventCallback

  • Maintaining a Long-Running Event Subscription

  • Conclusion

  • Additional Resources

Using the Web Services API

The Microsoft Office Groove 2007 Web Services API provides rich access to the Microsoft Office Groove 2007 environment and to the data stored in Groove workspaces, but if you use this API directly, you must enter multiple lines of code to set up the Web services environment for each operation call. If you are creating a large, complex solution, you can reduce the size and complexity of your code by creating a set of wrappers that provide a higher level of abstraction.

The Groove Web Services V12 Helpers project provides such a set of wrappers. You can download this project CodePlex, Microsoft's open source project hosting web site.

Overview of Events

A Groove instance generates events internally in response to actions of the user or upon receipt of deltas from other members of the user’s workspaces or personal contacts of the user. An instance enqueues these events internally for delivery to the Web services event consumers depending on the subscriptions set up by a Web services consumer to various classes of events or event sources. The Groove Helpers periodically read this event data from the Events Web service and raise .NET Framework events to notify the Web services consumer that events occurred asynchronously.

A Groove instance delivers events to a consumer based on event subscriptions. There are two kinds of subscriptions: simple subscriptions and aggregated subscriptions. Simple subscriptions register for events from a single Groove event source, for example from a specific Groove Files tool. Simple subscriptions are easy to create and remove using the Groove Helpers. For example, the following code adds and removes a subscription for the OnSpaceAdded event of an Identity object:

identity.OnSpaceAddedEvent += new Identity.OnSpaceAddedDelegate(identity_OnSpaceAddedEvent);
identity.OnSpaceAddedEvent -= new Identity.OnSpaceAddedDelegate(identity_OnSpaceAddedEvent);

Aggregated subscriptions register a class of event source (such as, forms tool events) from a more encompassing Groove object like a specific workspace, or simply all objects of the given class. For example, you can create an aggregated subscription that registers all forms tools in all workspaces for an identity on a Groove instance, or all files tools in a workspace. Aggregated subscriptions are useful for aggregating events from a class of tools because simple subscriptions already aggregate events from objects other than tools. A simple subscription to Space events aggregates events from all spaces. Groove is much more efficient handling a single aggregated subscription to a class of tools than handling a large number of subscriptions to the individual tools. Also, with aggregated subscriptions, Groove manages the process of adding and deleting new instances of the subscribed class of objects. For example, newly added tools in the class participate in existing aggregated subscriptions automatically. The section Establishing an Aggregated Subscription and Processing Events explains how to establish an aggregated subscription.

The Groove Helpers run a loop that polls Groove for events and delivers the events to the application by raising a .NET Framework event. The loop starts running when the first subscription is established and is torn down when the last subscription is removed. It is important to tear down all subscriptions before exiting an application, or this loop, which runs on a separate, foreground thread, continues to run and prevents the application from exiting.

Event subscriptions can be persistent. This means that you can establish subscriptions that continue to run during Groove shutdown and startup. Also, you can remove a subscription from the Groove Helpers and application program but remains established within Groove. This allows a persistent subscription to survive application shutdown and restart. For more information about establishing and maintaining such long-running persistent subscriptions, see Maintaining a Long-Running Event Subscription.

Establishing a Simple Subscription and Processing Events

For the first event example, we’ll look at receiving Contact events. The code in Example 1, Example 1. Contact Event Handlers shows the event handlers that are registered to be called by the Groove Helpers when a Contact event occurs. The code in Example 2, Example 2. Registering and Unregistering for Events shows how to create a context that refers to the local Groove instance, open an Account object and register handlers for Contact events for that account. After the handlers are registered, the Groove Helpers deliver events to the event handlers until the handlers are unregistered.

You need to understand the following points:

  • The Groove Helpers run a procedure on a separate thread that periodically polls Groove and reads Groove events using the GrooveEvents Web service. This procedure reads all the events queued to a subscription from Groove, delivers the Groove events by executing events in the .NET Framework, and then deletes the Groove events from the subscription queue. This ensures that Groove events are not lost, but can result in delivering an event more than once if the application stops running before an event is deleted from the Groove event queue. When the application is restarted, it may re-read events that it had previously executed as events but did not yet delete.

  • The Groove Helpers call event handlers on an arbitrary thread, not the user interface (UI) thread. If the event handlers were to operate on UI components in a Form, for example, they would have to delegate to the UI thread using the Form.Invoke() method.

  • You must disconnect event handlers before the consumer of the events terminates. Be sure to try to disconnect event handler methods like the Dispose method or the ApplicationExit method to ensure that the handlers are always disconnected before terminating.

  • Simple subscriptions work well if you are interested in events from a specific Groove object, for example Contact events from a Groove account object or if you are interested in events from a specific Groove tool. However, if you need to receive events from an entire class of Groove objects (such as, events from a kind of tool in a number of workspaces), you should consider aggregated subscriptions, which are described in the next section.

Example 1. Contact Event Handlers

    #region Contact Event Handlers
    private void m_Account_OnContactAddedEvent(Contact i_Contact)
    {
         System.Diagnostics.Debug.WriteLine(String.Format(
             "Added: {0}({1}) - {2}", 
             i_Contact.Name, i_Contact.URI, 
             i_Contact.VCard.EmailAddresses[0]));
    }

    private void m_Account_OnContactUpdatedEvent(Contact i_Contact)
    {
        System.Diagnostics.Debug.WriteLine(
        String.Format("Updated: {0}({1}) - {2}", 
        i_Contact.Name,
        i_Contact.URI,
        i_Contact.VCard.EmailAddresses[0]));
    }

    private void m_Account_OnContactRemovedEvent(string i_ContactURI)
    {
        System.Diagnostics.Debug.WriteLine(String.Format(
            "Removed: {0}", i_ContactURI));
    }
    #endregion

Example 2. Registering and Unregistering for Events

    class MemberMonitor : IDisposable
    {
        // use default context - https://localhost:9080
        private Context m_Context = new Context();
        private Account m_Account;
        private bool m_Disposed = false;

        internal void Initialize()
        {
            try
            {
                // use the first account (or ask the user to select
                // one)
                Account[] accounts = Account.GetAccounts(m_Context);
                if (accounts.Length > 1)
                    Debug.WriteLine("Using first account");
                m_Account = accounts[0];

                // register for contact events...
                m_Account.OnContactAddedEvent 
                   += new Account.OnContactAddedDelegate(
                   m_Account_OnContactAddedEvent);
                m_Account.OnContactUpdatedEvent 
                    += new Account.OnContactUpdatedDelegate(
                    m_Account_OnContactUpdatedEvent);
                m_Account.OnContactRemovedEvent 
                    += new Account.OnContactRemovedDelegate(
                    m_Account_OnContactRemovedEvent);
            }
            catch (Exception ex)
            {
                // It's possible that groove.exe wasn't running
                Debug.WriteLine(String.Format(
                    "Exception - {0}\n{1}", ex.Message, 
                    ex.StackTrace));
                return;
            }
        }

        #region IDisposable Members
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected void Dispose(bool i_Disposing)
        {
            if (!this.m_Disposed)
            {
                // If disposing equals true, dispose all managed
                // and unmanaged resources.
                if (i_Disposing)
                {
                    // Dispose managed resources.
                }

                if (m_Account.OnContactAddedEvent != null)
                    m_Account.OnContactAddedEvent 
                        -= new Account.OnContactAddedDelegate(
                        m_Account_OnContactAddedEvent);
                if (m_Account.OnContactAddedEvent != null)
                    m_Account.OnContactUpdatedEvent 
                        -= new Account.OnContactUpdatedDelegate(
                        m_Account_OnContactUpdatedEvent);
                if (m_Account.OnContactRemovedEvent != null)
                    m_Account.OnContactRemovedEvent 
                        -= new Account.OnContactRemovedDelegate(
                        m_Account_OnContactRemovedEvent);

                // Note disposing has been done.
                m_Disposed = true;
            }
        }
        #endregion

Establishing an Aggregated Subscription and Processing Events

As mentioned previously, an aggregated Subscription is useful for processing events from an entire class of Groove objects. Classes are defined in the GrooveWebServicesV12Helpers.Shared namespace, as shown below.

Table 1. Event Classes

Event Class Description

GROOVE_WEB_SERVICES_SPACE_EVENT_CLASS

This class provides events when workspaces are added, updated, or deleted.

GROOVE_WEB_SERVICES_TOOL_EVENT_CLASS

This class provides events when tools are added, updated, or deleted.

GROOVE_WEB_SERVICES_CONTACT_EVENT_CLASS

This class provides events when tools are added, updated, or deleted.

GROOVE_WEB_SERVICES_FILES_EVENT_CLASS

This class provides events when files or folders are updated, removed, moved, or downloaded.

GROOVE_WEB_SERVICES_DISCUSSION_EVENT_CLASS

This class provides events when discussion items in Groove 3.1 Discussion tools are updated or removed.

GROOVE_WEB_SERVICES_CALENDAR_EVENT_CLASS

This class provides events when calendar entries are updated or removed.

GROOVE_WEB_SERVICES_MEMBER_EVENT_CLASS

This class provides events when member items are updated or removed. Includes events indicating that the awareness has changed such as when members enter or exit a workspace or enter or exit a tool.

GROOVE_WEB_SERVICES_FORMS_EVENT_CLASS

This class provides events when forms in Groove 3.1 Forms tools are updated or removed.

GROOVE_WEB_SERVICES_FORMS2_EVENT_CLASS

This class provides events when forms are updated or removed.

GROOVE_WEB_SERVICES_CONTACT_DIRECTORY_EVENT_CLASS

This class provides events when contact directory items are update or removed. Includes events which provide events when members enter or exit a workspace or enter or exit a tool.

GROOVE_WEB_SERVICES_MESSAGE_EVENT_CLASS

This class provides events when messages are opened, removed, or sent.

Example 3, Example 3. Aggregated Tool Event Handler, shows an example of registering aggregated tool event handlers. The Groove Helpers provide additional support for registering tool event handlers, in the form of two sets of AddAggregatedToolEventHandler methods; one on the Identity object to register for all tools in an identity, and one on the Space object, to register form events on all tools in a workspace. The code shown in Example 3 uses the Identity form of the AddAggregatedToolEventHandler method to register for Forms tool events for both Version 4 (Groove 3.1) and Version 5 (Groove 2007) Forms Tools. The code also registers an ApplicationExit handler to unregister for the Tool events.

The code also extends the System.Windows.Form object to implement the IGrooveWebServicesAdvancedEventCallback method, which inherits events from the IGrooveWebServicesEventCallback method. Here we explain just the IGrooveWebServicesCallback implementation that processes events; in the next section we discuss the Advanced callbacks.

To process events from the aggregated subscriptions, the Form implements (at least) IGrooveWebServicesCallback. This code is shown in Example 4, Example 4. IGrooveWebServicesEventCallback.

Example 3. Aggregated Tool Event Handler

public partial class Form1 : System.Windows.Forms.Form,
GrooveWebServicesV12Helpers.Shared.IGrooveWebServicesAdvancedEventCallback
    {
        Context m_Context = new Context();
        Account m_Account;
        Identity m_Identity;
        string m_AggregatedFormsSubscriptionID;
        string m_AggregatedForms2SubscriptionID;

        const string EVENT_CALLBACK_URI = 
            @"dpp://localhost/TestAggregatedEventsApp_01";

        public Form1()
        {
            String Query = String.Empty;
            int TimeToLiveHours = 8;

            InitializeComponent();

            Account[] accounts = Account.GetAccounts(m_Context);
            m_Account = accounts[0];

            Identity[] identities = m_Account.Identities;
            m_Identity = identities[0];

            // 3.1 forms tool (V4)
            m_AggregatedFormsSubscriptionID = 
                m_Identity.AddAggregatedToolEventListener(
                    Shared.GROOVE_WEB_SERVICES_FORMS_EVENT_CLASS,
                    EVENT_CALLBACK_URI,
                    Query,
                    TimeToLiveHours,
                    this);

            // 2007 forms tool (V5)
            m_AggregatedForms2SubscriptionID = 
                m_Identity.AddAggregatedToolEventListener(
                    Shared.GROOVE_WEB_SERVICES_FORMS2_EVENT_CLASS,
                    EVENT_CALLBACK_URI,
                    Query,
                    TimeToLiveHours,
                    this);

            System.Windows.Forms.Application.ApplicationExit 
                += new EventHandler(Application_ApplicationExit);
        }

        void Application_ApplicationExit(object sender, EventArgs e)
        {
            m_Identity.RemoveAggregatedToolEventListener(
                m_AggregatedFormsSubscriptionID, this);
            m_Identity.RemoveAggregatedToolEventListener(
                m_AggregatedForms2SubscriptionID, this);
        }

Example 4, Example 4. IGrooveWebServicesEventCallback, shows an implementation of IGrooveWebServicesEventCallback that processes events from Forms tools. ProcessEvent picks out the Add events and calls ProcessFormsRecordAddEvent, which converts the event data to a System.Data.DataSet and does further processing with the data set.

Example 4. IGrooveWebServicesEventCallback

    #region IGrooveWebServicesEventCallback Members
    public void ProcessEvent(
    GrooveWebServicesV12Helpers.GrooveEventsWebService.Event i_Event)
    {
         switch (i_Event.EventType)
         {
            case Shared.GROOVE_FORMS_RECORD_ADD_EVENT_TYPE:
            case Shared.GROOVE_FORMS2_RECORD_ADD_EVENT_TYPE:
            ProcessFormsRecordAddEvent(i_Event);
            break;

            default:
            System.Diagnostics.Debug.WriteLine(
                  String.Format("ProcessEvent: EventType={0}", 
                  i_Event.EventType));
            break;
        }
    }

 private void ProcessFormsRecordAddEvent(
 GrooveWebServicesV12Helpers.GrooveEventsWebService.Event i_Event)
 {

     DataSet ds = null;
     if (i_Event.EventData is GrooveFormsRecordAddEventData)
     {
         GrooveFormsRecordAddEventData AddEvent 
             = i_Event.EventData as GrooveFormsRecordAddEventData;
         RecordDataSet rds = new RecordDataSet(AddEvent.RecordDataSet);
         ds = rds.ToDataSet();
     }
     else if (i_Event.EventData is GrooveForms2RecordAddEventData)
     {
         GrooveForms2RecordAddEventData AddEvent2 
             = i_Event.EventData as GrooveForms2RecordAddEventData;
         GrooveWebServicesV12Helpers.Forms2.RecordDataSet rds2 
             = new GrooveWebServicesV12Helpers.Forms2.RecordDataSet(
             AddEvent2.RecordDataSet);
         ds = rds2.ToDataSet();
     }
     …
 }

Implementing IGrooveWebServicesAdvancedEventCallback

As mentioned before, the Web Helpers EventManagerThreadProc runs a loop that polls Groove for events. The advanced event callbacks give the user some information about that loop. The advanced callback methods are:

OnEventProcessingCompleted(Subscriptioni_Subscription, longi_SequenceNumber)—called after all events are delivered to the application for a polling cycle by calls to ProcessEvent, but before the events are deleted from the Groove internal event queue.

OnEventsDeleted(Subscriptioni_Subscription, longi_SequenceNumber) – called after the events delivered in multiple calls to ProcessEvent are deleted from the Groove internal event queue.

OnSubscriptionUpdated(Subscriptioni_Subscription) – called when the loop updates a subscription, which it does roughly halfway through the time-to-live specified for the subscription, extending the subscription by another time-to-live.

You can use OnEventProcessingCompleted update the UI, having made changes to data in multiple calls to ProcessEvent, or to commit changes to an external data store. OnEventsDeleted is also useful in similar circumstances, depending on your failure recovery strategy. (Committing in OnEventProcessingCompleted is subject to delivery of redundant events; whereas, committing on OnEventsDeleted is subject to loss of events.)

You can use OnSubscriptionUpdated to maintain a persistent store of a long running subscription’s expiration time, see Maintaining a Long-Running Event Subscription.

Maintaining a Long-Running Event Subscription

If the application must process every event (even if an event occurs when the application is not running), then it must maintain a long-running event subscription. The application must make subscriptions persistent and re-use the same subscription on start up. Also, you should establish the subscriptions with a time-to-live value that is greater than the maximum expected duration of the shut down. If these conditions are met, Groove maintains events and delivers them to the application. If the duration of the shutdown exceeds the value of a subscription’s time-to-live, then the application may lose events. In this case, the application must establish a new subscription to continue to receive events. It is very important to keep track of long running subscriptions and ensure that they are deleted if they are no longer used. The Groove Helpers provide the following:

  • Establish a subscription with a given subscription ID to allow for re-establishing an existing subscription.

  • Remove a subscription from the EventManagerThreadProc yet leave the subscription active in Groove.

  • The EventManagerThreadProc updates a subscription to its full time-to-live whenever the time-to-live approaches its half-life. (The EventManagerThreadProc checks a subscription’s time-to-live every poll cycle.) The Groove Helpers call the IGrooveWebServicesAdvancedEventCallbackOnSubscriptionUpdated method when they update a subscription.

  • For a Groove 2007 instance, when a subscription is deleted or expires, Groove deletes all events from the event queue. If the application attempts to use an expired subscription, the Web service throws an exception and the application can take that to mean that you need to establish a new subscription.

Conclusion

You can reduce the amount of time it takes to develop a Groove Web services application by using a set of wrappers. The wrappers reduce the number of lines of code needed to call the Groove Web services operations.

Additional Resources

For more information, see the following resources:

Using Web Services Helpers to Access Data in Groove 2007