If you would like to alter the default publish/subscribe behavior, COM+ provides a mechanism called event filtering. There are two kinds of filtering. The first, publisher filtering, lets you change the way events are published and therefore affect all the subscribers for an event class. The second, subscriber filtering, affects only the subscriber using that filter.
Both kinds of filters usually let you filter events without changing the publisher or the subscriber code. However, I find that event filtering is either cumbersome to use and implement, or limited and incomplete in what it offers. Those shortcomings are mitigated by the use of the COM+ Catalog wrapper object.
Publisher filtering is a powerful mechanism that gives the publisher fine-grained control over event delivery. You can use a filter to publish to only certain subscribers, control the order in which subscribers get an event, and find out which subscribers did not get an event or had encountered an error processing it. The publisher-side filter intercepts the call the publisher makes to the event class, applies filtering logic on the call, and performs the actual publishing (see Figure 9-8).
If you associate a filter with an event class, all events published using that class go through the filter first. You are responsible for implementing the filter (you will see how shortly) and to register it in the COM+ Catalog. The publisher filter CLSID is stored in the COM+ Catalog as a property of the event class that it filters. At any given time, an event class has at most one filter CLSID associated with it. As a result, installing a new filter overrides the existing one.
When a publisher fires events on the event class, COM+ creates the publisher object and lets it perform the filtering.
A publisher-side
filter is a COM object that implements an interface called
IMultiInterfacePublisherFilter
. The filter need not necessarily be a
COM+ configured component. The filter interface name contains the
word Multi because it filters all the events fired on all the
interfaces of the event class. Another interface, called
IPublisherFilter
, allows you to associate a filter
with just one sink interface supported by an event class. It is still
mentioned in the documentation, but has been deprecated (i.e.,
don’t use it).
The definition for IMultiInterfacePublisherFilter
is:
interface IMultiInterfacePublisherFilter : IUnknown { HRESULT Initialize([in]IMultiInterfaceEventControl* pMultiInterfaceEventControl); HRESULT PrepareToFire([in]IID* piidSink,[in]BSTR bstrMethodName, [in]IFiringControl* pFiringControl); }
Only COM+ calls the methods of
IMultiInterfacePublisherFilter
as part of the
event publishing sequence. If an event class has a publisher filter
object associated with it, COM+ CoCreates the filter object and calls
the Initialize( )
method when the
publisher CoCreates the event class.
Each time the publisher fires an event at the event class, instead of
publishing the event to the subscribers, COM+ calls the
PrepareToFire( )
method and lets you do the filtering. When the publisher releases the
event class, COM+ releases the filter object.
When the Initialize( )
method is called, COM+
passes in as a parameter an interface pointer of type
IMultiInterfaceEventControl
, defined as:
interface IMultiInterfaceEventControl : IUnknown { HRESULT GetSubscriptions( [in] IID* piidSink, [in] BSTR bstrMethodName, [in] BSTR bstrCriteria, [in] int* nOptionalErrorIndex, [out, retval] IEventObjectCollection** ppCollection); //Other methods }
The only method of IMultiInterfaceEventControl
relevant to publisher-side
filtering is GetSubscriptions( )
,
used to get the list of subscribers at the time the event is
published. Since COM+ calls the Initialize( )
method only once, you should cache the
IMultiInterfaceEventControl
pointer as a member
variable of the filter object.
The actual filtering work is performed in the
scope of the PrepareToFire( )
method. The first
thing you need to do in the PrepareToFire( )
method is call the
IMultiInterfaceEventControl::GetSubscriptions( )
method, passing an initial filtering criteria in as a parameter.
Filtering criteria are mere optimizations—a filter is used to inspect subscribers, and the filter may provide COM+ with an initial criterion of which subscribers to even consider for publishing.
The criterion is a BSTR containing some information about the subscribers. For example, consider a filtering criterion of the form:
_bstr_t bstrCriteria = "EventClassID == {F89859D1-6565-11D1-88C8-0080C7D771BF} AND MethodName = "OnNewOrder"";
This causes COM+ to retrieve only subscribers that have subscribed to
the specified event class and for the method called
OnNewOrder
on one of the event class interfaces.
Another example of a criterion is ALL
, meaning
just get all the subscribers. See the
IMultiInterfaceEventControl
documentation for more
information on the exact criteria syntax.
GetSubscriptions( )
returns an interface pointer
of type
IEventObjectCollection
, which you use to access the
subscribers collection.
Next, you call IEventObjectCollection::get_NewEnum( )
to get an enumerator of type
IEnumEventObject
to iterate over the subscribers
collection. While you iterate, you get one subscriber at a time in
the form of
IEventSubscription
. You retrieve the
IEventSubscription
properties (such as the
subscriber’s name, description, IID), apply filtering logic,
and decide if you want to publish to that subscriber. If you want to
fire the event at that subscriber, use the last parameter passed to
PrepareToFire( )
, a pointer of type
IFiringControl
, passing in the Subscriber interface:
pFiringControl->FireSubscription(pSubscription);
At this point, you also get the exact success code of publishing to that particular subscriber. You then release the current subscriber and continue to iterate over the subscription collection.
If you want to publish to the subscribers in a different order than the one in which COM+ handed them to you, you should iterate over the entire collection, copy the subscribers to your own local list, sort the list to your liking, and then fire.
By now, you probably feel discouraged from implementing a
publisher-side filter. The good news is that the filtering plumbing
is generic, so I was able to implement all of it in an ATL COM object
called CGenericFilter
. CGenericFilter
performs the messy interaction with the COM+ event system required of
a publisher filter. All you have to do is provide the filtering logic
(which is what a filter should do).
As part of the source files available with this book at
O’Reilly’s web site, you will find the Filter
project—an ATL project containing the implementation of the
CGenericFilter
class.
CGenericFilter
lets you control which subscribers
to publish to. If you want a different filter, such as one that
controls the publishing order, you can implement that filter
yourself, using the source files as a starting point.
The CGenericFilter
class definition is (with some
code omitted for clarity):
class CGenericFilter: public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CGenericFilter,&CLSID_MyFilter>, public IMultiInterfacePublisherFilter { public: CGenericFilter( ); void FinalRelease( ); BEGIN_COM_MAP(CGenericFilter) COM_INTERFACE_ENTRY(IMultiInterfacePublisherFilter) END_COM_MAP( ) //IMultiInterfacePublisherFilter STDMETHOD(Initialize)(IMultiInterfaceEventControl* pMultiEventControl); STDMETHOD(PrepareToFire)(IID* piidSink, BSTR bstrMethodName, IFiringControl* pFiringControl); //Helper methods, used for domain logic specific filtering HRESULT ExtractSubscriptionData(IEventSubscription* pSubscription, SubscriptionData* pSubscriptionData)const; BOOL ShouldFire(const SubscriptionData& subscriptionData)const; _bstr_t GetCriteria( )const; IMultiInterfaceEventControl* m_pMultiEventControl; };
The only thing you have to provide is the application domain-specific
filtering logic, encapsulated in the two simple helper methods:
CGenericFilter::ShouldFire( )
and CGenericFilter::GetCriteria( )
. The
CGenericFilter
implementation calls
GetCriteria( )
once per event to allow you to
provide a
filtering criteria. The
default implementation returns ALL
:
_bstr_t CGenericFilter::GetCriteria( )const { _bstr_t bstrCriteria = "ALL";//ALL means all the subscribers, //regardless of event classes return bstrCriteria; }
CGenericFilter::ShouldFire( )
is the most
interesting method here. CGenericFilter
calls the
method once per subscriber for a particular event. It passes in as a
parameter a custom struct of type
SubscriptionData
, which contains every available bit
of information about the subscriber—including the name,
description, and machine name:
struct SubscriptionData { _bstr_t bstrSubscriptionID; _bstr_t bstrSubscriptionName; _bstr_t bstrPublisherID; _bstr_t bstrEventClassID; _bstr_t bstrMethodName; _bstr_t bstrOwnerSID; _bstr_t bstrDescription; _bstr_t bstrMachineName; BOOL bPerUser; CLSID clsidSubscriberCLSID; IID iidSink; IID iidInterfaceID; };
ShouldFire( )
examines the subscriber and returns
TRUE
if you wish to publish to this subscriber or
FALSE
otherwise.
An example for implementing filtering logic in ShouldFire( )
is to publish only to subscribers whose description field
in the Component Services Explorer says Paid Extra. See Example 9-2.
Example 9-2. Base your implementation of Shouldfire() on the information in SubscriptionData
BOOL CGenericFilter::ShouldFire(const SubscriptionData& subscriptionData)const { if(subscriptionData.bstrDescription == _bstr_t("Paid Extra")) return TRUE; else return FALSE; }
Finally, Example 9-3 shows the
CGenericFilter
implementation of
PrepareToFire( )
, which contains all the
interaction with the COM+ event system outlined previously; some
error-handling code was removed for clarity.
Example 9-3. CGenericFilter implementation of PrepareToFire( )
STDMETHODIMP CGenericFilter::PrepareToFire(IID* piidSink, BSTR bstrMethodName, IFiringControl* pFiringControl) { HRESULT hres = S_OK; DWORD dwCount = 0; IEnumEventObject* pEnum = NULL; IEventSubscription* pSubscription = NULL; IEventObjectCollection* pEventCollection = NULL; _bstr_t bstrCriteria = GetCriteria( );//You provide the criteria hres = m_pMultiEventControl->GetSubscriptions(piidSink, bstrMethodName, bstrCriteria,NULL, &pEventCollection); //Iterate over the subscribers, and filter in this example by name hres = pEventCollection->get_NewEnum(&pEnum); pEventCollection->Release( ); while(TRUE) { hres = pEnum->Next(1,(IUnknown**)&pSubscription,&dwCount); if(S_OK != hres) { //Returns S_FALSE when no more items if(S_FALSE == hres) { hres = S_OK; } break; } long bEnabled = FALSE; hres = pSubscription->get_Enabled(&bEnabled); if(FAILED(hres) || bEnabled == FALSE) { pSubscription->Release( ); continue; } SubscriptionData subscriptionData; subscriptionData.iidSink = *piidSink; //A helper method for retrieving all of the subscription //properties and packaging them in the handy SubscriptionData hres = ExtractSubscriptionData(pSubscription,&subscriptionData); if(FAILED(hres)) { pSubscription->Release( ); continue; } //You provide the filtering logic in ShouldFire( ) BOOL bFire = ShouldFire(subscriptionData); if(bFire) { pFiringControl->FireSubscription(pSubscription); } pSubscription->Release( ); } pEnum->Release( ); return hres; }
Again, let me emphasize that all you have to provide is the filtering
logic in ShouldFire( )
and GetCriteria( )
; let CGenericFilter
do the hard work
for
you.
What
begs an answer now (as I am sure you
have already wondered) is why is PrepareToFire( )
called “Prepare” if
the event is fired there? Why not just call it Fire( )
? It is called Prepare to support filtering based on the
event parameters as well. In PrepareToFire( )
,
COM+ only tells you what event is fired.
What if you need to examine the actual event parameters to make a sound decision on whether or not you want to publish? In that case, the publisher filter can implement the same sink interfaces as the event class it is filtering.
After calling PrepareToFire( )
, COM+ queries the
filter object for the sink interface. If the filter supports the
event interface, COM+ only fires to the filter. The filter should
cache the information from PrepareToFire( )
and
perform the fine-tuned parameters-based filtering. In its
implementation of the sink method, it uses
IFireControl
to fire the event to the client.
Publisher-side filters usually base their filtering logic on the standard subscription properties—the subscription name, description, and so on. These properties are pre-defined and are available for every subscription. COM+ also allows you to define new custom properties for subscriptions and assign values to these properties, to be used by the publisher filter. Usually, you can take advantage of custom properties if you develop both the subscribing component and the publisher filter. You can define custom subscription properties administratively only for persistent subscribers.
To define a new custom property, display the subscription properties page, and select the Publisher Properties tab (the name is misleading). You can click the Add button to define a new property and specify its value (see Figure 9-9).
Transient subscribers have to program against the
component COM+ Catalog. Get hold of the transient subscription
collection, find your transient subscription catalog object, and
navigate from it to the
PublisherProperties
collection. You can then add or
remove custom properties in the collection.
As explained before, when the publisher filter iterates over the
subscription collection, it gets one subscriber at a time in the form
of an
IEventSubscription
interface pointer. The filter can call
IEventSubscription::GetPublisherProperty( )
,
specify the custom property name, and retrieve its value.
For example, here is how you retrieve a custom subscriber property
called Company Name
:
_bstr_t bstrPropertyName = "Company Name"; _variant_t varPropertyValue; hres = pSubscription->GetPublisherProperty(bstrPropertyName,&varPropertyValue);
If the subscriber does not have this property defined,
GetPublisherProperty( )
returns
S_FALSE
. You can even define method parameter
names as custom properties and specify a value or range in the
property data. If the filter is doing parameters-based filtering, it
can be written to parse the custom property value and to publish to
that subscriber only when the parameter value is in that range.
There are two ways for associating a publisher filter with an event class. In the absence of any names for these two ways from the COM+ team at Microsoft, I call the first static association and the second dynamic association.
Static association requires you to program against the COM+ Catalog and store the filter CLSID as a property of the event class. The filter will stay there until you remove it or override it with another CLSID. Static association affects all publishers that use that event class, in addition to all instances of the event class.
Dynamic association takes place at runtime. The publisher will not only create an event class, but also directly creates a filter object and associates it only with the instance of the event class it currently has. Dynamic association affects only the publishers that use that particular instance of the event class. Dynamic association does not persist beyond the lifetime of the event class object. Once you release the event class, the association is gone. Dynamic association allows a publisher to bind a particular instance of an event class with a particular instance of a filter; it overrides any static filter currently installed.
The main disadvantage of dynamic association is that you cannot dynamically associate a filter with an instance of a queued event class (discussed later on), since you are interacting with the recorder for the event class, not the event class itself.
To statically associate a publisher filter CLSID with the event class you want it to filter, you have to follow these steps:
Create the catalog object.
Get the Applications
collection.
For each application in the collection, get the
Components
collection.
Iterate through the Components
collection looking
for the event class. If the class is not found, get the next
Application
collection and scan its
Components
collection.
Once you find the event class, set the
MultiInterfacePublisherFilterCLSID
event class property to the CLSID of
the filter.
Save changes on the Components
collection and
release everything.
Again, the Catalog
wrapper helper object is useful, as it
saves you the interaction with the COM+ Catalog. The helper object
implements an interface called IFilterInstaller
,
defined as:
interface IFilterInstaller : IUnknown { HRESULT Install([in]CLSID clsidEventClass,[in]CLSID clsidFilter); HRESULT Remove ([in]CLSID clsidEventClass); };
IFilterInstaller
makes adding a filter a
breeze—just specify the CLSID of the event class and the CLSID
of the filter, and it will do the rest for you:
HRESULT hres = S_OK; hres = ::CoCreateInstance(CLSID_CatalogWrapper,NULL,CLSCTX_ALL, IID_IFilterInstaller,(void**)&pFilterInstaller); hres = pFilterInstaller->Install(CLSID_MyEventClass,CLSID_MyFilter); pFilterInstaller->Release( );
Note that you do not need to specify the application name as a
parameter; just specify the event class and the filter CLSID. Use
IFilterInstaller::Remove( )
to remove any filter
associated with a specified event class.
To associate a publisher filter object with an event class instance dynamically, follow these steps:
Create the event class and get the sink interface.
Query the event class for
IMultiInterfaceEventControl
interface.
Create the filter object.
Call
IMultiInterfaceEventControl::SetMultiInterfacePublisherFilter( )
and pass in the filter object.
Release IMultiInterfaceEventControl
.
Publish events to the event class object. The events will go through the filter you have just set up.
Release the event class and the filter when you are done publishing.
Example 9-4 shows some sample code that uses this technique.
Example 9-4. Installing a publisher-side filter dynamically
HRESULT hres = S_OK; IMySink* pMySink = NULL; IMultiInterfacePublisherFilter* pFilter = NULL; IMultiInterfaceEventControl* pEventControl = NULL; //Create the filter hres = ::CoCreateInstance(CLSID_MyFilter,NULL,CLSCTX_ALL, IID_IMultiInterfacePublisherFilter,(void**)&pFilter); //Create the event class hres = ::CoCreateInstance(CLSID_MyEventClass,NULL,CLSCTX_ALL, IID_IMySink,(void**)&pMySink); //Query the event class for IMultiInterfaceEventControl hres = pMySink ->QueryInterface(IID_IMultiInterfaceEventControl, (void**)pEventControl); //Setting the filter hres = pEventControl->SetMultiInterfacePublisherFilter(pFilter); pEventControl->Release( ); //Firing the event hres = pMySink->OnEvent1( );//The event is now filtered pMySink->Release( ); pFilter->Release( );
Unfortunately, COM+ has a bug regarding correct handling of
dynamically associating a publisher filter with an event class. COM+
does not call the filter method
IMultiInterfacePublisherFilter::Initialize( )
, and
as a result, you can’t do much filtering. I hope this situation
will be fixed in a future release of COM+.
This defect, plus dynamic association’s inability to work with queued event classes, renders it effectively useless. Avoid dynamic association of a publisher filter; use static association instead.
Not all subscribers have meaningful operations to do as a response to every published event. Your subscriber may want to take action only if your favorite stock is trading, or perhaps only if it is trading above a certain mark. One possible course of action is to accept the event, examine the parameters and decide whether to process the event or discard it.
However, this action is inefficient if the subscriber is not interested in the event for the following reasons:
It forces a context switch to allow the subscriber to examine the event.
It adds redundant network round trips.
Writing extra examination code may introduce defects and require additional testing.
Event examination and processing policies change over time and between customers. You will chase your tail trying to satisfy everybody.
What you should really do is to put the filtering logic outside the scope of the subscriber. You should have an administrative, configurable, post-compilation, deployment-specific filtering ability. This is exactly what subscriber-side filtering is all about (see Figure 9-10). Subscribers that do not want to be notified of every event published to them, but want to be notified only if an event meets certain criteria, can specify filtering criteria.
A subscriber-side filter is a string containing the filtering criteria. For example, suppose you subscribe to an event notifying you of a new user in your portfolio management system, and the method signature is:
HRESULT OnNewUser([in]BSTR bstrName,[in]BSTR bstrStatus);
You can specify such filtering criteria as:
bstrName = "Bill Gates" AND bstrStatus = "Rich"
The event will only be delivered to your object if the username is Bill Gates and his current status is Rich.
The filter criteria string recognizes relational operators for
checking equality (==
,!=
),
nested parentheses, and logical keywords AND
,
OR
, and NOT
. COM+ evaluates the
expression and allows the call through only if the criteria are
evaluated to be true.
If you have wrong parameters or spelling mistakes, or if the parameter names were changed, the subscriber will never be notified.
Because subscriber-side filtering occurs only after the event has been fired, if a publisher filter is used, then the event has to pass the publisher filter first. The obvious conclusion is that publisher-side filtering takes precedence over subscriber-side filtering.
Only persistent subscribers can specify a subscriber filter administratively. They can do so by displaying the persistent subscription properties page, selecting the Options tab, and specifying the Filter criteria (see Figure 9-11).
Transient subscribers have to program against the Catalog to set a transient subscription filter criteria, following similar steps to those performed when registering a transient subscription:
The Catalog wrapper’s interface
ITransientSubscription
, discussed earlier, allows you to add (or
remove) a subscriber-side filter to a transient subscription with the
AddFilter( )
and
RemoveFilter( )
methods. The methods accept the subscription name and a filtering
string.
Example 9-5 demonstrates the same example from the persistent subscriber filter, but for a transient subscriber for the same event.
Example 9-5. Adding a transient subscription filtering criteria using the wrapper object
//Adding a transient subscription filter: LPCWSTR pwzCriteria = L"bstrUser = "Bill Gates" AND bstrStatus = "Rich"" //"MySubs" is the transient subscription name hres = pTransSubs->AddFilter(L"MySubs",pwzCriteria); //Or removing the filter: pTransSubs ->RemoveFilter(L"MySubs");
The main disadvantage of a transient subscriber filter compared to a persistent subscriber filter is that you hardcode a filter, which is sometimes deployment- or customer-specific. Persistent subscribers can always change the filtering criteria using the Component Services Explorer during deployment.