InfiniTec - Henning Krauses Blog

Don't adjust your mind - it's reality that is malfunctioning

Exchange Eventsink Foundation

Although Eventsinks are being removed from the next version of Exchange (see Exchange Developer Roadmap - WebDAV and StoreEvents gone), they are widely used and sometimes the only option. Especially when working with tasks, contacts and appointments.

But writing Eventsinks has always been a pain because all this interop stuff, COM+ registration issues and more. Attached to this post you can find a small framework which simplifies writing event sinks (both synchronous and asynchronous ones).

The InfiniTec.Exchange.Eventing assembly contains several base classes from which you inherit your Eventsink. The only thing you have to do is to overwrite some methods (OnItemChanged, OnItemCreated, OnItemCreated) and you get relevant information about the event which happened in a compact object (EventInfo class). Below is a class diagram of the InfiniTec.Exchange.Eventing namespace:

image

Depending whether you want to create an asynchronous or synchronous Eventsink, you derive your sink from either SynchronousEventSink or AsynchronousEventSink. In you derived class, you simply override the notifications you want to catch. One thing all Eventsinks have in common is the registration process – Implemented by the OnRegisteringEvent. By returning false from this method you can prevent the registration on a certain url.

The synchronous Eventsink is called two times for each event: The first execution is called the Begin Phase; during this stage, the item is writable and you can even check for changes by loading the original item (via EventInfo.OpenItemLocally()). The seconds stage is called Commit Phase; the item is read-only now. If you need to share information about a particular event between these to stages, set the EventInfo.UserState property. The object graph is serialized using the BinarySerializer during the begin phase, so each instance used here must be serializable. This is a feature which was not possible with the interop files generated with tlbimp, because that tool did not generate the proper interop code. Since I’ve incorporated the complete interop code in this assembly, I fixed the signature of the affected interface.

Correctly deciphering the flags passed to the Evensink was an art of it’s own, so I’ve cleaned up those flags enumerations as well and routed them different methods (OnItemCreated with its CreationMode parameter and OnItemDeleted with its DeletionMode parameter).

At last, you don’t need the the regevent.vbs script any longer because I’ve included an EventSinkInstaller which registers your EventSink on a certain folder.

Here is a sample event sink:

   1: using System;
   2: using System.Runtime.InteropServices;
   3: using ADODB;
   4: using InfiniTec.Exchange.Eventing;
   5:  
   6: namespace TestSink
   7: {
   8:     [ComVisible(true)]
   9:     [Guid("FD0D03A3-9FD2-432b-B331-E7C4D412827F")]
  10:     [ProgId("TestSink.TestSink")]
  11:     public class TestSink: SynchronousEventSink
  12:     {
  13:         protected override void OnItemCreating(EventInfo info, CreationMode creationMode)
  14:         {
  15:             // Do something of interest here - you have read/write  access to the item
  16:             Record item = info.Item;
  17:  
  18:             // you can save an object here (it must be serializable) and reuse it during the OnItemCreated method
  19:             info.UserState = "test";
  20:             
  21:  
  22:             base.OnItemCreating(info, creationMode);
  23:         }
  24:  
  25:         protected override void OnItemCreated(EventInfo info, CreationMode creationMode)
  26:         {
  27:             base.OnItemCreated(info, creationMode);
  28:  
  29:             // Do something of interest here - the item is readonly now.
  30:             
  31:             // will be "test"
  32:             string s = (string) info.UserState;
  33:         }
  34:  
  35:  
  36:         protected override void OnItemDeleted(EventInfo info, DeletionMode deletionMode)
  37:         {
  38:             base.OnItemDeleted(info, deletionMode);
  39:         }
  40:  
  41:         protected override void OnItemUpdated(EventInfo info)
  42:         {
  43:             base.OnItemUpdated(info);
  44:         }
  45:  
  46:         protected override void OnErrorOccured(EventInfo eventInfo, Exception ex)
  47:         {
  48:             base.OnErrorOccured(eventInfo, ex);
  49:         }
  50:  
  51:         protected override void OnInitialize()
  52:         {
  53:             base.OnInitialize();
  54:         }
  55:  
  56:         protected override bool OnRegisteringEvent(EventInfo info)
  57:         {
  58:             return base.OnRegisteringEvent(info);
  59:         }
  60:  
  61:         protected override void OnItemCreationAborted(EventInfo info, CreationMode creationMode)
  62:         {
  63:             base.OnItemCreationAborted(info, creationMode);
  64:         }
  65:  
  66:         protected override void OnItemDeleting(EventInfo info, DeletionMode deletionMode)
  67:         {
  68:             base.OnItemDeleting(info, deletionMode);
  69:         }
  70:  
  71:         protected override void OnItemDeletionAborted(EventInfo info, DeletionMode deletionMode)
  72:         {
  73:             base.OnItemDeletionAborted(info, deletionMode);
  74:         }
  75:  
  76:         protected override void OnItemSilentlySaving(EventInfo info)
  77:         {
  78:             base.OnItemSilentlySaving(info);
  79:         }
  80:  
  81:         protected override void OnItemUpdateAborted(EventInfo info)
  82:         {
  83:             base.OnItemUpdateAborted(info);
  84:         }
  85:  
  86:         protected override void OnItemUpdating(EventInfo info)
  87:         {
  88:             base.OnItemUpdating(info);
  89:         }
  90:     }
  91: }

As I did here, you should always add those three attributes to the class, albeit with different values for the Guid and ProgId attribute. And you should add these lines to your assemblyinfo file:

   1: [assembly: ApplicationActivation(ActivationOption.Server)]
   2: [assembly: ApplicationName("TestSink")]
   3: [assembly: ApplicationAccessControl(false, AccessChecksLevel = AccessChecksLevelOption.Application)]

Be sure to customize the ApplicationName attribute, though.

Now that you have created an Eventsink, you should add an installer so you can deploy the solution more easily:

   1: using System.Collections;
   2: using System.ComponentModel;
   3: using System.Configuration.Install;
   4: using InfiniTec.Exchange.Eventing;
   5:  
   6: namespace TestSink
   7: {
   8:     [RunInstaller(true)]
   9:     public class TestInstaller: Installer
  10:     {
  11:         public override void Commit(IDictionary savedState)
  12:         {
  13:             Initialize();
  14:             base.Commit(savedState);
  15:         }
  16:  
  17:         public override void Install(IDictionary stateSaver)
  18:         {
  19:             Initialize();
  20:             base.Install(stateSaver);
  21:         }
  22:  
  23:         public override void Rollback(IDictionary savedState)
  24:         {
  25:             Initialize();
  26:             base.Rollback(savedState);
  27:         }
  28:  
  29:         public override void Uninstall(IDictionary savedState)
  30:         {
  31:             Initialize();
  32:  
  33:             base.Uninstall(savedState);
  34:         }
  35:  
  36:         private void Initialize()
  37:         {
  38:             var url = Context.Parameters["url"] + "/eventsink.evt";
  39:  
  40:             Context.LogMessage("Binding Url: " + url);
  41:             string criteria = Context.Parameters["Criteria"] ?? string.Empty;
  42:             criteria = criteria.Replace("$", "\"");
  43:  
  44:             Installers.Add(new EventSinkInstaller<TestSink>
  45:                                {
  46:                                    Criteria = criteria,
  47:                                    EventMethods = EventMethods.SynchronousEvents,
  48:                                    Scope = MatchScope.Shallow,
  49:                                    Url = url
  50:                                });
  51:         }
  52:     }
  53: }

The reason why the Initialize() method is called in each of the methods is that the installer requires the url to the path on which the sink is registered. Unfortunately, the Context property is set after the constructor is run. What you basically need to do is  to add an EventSinkInstaller the list of installers executed. The EventSinkInstaller has a type parameter which you can use to specify the type of sink registered. The installer will use this type parameter to determine the correct ProgId for the Eventsink. This example registers a synchronous event with a MatchScope of Shallow on the folder specified via the “url” parameter. Additionally, the constraint for the Eventsink is taken from the “constraint” parameter. This approach is superior to the regevent.vbs script because it lets you as the developer decide which parameters are fixed (the scope and type of the EventSink in this case) and which are variable, making the whole registration purpose less prone to errors.

Debugging

The InfiniTec.Exchange.Eventing assembly also defines a TraceSource which you can use for debugging. The TestSink application attached to this article contains the following application.config to activate the trace component:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.diagnostics>
   4:         <trace autoflush="true" />
   5:         <sources>
   6:             <source name="infinitec.exchange.eventing"
   7:               switchName="verbosity"
   8:               switchType="System.Diagnostics.SourceSwitch">
   9:         <listeners>
  10:           <add name="listener"
  11:            type="System.Diagnostics.TextWriterTraceListener"
  12:            initializeData="c:\temp\TextWriterOutput.log" />
  13:         </listeners>
  14:       </source>
  15:         </sources>
  16:         <switches>
  17:             <add name="verbosity" value="All"/>
  18:         </switches>
  19:     </system.diagnostics>
  20: </configuration>

Licensing

You may use this code (InfiniTec.Exchange.Eventing and InfiniTec.Common) in your application, regardless whether it is personal or business, free of charge.

Downloads


Tags: , ,

Technorati: , ,

Posted by Henning Krause on Tuesday, July 22, 2008 5:16 PM, last modified on Tuesday, July 22, 2008 5:18 PM
Permalink | Post RSSRSS comment feed

Comments (4) -

On 8/15/2008 11:05:19 AM Lars Peter Thomsen Denmark wrote:

Lars Peter Thomsen

Could you go through the steps of installing the TestSink on an exchange server. I&amp;amp;#39;ve tried it, but keep getting errors. On a clean download of the foundation to an Exchange 2007 server:

Run:

C:\Documents and Settings\lars\Desktop\WindowsLiveWriter_ExchangeEventSinkFoundation_C962_InfiniTec.Exchange.Eventing\TestSink\bin\Debug&amp;amp;amp;gt;c:\WINDOWS\microsoft.net\Framework\v2.0.50727\InstallUtil.exe TestSink.dll

Result:
An exception occurred during the Install phase.
System.IO.FileNotFoundException: Could not load file or assembly &amp;amp;#39;ADODB, Version=7.0.3300.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a&amp;amp;#39; or one of its dependencies. The system cannot find the file specified.

Alright, so the adodb.dll in InfiniTec.Exchange.Eventing couldn&amp;amp;#39;t be found. Copy it to TestSink\bin\Debug\ and repeat:

An exception occurred during the Install phase.
System.Runtime.InteropServices.COMException: Provider cannot be found. It may not be properly installed.

Not a very helpful response - what am I doing wrong here?

On 6/17/2009 11:18:53 AM Sandhya India wrote:

Sandhya

Can you please let us know the steps of installing the TestSink on an exchange server (2007)?

On 8/3/2009 12:35:31 PM Datta Kandalkar United States wrote:

Datta Kandalkar

Hi All,

Can anybody please let me know the steps of installing the TestSink.dll on the exchange server 2003

On running

&amp;amp;amp;gt;InstallUtil.exe TestSink.dll

I am getting the error as

An exception occurred during the Install phase.
System.Runtime.InteropServices.COMException: Object or data matching the name, r
ange, or selection criteria was not found within the scope of this operation.

On 8/3/2009 12:36:07 PM Datta Kandalkar India wrote:

Datta Kandalkar

Hi All,

Can anybody please let me know the steps of installing the TestSink.dll on the exchange server 2003

On running

&amp;amp;amp;gt;InstallUtil.exe TestSink.dll

I am getting the error as

An exception occurred during the Install phase.
System.Runtime.InteropServices.COMException: Object or data matching the name, r
ange, or selection criteria was not found within the scope of this operation.

 +Pingbacks and trackbacks (1)