Programming Microsoft Outlook and Microsoft Exchange, Second Edition (DV-MPS Programming)

[Previous] [Next]

Being able to capture and program events in Exchange Server 2000 opens up a world of possibilities when writing applications. Because the events are server side, it doesn't matter which client puts items into the database; no matter what the client, the events will fire and your code will run. This means that if a user drags and drops a Word document into Exchange Server using the new file system capabilities, your events will fire.

Exchange Server 5.5 took an impressive first step toward utilizing events. As you've seen in this book, the Event Scripting Agent provides countless possibilities for Exchange Server 5.5 applications to add workflow, data validation on the server, and other server-side programming capabilities. However, the Exchange Server 5.5 Event Scripting Agent supports only asynchronous events, meaning events are fired after the item has been committed into the Exchange Server database. This makes it hard to prohibit users from moving, copying, deleting, or modifying items since you're notified after they have already performed these activities. Fortunately, any applications you've written using the Exchange Server 5.5 event architecture will run in Exchange Server 2000.

Besides supporting asynchronous events, Exchange Server 2000 adds synchronous events, which are called before the item is committed to the database. Your application can look at the item, and then either accept it or prevent it from being committed into Exchange Server.

Be aware that events are fired when items are moved, copied, created, or deleted in Exchange Server. The scope of these events is scoped to a folder, and you can write event handlers that are passed ADO or OLE DB interfaces, depending on the programming language you use. You can write your event handlers in script, Visual Basic, or Visual C++. I'll concentrate on Visual Basic in this discussion. I recommend that you write your event handlers in this language, unless of course your language of choice is Visual C++.

I don't recommend writing the event handlers in script because it's harder to write and debug. However, if you don't have permissions on the server to install your event handlers, using scripts can be helpful. Since your event handler is a script that you can store directly in Exchange and not a COM component you need to install on the Exchange Server, as long as you have permissions to register for events, your scripts can fire.

Firing Order of Events

When an item is saved into Exchange Server, events are fired in a fixed order in the system. This firing order is important to understand because multiple entities can be working on the items in your folders, which can make it look as though your application isn't working. Synchronous events fire first, followed by any folder rules, and finally by asynchronous events. As you might have guessed, these events build on one another. For instance, if your synchronous event aborts the item from being saved, then the rules and asynchronous events will not be notified of the item. Furthermore, if any events that are higher up in the chain move or delete item, the other events should be prepared to not have access to the item.

Security Requirements

There are some security requirements that you should know about before you write your event handlers. First, you must be a folder owner in order to register an event handler in a folder. Plus, Exchange Server provides an extra security precaution: if you're registering someone else's event handler, he can implement the ICreateRegistration interface, which is called when you attempt to register his component as the object to handle events in your folder. If the component writer wants, he can prohibit you from registering his event handler.

Second, if you're writing COM components to implement your event handlers, you must have access to install components on the Exchange server. Exchange Server does not support instantiating and running remote components via DCOM as event handlers. Although there is nothing stopping you from doing this in your event handler, you must remember that the EXOLEDB provider is not remotable. Therefore, you should run your code on the same server the data is on. You can, however, use DCOM to connect to another component residing on another server that accesses data on that server. Getting the security contexts to support this will be the hard part. If you use COM+ applications (which we'll talk about later in this section), you can have your components run in a specific security context, making this easier.

Supported Events

As mentioned earlier, Exchange 2000 supports a wide range of events, including synchronous events, asynchronous events, and system events. We will cover these in this section.

Synchronous Events

Synchronous event handlers are called twice by Exchange Server. The first time, the event handler is called before the item is committed into the Exchange Server database. The second time the event handler is called, the item either has been committed or aborted. On the first pass, the item is read/write. You can modify properties or copy the item somewhere else. However, on the second pass, after the transaction has been committed, the item is read-only. Be aware that the item is not a true item in the database on the first pass. Since the item is not yet committed, you shouldn't grab any properties that could change in the future—between the time you access the item and the time it is committed. For example, the URL to the item is not valid on the first pass since the item is not yet in the database. Since many factors could change the URL, you shouldn't query or save it during the first pass.

In synchronous events, your event handler runs in the context of a local OLE DB transaction. Therefore, any changes that you make to the item will not trigger other events. However, you also must realize that the work performed in your event handler can be undone if another event handler rejects the item from being committed. All the event handlers that act on an item in a folder must commit in order for the action to occur. If any event handler rejects the transaction, the action will not occur. If your event handler has already run, it will be called a second time and will be notified that the action has been aborted. Your event handler can then perform any necessary cleanup.

For example, let's say you have two event handlers registered in a folder for the OnSyncSave event. One of them opens the item and saves attachments to another location. The other validates data in the item before allowing it to commit. Based on the priority you set for your event handlers, if the validation occurs after copying the attachments, the validation could fail and the transaction for saving the item could be aborted. Now, any good developer obviously would make validation precede the other events. However, since multiple developers can register event handlers (as long as they meet the security requirements mentioned earlier), you should be aware that other event handlers can abort transactions. In this case, your event handler for copying the attachments will be notified that the transaction was aborted, and you should clean up your work by deleting the attachments from the other location.

You also should be aware that synchronous event handlers, while running, are the only process that can access an item. Exchange Server will block any other threads, processes, or applications from accessing that item while your event handler is running and working on it. This is critical because if you write an inefficient event handler, you can degrade the performance of other applications and Exchange Server. For example, if your event handler takes 10 seconds to run, each time an Outlook user saves an item that triggers your event handler, Outlook will show an hourglass for 10 seconds. Therefore, if you can use asynchronous events to implement your functionality, you should do so. If the Outlook scenario had been with an asynchronous event, Outlook would have returned immediately and allowed the user to continue working.

Synchronous events are also expensive for Exchange Server to perform. The server needs to stop its processing on the item, call your event handler, wait, and then figure out whether to commit or abort the transaction for the item based on your event handler. All this creates temporary copies of the item before committing and forcing the Exchange Server threads to wait, which affects performance.

Continuing with the Outlook scenario, if you abort the transaction, different clients will display different error messages. Outlook will probably display an error message stating that the item couldn't be saved. You cannot show user interface elements in your event handlers because they are running on the server. Instead, you'll have to find a way to notify your users that they submitted the item incorrectly, or that the action they're trying to perform is not allowed. You can perform this notification via e-mail or another method.

Exchange Server 2000 supports two synchronous events: OnSyncSave and OnSyncDelete. As you can guess from their names, the OnSyncSave and OnSyncDelete events support save and delete operations. However, both events are called as part of move and copy operations. For example, if you move an item from one folder to another, a save event will be fired in the new folder and a delete event will be fired in the old folder. If either is aborted, the move will not occur. With a copy, you will get a save event in the location where the copy is supposed to be placed. You should know that the OnSyncDelete event is not called on an item when the item's parent folder is moved or deleted. Also, OnSyncDelete can distinguish between hard and soft deletes. A hard delete is a deletion in which the item is completely removed from the Exchange database. A soft delete is a deletion in which the item is moved into the dumpster. (An analogy would be the deleted items subfolder for every folder.) A user can recover an item from the dumpster. You are notified of the deletion type by the flags that are passed to the OnSyncDelete event.

OnSyncSave is not called for items in a folder when the parent folder for the items is being moved or copied. The event will be called, however, for the parent folder; you can abort the transaction if you don't want the items moved or copied.

You might be asking yourself, "If only Save and Delete are supported, how do I get notified of a change?" If a user makes a change to an item (such as modifying the subject or any property) and saves the item, you will receive the OnSyncSave event. The flags that are passed to the event will notify you that the item has been modified. However, you will not receive notification of which property the user changed in the item. You will have to scan the item to see what changed. To do this, you must have an original copy of the item, which you can obtain by copying the item in your event handler to another folder.

When you register your event handler, if you do not specify criteria for the types of items for which you want to receive events, Exchange Server will notify you of all new items being put into the folder. Folders store some surprising items that you might not expect to handle in your event handler. For example, when someone publishes a form or adds a field to a folder in Outlook, a hidden item is added to the folder. This will trigger an event. You should set the criteria for your event registration, which we'll learn about in a little while, so that only the items your event handler is interested in can trigger events.

Asynchronous Events

Exchange Server 2000 supports two asynchronous events, OnSave and OnDelete. These asynchronous events are called after an item has been committed to the database, and they fire in no particular order. Although these events are guaranteed to be called, another process or user could delete or move the item before the event handler even sees it. Exchange Server doesn't guarantee when it will call your event handler, but usually your event handler will be called as soon as the item is committed to the Exchange Server database. Furthermore, if multiple asynchronous event handlers are registered for a single folder, Exchange does not guarantee in which order the event handlers will be called. To set the firing order for synchronous event handlers, you can use the priority property, which we will discuss in a little while. Again, you should use asynchronous events rather than synchronous events whenever possible.

System Events

The three system events of Exchange Server 2000 are OnMDBStartup, OnMDBShutdown, and OnTimer. OnMDBStartup and OnMDBShutdown are called whenever an Exchange Server store starts or shuts down. This is useful for event handlers that want to scan the database or perform some sort of activity whenever the database starts or shuts down. (An event handler is an object whose methods Windows uses to notify an application about events). Because these two events are asynchronous, Exchange Server won't wait for your event handler to finish before continuing execution of the item in question.

The OnTimer event fires according to your configured parameters. For example, you can have a timer event fire every five minutes, daily, weekly or monthly. It all depends on the requirements of your application. We'll see how the Training application uses timer events for notification about new courses and student surveys for courses that already have taken place.

Registering an Event Handler

I'm going to do this a little bit backwards. Since writing event handlers involves working with registration parameters, I'll discuss the registration process first. That way, things will be clearer to you when we talk about writing actual event handlers later in this section.

Registering an event handler is quite easy. Exchange Server 2000 provides a script program called regevent.vbs, which allows you pass some parameters to the program and regevent.vbs registers for all the event types you specify. Besides using regevent.vbs, you can create event registration items for your applications just by creating new items in Exchange using ADO. In the setup program for the Training application, the three event handlers for the application are registered automatically using the ADO method. We'll look at the code for this registration at the end of this section.

Event Registration Properties

When registering events, you need to set some criteria to tell Exchange Server what events you're interested in, what the ProgID or script location of the event handler is, and so on. Table 19-6 shows the criteria required to register an event handler. All these properties are contained in the http://schemas.microsoft.com/exchange/events/ name space.

Table 19-6. Criteria required by Exchange Server 2000 to register an event handler.

Property Name Required
Criteria No
Enabled No
EventMethod Yes
Matchscope No
Priority No
Scope Yes (Note that this property is in RC1 but will not be in the RTM product. Therefore, if you are working with RC1 of Exchange 2000, you will have to set this property. In the final version of Exchange 2000, this property will no longer exist.)
scripturl Yes (for script event handlers only)
sinkclass Yes
timerexpirytime No (for timer events only)
timerinterval Yes (for timer events only)
timerstarttime Yes (for timer events only)

criteria property

The criteria property allows you to specify a SQL WHERE clause that will act as a filter for your event handler so that the handler is called only when items meet your criteria. This property allows you to avoid being called for items that you're not interested in. For example, the Training application uses the following criteria so that it doesn't get called when hidden items or folders are created:

WHERE "DAV:ishidden" = false and "DAV:isfolder" = false

You can use AND, OR, NOT, or EXISTS as part of your WHERE clause. CONTAINS is not supported, however. Also, if you plan to check custom schema, you must explicitly cast your custom property to the right data type. For example, if you want to make sure that your event handler is called only in an application in which a property on items submitted is greater than 100, you would set the criteria property for your event registration to the following value:

"WHERE cast($"MySchema/MyNumber"$ as 'i4')>100"

Notice how the $ character is used to escape double quotation marks.

enabled property

This Boolean property allows you to specify whether your event handler is enabled. Rather than deleting an event registration, if you plan to reuse it in the future, you can set this property to False.

eventmethod property

This property is a multivalued string that allows you to specify the types of events you are interested in receiving, such as OnSyncSave and OnDelete. You can register for event methods of the same type, within the same event registration. For example, one event registration can be used for OnSyncSave, OnSyncDelete, OnSave, or OnDelete but cannot include OnTimer. You must register OnTimer and the other system events separately. However, your event handler COM component could implement the interfaces for all the events.

matchscope property

This property allows you to specify the scope of the event. The values for this property can be any, fldonly, deep, exact, and shallow. You'll use only the value any with database wide events. The scope of the exact value is a specific item. This is similar to shallow, which fires for items only in the exact folder you specify. The fldonly value will notify you only of changes to the folder itself, such as modifications to a property on the folder. The deep value notifies you of changes in the current folder as well as any items in subfolders. By setting the property to deep, even new subfolders and items created in them will trigger your event handler. This capability is an improvement from Exchange Server 5.5, in which there was no concept of a deep scope. If new folders were created in Exchange 5.5, you would have to explicitly register new event handlers in them. Exchange Server 2000 system events do not support this property.

priority property

This integer property allows you to specify a number that indicates the priority of your event handler, compared to other event handlers. The number for this property can range from 0 to FFFFFFFF. By default, your event handler is registered with a value of 65,535 (0x0000FFFF). This property is valid only for synchronous events and tells the system what order you want these events to fire in. If you give two synchronous events the same priority, it is undetermined which one will get called first. When registering your event handlers, you might want to check to see whether other event registrations exist in the folder. If they do, check their priority before registering your event handler.

scope property

The scope property provides a URL to the folder where the event handler wants to be notified of events. This can be in the form of a file:// or http:// URL. This property is valid only for store events, not for system events. Please note that this property, while required in RC1 of Exchange 2000, will be removed from the final product.

scripturl property

When you write script for your event handler, this property holds the URL to the script file. Exchange Server supports the file:// and http:// URL formats in this property. The script file can live in Exchange Server or in another location accessible via a URL, as long as the location is on the same machine as the Exchange server. When using script handlers, be aware that you must specify ExOleDB.ScriptEventSink for the sinkclass property in addition to filling out the scripturl property.

sinkclass property

This property holds the CLSID or the ProgID of your event handler. Exchange Server will then instantiate the object when an event is triggered. By default, Exchange Server will cache your object so that it doesn't have to instantiate the object multiple times.

timerexpirytime property

This integer property specifies the number of minutes after the timerstarttime property that the event handler should stop receiving OnTimer notifications. If you don't specify this property, your event handler will never stop receiving notifications. This property is valid only for OnTimer event registrations.

timerinterval property

This integer property specifies the amount of minutes to wait to notify your event handler of another OnTimer event. If you do not set this property, Exchange Server will call your event handler only once after the creation of your registration item for the OnTimer event.

timerstarttime property

This property allows you to specify the date and time to start notifying your event handler of OnTimer events. If you do not specify this property, Exchange Server will start notifying your handler immediately. Note that this value can be affected by Exchange storing date and time values as UTC values. Therefore, you're going to want to set the UTC time, not the local time, when you want your timer event to start firing. For example, the Training application setup program needs to create the event registration for survey notifications. The survey notification event handler should be called at 10 pm local time every night. To create the correct timerstarttime property value, the setup program has to figure out what 10 pm local time is in UTC. Then it registers to be notified at the correct UTC time, which will be converted by Exchange to 10 pm local time depending on the server machine's time zone.

Creating an Event Registration Item

The easiest way to register your event handler with Exchange Server is to use ADO to create an event registration item in the Exchange Server database and set the properties we just discussed on that item. Be aware that when you create your event registration item, you should do it in the context of an OLE DB transaction. Why? Because Exchange Server utilizes its own events to provide an event registration event handler, which is installed by default. This event handler looks for items with a special content class, urn:content-class:storeeventreg. The event handler then takes those items, scans the properties, and performs its magic, making the items valid event registrations. This magic includes turning the item into an invisible message in the folder by making it part of the associated contents table. This table is the same place that views, forms, and rules are stored.

If you don't use OLE DB transactions, you might trigger the event handler when you first attempt to create your registration item using ADO and set the DAV:contentclass property to urn:content-class:storeeventreg. If this happens and you haven't set the properties for your registration item yet, the event handler will think that your event registration item is invalid. By using an OLE DB transaction and atomically creating and setting your properties at the same time, you can avoid this problem.

The following code, taken from the setup program for the Training application, shows you how easy it is to create an event registration item:

… 'Create the Survey Notification Event Registration 'Timer event strNow = Now arrRequired = GenerateRequiredEventArray("", "ontimer", _ "EventSink.SurveyNotify", "", "") arrOptional = GenerateOptionalEventArray("", "", "", "", _ 1440, strNow, "") CreateEvtRegistration oConnection, strPath & _ "Schedule/surveynotification", arrRequired, arrOptional, False … 'Event Registration Helper Sub Sub CreateEvtRegistration(cn, strEventRegPath, arrRequiredParameters, _ Optional arrOptionalParameters, Optional bWorkflow) On Error GoTo errHandler 'Create the event registration. 'cn - Connection to Exchange Server database for transaction purposes 'strEventRegPath - Full file path to the event item 'arrRequiredParameters - Required parameters for all event registrations 'arrOptionalParameters - Optional parameters such as criteria Const propcontentclass = "DAV:contentclass" Const propScope = "http://schemas.microsoft.com/exchange/events/Scope" Dim rEvent As New ADODB.Record cn.BeginTrans rEvent.Open strEventRegPath, cn, 3, _ adReadWrite + adCreateOverwrite + adCreateNonCollection 'Set the properties in the item With rEvent.Fields .Item(propcontentclass) = "urn:content-class:storeeventreg" 'Scroll through and commit required parameters. 'Scroll through and commit optional parameters. If IsArray(arrRequiredParameters) Then For i = LBound(arrRequiredParameters, 1) To _ UBound(arrRequiredParameters, 1) 'Use Dimension 1 since this the second dimension should 'always be the same If Not (IsEmpty(arrRequiredParameters(i, 0))) Then .Item(arrRequiredParameters(i, 0)) = _ arrRequiredParameters(i, 1) End If Next End If If IsArray(arrOptionalParameters) Then For i = LBound(arrOptionalParameters, 1) To _ UBound(arrOptionalParameters, 1) 'Use Dimension 1 since the second dimension should always 'be the same If Not (IsEmpty(arrOptionalParameters(i, 0))) Then .Item(arrOptionalParameters(i, 0)) = _ arrOptionalParameters(i, 1) End If Next End If 'Add custom properties that the event sink can use to 'determine the context of this event registration If bWorkflow = False Then strConfigurationFolderPath = strPath & "Configuration/" Dim propname As String 'Hard-code this one property so that we can always find it! propname = "http://thomriz.com/schema/configurationfolderpath" 'For a case switch in the event sink .Append propname, adVariant, , , strConfigurationFolderPath End If .Update 'Get the ADO object current End With cn.CommitTrans Exit Sub errHandler: MsgBox "Error in CreateEvtRegistration. Error " & Err.Number & " " & _ Err.Description End End Sub Function GenerateRequiredEventArray(strCriteria, strEventMethod, _ strSinkClass, strScriptURL, strSinkList) Const propCriteria = _ "http://schemas.microsoft.com/exchange/events/Criteria" Const propEventMethod = _ "http://schemas.microsoft.com/exchange/events/EventMethod" Const propSinkClass = _ "http://schemas.microsoft.com/exchange/events/SinkClass" Const propScriptURL = _ "http://schemas.microsoft.com/exchange/events/ScriptUrl" Const propSinkList = _ "http://schemas.microsoft.com/exchange/events/SinkList" 'Generate the array by checking the passed arguments. 'Note dynamic arrays only support redimensioning the last dimension. 'This causes a problem, so dimension an array and fill in blanks if 'necessary. 'Flip-flop value - propname Dim arrRequired(4, 1) iArrayCount = 0 If strCriteria <> "" Then arrRequired(iArrayCount, 0) = propCriteria arrRequired(iArrayCount, 1) = strCriteria iArrayCount = iArrayCount + 1 End If If strEventMethod <> "" Then arrRequired(iArrayCount, 0) = propEventMethod arrRequired(iArrayCount, 1) = strEventMethod iArrayCount = iArrayCount + 1 End If If strSinkClass <> "" Then arrRequired(iArrayCount, 0) = propSinkClass arrRequired(iArrayCount, 1) = strSinkClass iArrayCount = iArrayCount + 1 End If If strScriptURL <> "" Then arrRequired(iArrayCount, 0) = propScriptURL arrRequired(iArrayCount, 1) = strScriptURL iArrayCount = iArrayCount + 1 End If If strSinkList <> "" Then arrRequired(iArrayCount, 0) = propSinkList arrRequired(iArrayCount, 1) = strSinkList iArrayCount = iArrayCount + 1 End If GenerateRequiredEventArray = arrRequired End Function Function GenerateOptionalEventArray(bEnabled, strMatchScope, lPriority, _ bReplicateReg, iTimerInterval, iTimerStart, iTimerStop) Const propEnabled = _ "http://schemas.microsoft.com/exchange/events/Enabled" Const propMatchScope = _ "http://schemas.microsoft.com/exchange/events/MatchScope" Const propPriority = _ "http://schemas.microsoft.com/exchange/events/Priority" Const propReplicateEventReg = _ "http://schemas.microsoft.com/exchange/events/ReplicateEventReg" Const propTimerInterval = _ "http://schemas.microsoft.com/exchange/events/TimerInterval" Const propTimerStartTime = _ "http://schemas.microsoft.com/exchange/events/TimerStartTime" Const propTimerExpiryTime = _ "http://schemas.microsoft.com/exchange/events/TimerExpiryTime" Dim arrOptional(6, 1) iArrayCount = 0 If bEnabled <> "" Then arrOptional(iArrayCount, 0) = propEnabled arrOptional(iArrayCount, 1) = bEnabled iArrayCount = iArrayCount + 1 End If If strMatchScope <> "" Then arrOptional(iArrayCount, 0) = propMatchScope arrOptional(iArrayCount, 1) = strMatchScope iArrayCount = iArrayCount + 1 End If If lPriority <> "" Then arrOptional(iArrayCount, 0) = propPriority arrOptional(iArrayCount, 1) = lPriority iArrayCount = iArrayCount + 1 End If If bReplicateReg <> "" Then arrOptional(iArrayCount, 0) = propReplicateEventReg arrOptional(iArrayCount, 1) = bReplicateReg iArrayCount = iArrayCount + 1 End If If iTimerInterval <> "" Then arrOptional(iArrayCount, 0) = propTimerInterval arrOptional(iArrayCount, 1) = iTimerInterval iArrayCount = iArrayCount + 1 End If If iTimerStart <> "" Then arrOptional(iArrayCount, 0) = propTimerStartTime arrOptional(iArrayCount, 1) = iTimerStart iArrayCount = iArrayCount + 1 End If If iTimerStop <> "" Then arrOptional(iArrayCount, 0) = propTimerStopTime arrOptional(iArrayCount, 1) = iTimerStop iArrayCount = iArrayCount + 1 End If GenerateOptionalEventArray = arrOptional End Function

As you can see in the code, ADO is used to create an event registration item. Using the Fields collection, the properties to make the item a valid event registration are filled in, and the item is saved into Exchange Server. Although this particular application uses events in public folders, you can register and fire events from private folders as well.

Registering a Database wide Event

In addition to scoping your event handlers to just one folder, you can scope them to the entire store or MDB. However, you cannot scope your event handler to the entire server. All types of event registrations are per individual MDB (database) only. To create an MDB-wide event registration, you need to change the value for the matchscope property and the location where you put your registration item.

You should specify any as the value of the matchscope property. This indicates that any scope is valid for notifying your event handler. All other properties can stay the same, based on the type of event you're registering for.

As just mentioned, the location in which you put the registration item will change. Instead of throwing the item into the folder where you want the scope of the event notifications to begin, you need to place the registration item in the Globalevents folder. This folder is located in Public Folders in the non-ipm subtree in a folder called StoreEvents{MDBGUID}, where MDBGUID is the unique identifier for the MDB. The easiest way to retrieve the MDBGUID is to use the StoreGUIDFromURL method in EXOLEDB. The following code sample shows you how to use this method:

set oStoreGUID=CreateObject("Exoledb.StoreGuidFromUrl") 'Get the GUID strguid = oStoreGUID.StoreGuidFromUrl("file://./backofficestorage/" & _ somestoreitem)

A valid path to save your registration item to would look something like this:

file://./backofficestorage/ADMIN/domain/public folders/non_ipm_sub tree/StoreEvents{GUID}/globalevents/ 'For a mailbox database File://./backofficestorage/ADMIN/domain/MBX/SystemMailbox{GUID}/_ Storeevents/GlobalEvents

Only the Administrator account, which is a member of the Domain Administrators group, or users in the Exchange Administrators role can register global events. It is not enough to be a member of the Administrators group or the Exchange Servers group.

Using the ICreateRegistration Interface

If you're writing global event handlers that you think other developers will register for, or if you want to protect your application event handlers, you can implement the ICreateRegistration interface. This interface will be called whenever a user tries to register for your event handler. ICreateRegistration passes you the event registration information, including the registration item for the particular user. You can grab information from this item, or you can also retrieve from the item information about the user who's trying to register for your event handler. You can then decide whether to permit or reject the user's registration.

To implement this interface, you need to add an Implements ICreateRegistration line to your Visual Basic code and put your validation code in the ICreateRegistration_Register function.

Writing an Event Handler

The code samples containing event handlers that you'll see in this section are taken from the Training application. All these event handlers are written in Visual Basic. When writing Visual Basic event handlers, you first should create an ActiveX DLL. In addition, make sure to add references to the various object libraries your application might need to access. You definitely will need a reference to the EXOLEDB type library; libraries such as Microsoft ActiveX Data Objects 2.5 and Microsoft CDO for Exchange Server 2000 also might come in handy.

Once you've added the references, you need to use the Implements keyword. Depending on the type of event handler that you plan to write, you'll need to implement the IExStoreSyncEvents, IExStoreAsyncEvents, or IExStoreSystemEvents interface. You can implement all three in a single DLL if you want.

Next, you must implement the subroutines for the events you're interested in. The following code, taken from the course notification event handler, shows the OnSave and OnTimer events being implemented. I won't list all the code for the implementation of these two events because I mainly want to show you the parameters that are passed your functions.

Implements IExStoreAsyncEvents Implements IExStoreSystemEvents Const strHTMLMsgSubject = "New Course Email" Dim oEventRegistration 'Global that holds the event 'registration record Dim bShowPreviousDay 'Global that holds whether to show 'new courses only 'From previous day, just in case timer 'event runs after midnight Dim oRecord As ADODB.Record 'Global record to hold item that 'triggered event Dim strHTMLBody 'Global string that holds HTML message body 'Add Discussion group, file, and http link notification as part of 'the message. 'Update HTML message to incorporate file and http link, as well as 'discussion group. Private Sub IExStoreAsyncEvents_OnDelete(ByVal pEventInfo As _ Exoledb.IExStoreEventInfo, ByVal bstrURLItem As String, _ ByVal lFlags As Long) 'Not implemented End Sub Private Sub IExStoreAsyncEvents_OnSave(ByVal pEventInfo As _ Exoledb.IExStoreEventInfo, ByVal bstrURLItem As String, _ ByVal lFlags As Long) If (lFlags And EVT_NEW_ITEM) > 0 Then 'New item put in. 'Get the ADO Record object for the item. 'Set oRecord = dispInfo.EventRecord Set oRecord = CreateObject("ADODB.Record") oRecord.Open bstrURLItem 'Check to see whether the item happened very recently. 'If it did, just exit since the training event is 'probably having its survey information updated by 'a user. Don't notify people of old training events. If DateDiff("d", Now, _ oRecord.Fields("urn:schemas:calendar:dtstart").Value) > 0 Then 'It's OK; event happens in future. 'Load the global settings. LoadAppSettings oRecord, pEventInfo If bEventLogging Then App.LogEvent "Event Notification OnSave event called for " & _ "training event. Path: " & bstrURLItem End If strCategory = GetCourseCategory(oRecord) QueryPreferences strCategory End If End If End Sub Private Sub IExStoreSystemEvents_OnMDBShutDown _ (ByVal bstrMDBGuid As String, ByVal lFlags As Long) 'Not implemented End Sub Private Sub IExStoreSystemEvents_OnMDBStartUp _ (ByVal bstrMDBGuid As String, ByVal bstrMDBName As String, _ ByVal lFlags As Long) 'Not implemented End Sub Private Sub IExStoreSystemEvents_OnTimer(ByVal bstrURLItem As String, _ ByVal lFlags As Long) Dim rec As ADODB.Record Dim rst As ADODB.Recordset Dim conn As ADODB.Connection Set oBindingRecord = Nothing 'Get the registration On Error Resume Next Set oBindingRecord = CreateObject("ADODB.Record") oBindingRecord.Open bstrURLItem On Error GoTo 0 If Err.Number = 0 Then 'Could retrieve the item LoadAppSettings oBindingRecord If bEventLogging Then App.LogEvent "Event Notification: OnTimer event called at " & Now End If 'Get the folder where the timer is running. 'This is never used since we know the folder already. 'However, this shows you how to retrieve the folder if you need to. strFolder = oBindingRecord.Fields.Item("DAV:parentname") 'Figure out all the courses created in the current 24 hours or 'previous 24 hours curDate = Date If bShowPreviousDay Then 'Subtract a day curDate = DateAdd("d", -1, curDate) End If dISODateStart = TurnintoISO(curDate, "Start") dISODateEnd = TurnintoISO(curDate, "End") strSQL = "Select ""urn:schemas:mailheader:subject""," & _ ""DAV:href"",""urn:schemas:calendar:dtstart""," & _ ""urn:schemas:calendar:dtend"" FROM scope('shallow " & _ "traversal of """ & strScheduleFolderPath & _ """') WHERE (""DAV:iscollection"" = false) AND " _ & "(""DAV:ishidden"" = false) " & _ "AND (""urn:schemas:calendar:dtstart"" >= CAST(""" & _ dISODateStart & """ as 'dateTime'))" & _ "AND (""urn:schemas:calendar:dtstart"" <= CAST(""" & _ dISODateEnd & """ as 'dateTime'))" Set conn = CreateObject("ADODB.Connection") Set rst = CreateObject("ADODB.Recordset") Set oRecord = CreateObject("ADODB.Record") With conn .Provider = "exoledb.datasource" .Open strScheduleFolderPath End With 'Create a new Recordset object With rst 'Open Recordset based on the SQL string .Open strSQL, conn, adOpenKeyset End With Dim iAppt As CDO.Appointment If Not (rst.BOF And rst.EOF) Then Set iAppt = CreateObject("CDO.Appointment") 'On Error Resume Next rst.MoveFirst Do Until rst.EOF 'Set oRecord to the current item in the Recordset On Error Resume Next oRecord.Close Err.Clear On Error GoTo 0 oRecord.Open rst.Fields("DAV:href").Value, conn strCategory = GetCourseCategory(oRecord) QueryPreferences strCategory rst.MoveNext Loop rst.Close Set iAppt = Nothing Set rst = Nothing Set rec = Nothing Set conn = Nothing Else If bEventLogging Then App.LogEvent "Event Notification: No students need to be " & _ "notified of training event." End If End If End If End Sub

As you can see in the code, your application is passed different parameters for the different events. However, all events have some common parameters. For example, you are always passed a URL to the item that triggered the event. For system events such as OnTimer, this is be the event registration item itself. If you need to, you can add custom properties to the event registration item so that when your OnTimer event handler is called, you can retrieve that custom property.

I use this trick in the Training application. When I register the OnTimer event handler for the application's workflow process, I add a custom property that is the full URL to the application's configuration folder. Since you can customize where the application is installed, you need to tell the workflow process where to look for the customization information that you select during setup. Because I added an extra property, I can grab it in my workflow code, get the configuration message contained in the folder the property specifies, and determine the value for the customized fields in the application, such as which e-mail address to send notification messages from.

For nontimer events, the URL you receive is the path to the item that's triggering the event. You can then retrieve the item and look at it properties.

An event handler also receives a parameter called lFlags. This parameter corresponds to flags that tell you exactly what's happening to the item. For example, one of the flags, EVT_NEW_ITEM, tells you that the item triggering the event is a new item rather than an item that already existed in the folder and had some properties changed. You should use a bitwise AND with the lFlags parameter and one of the identifiers from Table 19-7 to determine the values of flags in the Iflags parameter. The flag names are included in the EXOLEDB type library.

NOTE


Not all flags are supported by all events. For example, the delete flags are supported only by the delete events.

Table 19-7. Flag values for the lFlags parameter.

Flag Description
EVT_NEW_ITEM The item being saved is new rather than an existing, modified item.
EVT_IS_COLLECTION The item being saved is a collection (folder).
EVT_REPLICATED_ITEM The item being saved is the result of a replication event, which occurs when the item is replicated using Exchange Server replication.
EVT_IS_DELIVERED The item being saved is the result of a message delivery.
EVT_SOFTDELETE A soft delete has occurred, meaning the item has been moved to the dumpster.
EVT_HARDDELETE A hard delete has occurred, meaning the item has been removed from the store.
EVT_INITNEW This is the first time that the event handler has been called. This allows you to perform any necessary initialization procedures. Your event handler is passed this flag only once during it's lifetime.
EVT_MOVE The event is the result of a move operation.
EVT_COPY The event is the result of a copy operation.
EVT_SYNC_BEGIN The event handler is being called in the begin phase of a synchronous event. This is the point at which you can abort the item being saved into the database. Also, the URL you receive for the item is invalid at this point, since the item is not in the database yet.
EVT_SYNC_COMMITTED The event handler is being called in the commit phase of a synchronous event, after the transaction has been committed to the database.
EVT_SYNC_ABORTED A synchronous event has been aborted.
EVT_INVALID_URL This tells the event handler that the URL passed to it is invalid.
EVT_INVALID_SOURCE_URL The source URL could not be obtained during a move operation.
EVT_ERROR Some error occurred in the event.

When using server events, in addition to the flags and the URL, you will receive a pEventInfo variable of type Exoledb.IExStoreEventInfo. You should set this variable to another variable of type Exoledb.IExStoreDispEventInfo, which is the IDispatch version of the interface. From this interface, you can control the transaction state for the event handler for synchronous events and get more information about the context of the event. Table 19-8 outlines the elements you retrieve and call on the IExStoreDispEventInfo interface.

Table 19-8. Elements of the IExStoreDispEventInfo interface.

Element Description
AbortChange Aborts the transaction in which the event is currently executing. Exchange Server will not commit the item to its database. You pass a long value, which specifies the error code you want returned.
EventBinding Returns as an object the registration item for the event handler. You can use the returned object to retrieve the custom properties that you set on the event registration item.
EventConnection The ADO Connection object under which the event is executing.
EventRecord The ADO Record object bound to the item that triggered the event.
SourceURL The original source URL in an OnSyncSave event. This property is valid only for a move operation.
StoreGUID The GUID for the MDB where the item that triggered the event is located.
UserGUID The GUID for the user that triggered the event. You can use this in conjunction with the Win32 APIs to look up the user's name.
UserSID The Security Identifier (SID) of the user who triggered the event.
Data This property allows you to save in-memory data between the begin and commit or abort phase of a synchronous event. This property is useful for passing to your event sink the variables between the different calls in a transaction; you don't have to save those variables to Exchange or on disk.

Creating COM+ Applications

If you plan to write your event handlers in Visual Basic, you should definitely install your DLLs as COM+ applications. There are a number of reasons for doing this. First, Exchange Server won't let you try to run an event handler for synchronous events written in VB that are not COM+ enabled. Second, if your event handler has problems, making it into a COM+ application isolates it and allows you to shut it down without affecting other parts of the system. Finally, by making your DLLs COM+ applications, you can take advantage of COM+ roles and security.

This last point is key—it allows you to retrieve the SID of the user who's triggering the event. Using the COM+ Services Type Library and the SID, you can employ COM+ roles-based security. For example, you can use the IsSecurityEnabled function to check whether COM+ security is enabled. You can also use the IsUserInRole function to see whether the user is in a particular role. For more information on COM+ security issues, refer to the Platform SDK on MSDN at http://msdn.microsoft.com.

The Training application places its event handlers into a COM+ application. The Training application's setup program deploys the COM+ application with the built-in COM+ deployment tools. When you export your COM+ application from the COM+ user interface, as shown in Figure 19-7, COM+ will create a Windows Installer file. You can then shell out to this file or run it directly if you want to install the COM+ application on your computer.

Figure 19-7. Exporting the COM+ application for the Training sample.

Debugging an Event Handler

When you set up the Training application, it asks if you want to enable event handler debugging. Debugging your event handler is easy if you use COM+ applications. The only three things you need to do to debug your event handler is set the identity of your COM+ application so that it uses the interactive user , add some breakpoints to your VB event handler, and then you put Visual Basic event handler in run mode. Then, you'll be able to take advantage of all the great VB debugging. Figure 19-8 shows an example of an event handler in the Visual Basic debugger.

Figure 19-8. One of the Training application event handlers in the Visual BasicB debugger.

In addition to using the debugger, you can also leverage VB's LogEvent method on the App object. Using this method, you can have VB place entries into the Windows 2000 event log. The Training application, when you set it up, asks if you want to enable event handler debugging. This is the sort of debugging that it uses. Figure 19-9 shows an example from the event log for of an event handler from one of the Training application's event handlers.

Figure 19-9. A message posted by Visual Basic into an event handler's log.

Категории