Exposing COM events from .NET: Implementing MapSurround in ArcMap
Recently I needed to implement a custom map surround object in ArcMap from within the .NET framework environment. The legend, scale bar, north arrow are all examples of a map surround. What is specific to these objects is that they are associated with a particular map (data frame) and are typically created via IMap.CreateMapSurround method. All map surround objects extend the MapSurround COM abstract class, which is comprised of these interfaces:
- IBoundsProperties
- IClone
- IConnectionPointContainer
- IMapSurround
- IMapSurroundEvents (source interface)
- IPersist
- IPersistStream
Clearly, some of these interfaces (cloning and persistence) are not as interesting as the others. In this article I'll focus mainly on the event supporting interfaces, as these are (arguably) the hardest to implement. Unfortunately, the ArcObjects documentation and library reference provide virtually no guidelines on approaching this particular problem. I can only speculate this stems from the fact that this kind of task was not possible in VB6 at all and since the .NET documentation seems to be mostly converted from the VB6 docs with some additional .NET-specific topics, it was simply left out.
Chances are this article will be also useful outside the ESRI stack world as it deals with some of COM interop issues in a generic fashion.
COM source interfaces as .NET events
Let me first elaborate on how COM source interfaces are converted into the .NET event/delegate model. This task is done through the Type Library Importer utility (tlbimp.exe) which basically takes a path to a type library and produces a .NET assembly. It has various switches to fine-tune the conversion process, specify output namespace and also allows the resulting assembly to be strong named (this is how ESRI's primary interop assemblies we reference in our projects are produced). There is an article on ESRI Developer Network which describes how COM to .NET conversion is done, and I suspect every ArcObjects developer has at some point read this through.
So, let's take a look at our IMapSurroundEvents interface. The importer takes these steps in order to support the event model:
- imports the IMapSurroundEvents interface
- for each method in the above interface, creates a delegate
named IMapSurroundEvents_methodnameEventHandler, ie.
- IMapSurroundEvents_AfterDrawEventHandler
- IMapSurroundEvents_BeforeDrawEventHandler
- IMapSurroundEvents_ContentsChangedEventHandler
- creates IMapSurroundsEvents_Event interface, which contains an event for every method (typed by its matching delegate); this interface is used to sink COM events in managed code
Launching Reflector on the ESRI.ArcGIS.Carto assembly, however, reveals two additional items which are tightly tied to the event interop:
- internal sealed class IMapSurroundEvents_EventProvider : IMapSurroundEvents_Event, IDisposable
- public sealed class IMapSurroundEvents_SinkHelper : IMapSurroundEvents
Turns out that the Type Library Importer does some additional work under the hood. Before we delve into what these two classes are for and why are they needed, some explanation of the pure COM event model is inevitable.
Introducing IConnectionPointContainer
First, those who are familiar with COM in C++ and alike can safely skip this section as it is intended primarily on folks coming from the VB background. Later on, we will be implementing IConnectionPointContainer in .NET.
In COM, an object providing outgoing interfaces implements IConnectionPointContainer, which is just one of the few interfaces supporting COM events. Obviously, the best place to look for detailed information is the MSDN – see Events in COM and Connectable Objects. Thorough description lies outside the scope of this post, I'll only present and describe the basic building blocks of this architecture and their interaction:
- IConnectionPointContainer lets the clients know that the object is connectable and provides outgoing interfaces. The client is able to find a connection point by specific outgoing interface's IID (FindConnectionPoint) or enumerate over all the available connection points (EnumConnectionPoints)). That said, map surround objects provide one connection point (for the IMapSurroundEvents outgoing interface).
- IConnectionPoint represents a connection point for a particular outgoing interface. Its purpose is to maintain connections to objects (sinks) which receive events. These sink objects (listeners) implement methods of the outgoing interface, which the connectable object (map surround in our case) calls when an event arises. Clients register the sink via Advise method and unregister by calling Unadvise. Advise method takes a reference to the sink object and returns an integer identifier (called cookie), which is later passed to Unadvise. Clients can also examine all „advised“ connections using EnumConnections.
So the typical scenario goes along these lines:
- The client casts the connectable object to IConnectionPointContainer and uses it FindConnectionPoint method using a particular outgoing interface ID (e.g. IMapSurroundEvents' IID). This returns an IConnectionPoint reference.
- The client uses Advise method with the sink object as an argument. This object implements the event interface methods. The connectable object stores reference to the sink and generates an integer cookie, which then client stores.
- Now, when an event is to be raised, the connectable object uses the connection point to go through all the connection points. For every connection point's sink object, the particular event's method is called.
- Later on, the client unregisters the sink via Unadvise method, using the previously stored cookie.
As you see, this is pretty simple, but still involved enough so that a more convenient way to work with events would come handy. In fact, this is exactly what good old Visual Basic does – it has the WithEvents keyword which hides this process altogether. In .NET/C#, the effect is very similar, but it certainly doesn't hurt to see how it's exactly done, as I'll describe in the next section.
Sinking COM events in managed code
With the information from the above section, it becomes clear that .NET has a bit more work to do than merely importing the necessary interaces – it has to somehow interlink the COM infrastructure and its delegate/event approach. As you would now guess, this is exactly where the IMapSurroundEvents_EventProvider and IMapSurroundEvents_SinkHelper classes come into play.
A comprehensive explanation of how these two classes work can be found in a great MSDN blog post How do we talk with COM the language of events and delegates. While I encourage you to take a look at the article, I will try to cover the basics since this will be needed for better understanding the next sections.
IMapSurroundEvents_SinkHelper implements the event source interface and instances of this class are passed to the aforementioned Advise method. It maintains the generated delegates, which are invoked upon calling the specific event interface methods (provided the delegate is not null, i.e., there are some observers subscribing for the event). Sink helpers are effectively event sinks which bridge the COM event caller and .NET event receiver endpoint.
Now, we need a means to link the event source (IConnectionPointContainer) to the sink helper instances and, in turn, the sink helpers to their event recipients, which is exactly the job of _EventProvider class. This class implements the _Event interface (IMapSurroundEvents_Event), which as we have seen before contains all the events. Events in .NET are typically subscribed using the += operator (and unsubscribed using =-) – and that's just about it, you do not need to maintain the list of subscribers yourself, the compiler does that for you. But you may happen to know there is also another way – that is, managing the list of recipients „manually“ through so called event accessors. The need to do so rarely arises in a day-to-day development though, if ever. This is exactly what the _EventProvider class does – it publishes .NET events, but does not maintain the list directly – instead, it keeps a list of the event sink helpers we have talked about. When you use „+=“ in your program over such event, say IMapSurroundEvents_Event.AfterDraw, the event add accessor is called and several things happen behind the scenes:
- The event provider gets and stores the IConnectionPoint from the wrapped IConnectionPointContainer if this has not been done before (the link to the connection point container is provided by the runtime) .
- Creates new instance of IMapSurroundEvents_SinkHelper is created, added to the internal list, and its IMapSurroundEvents_AfterDrawEventHandler delegate is set appropriately.
- Calls Advise with the newly created sink helper as its parameter. In this step, the obtained sink cookie is also stored with the sink helper.
Using the -="operator has analogous effects: the event' remove accessor calls Unadvise and removes the sink helper from the internal list. Apart from this, the event provider also manages cleanup of objects and COM references (it implements IDisposable). Details along with code excerpts can be found in the article linked above.
Events the other way round: exposing .NET events to COM
Now that we understand how COM events can be propagated to .NET clients, we can happily rush into solving the opposite: publishing events so that the COM clients can consume them. This can be fairly easy in some general cases. Say we have a component which provides an event called OnDone fired after something is, well, done. We also have to define a delegate for the event (or use one already existing in .NET if it suits our needs). Consider this code sample:
[ComVisible(false)]
public delegate void OnDoneEventHandler(object sender, EventArgs e);
// Outgoing (source/event) interface.
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[Guid(...)]
public interface ISomeEventInterface
{
[DispId(1)]
void OnDone(object sender, EventArgs e);
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(ISomeEventInterface))]
[ProgId(...)]
[Guid(...)]
public class SomeComClass :
{
public event OnDoneEventHandler OnDone;
internal void FireOnDone(object sender, EventArgs e)
{
if (OnDone != null)
{
OnDone(sender, e);
}
}
}
This is the usual way to publish .NET events to COM, and it works; you can easily sink the OnDone event in VBA/VB6 and other environments. Both .NET and COM clients are able to subscribe for the event seamlessly.
IUnknown – the trouble with IMapSurroundEvents
Too bad we cannot use this approach when implementing our own map surrounds, and here's why. You may have noticed ISomeEventInterface is attributed as IDispatch, but the event interface we need to implement, IMapSurroundEvents, derives from IUnknown, thus the dispatch mechanism (invoking methods by their dispatch ids or by their names) cannot be used at all. We obviously do not have control over ESRI interfaces, we cannot redefine them in any way. So, we'll have to make do by taking a different path and that is, implementing the IConnectionPointContainer and related interfaces all by ourselves.
Where are the COM event infrastructure interfaces defined
Before we implement the necessary interfaces, we need to know where they're defined along with some structures they use. This depends on your version of .NET framework. In .NET 2.0 and later, these types are in System.Runtime.InteropServices.ComTypes namespace. In .NET 1.0/1.1 they are directly in System.Runtime.InteropServices and are names a bit differently, prefixed with UCOM (for example UCOMIConnectionPointContainer). These are marked obsolete in versions from 2.0 up, so from now on I'll be only referring to the newer versions of these interfaces.
Now, let's look at the IEnumConnections.Next method's signature:
int Next (
int celt,
[OutAttribute] CONNECTDATA[] rgelt,
IntPtr pceltFetched
)
This is the main method of the enumerator, it fills the rgelt CONNECTDATA array (which the client sizes to celt elements prior to making the call) and stores the actual number of enumerated elements into pceltFetched. At as far as CONNECTDATA structure is concerned, the only thing we need to know at this point is that it stores the pointer to the event sink and the connection cookie.
If we point Reflector at this interface, here's what we will see:
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("B196B287-BAB4-101A-B69C-00AA00341D07")]
public interface IEnumConnections
{
[PreserveSig]
int Next(int celt, [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1)] CONNECTDATA[] rgelt, IntPtr pceltFetched);
[PreserveSig]
int Skip(int celt);
void Reset();
void Clone(out IEnumConnections ppenum);
}
Notice the SizeParamIndex marshalling parameter. It says that, during marshalling, the runtime should read the rgelt array size from parameter at index 1. Quoting the MSDN reference on SizeParamIndex:
Indicates which parameter contains the count of array elements, much like size_is in COM, and is zero-based.
The documentation clearly states that the index is zero-based, which with the value of 1 points at the array parameter itself, i.e., it should be 0 instead, pointing at the celt parameter. Thus I believe the definition in mscorlib.dll is incorrect and indeed, any attempts to use this method at runtime will result in a marshalling exception to be thrown. Strangely enough, this bug seems to be introduced in .NET 2.0 because the original (now obsolete) IUCOM_xxxx interfaces in .NET 1.1 do not appear to be incorrect:
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("B196B287-BAB4-101A-B69C-00AA00341D07"), Obsolete("Use System.Runtime.InteropServices.ComTypes.IEnumConnections instead. http://go.microsoft.com/fwlink/?linkid=14202", false)]
public interface UCOMIEnumConnections
{
[PreserveSig]
int Next(int celt, [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] CONNECTDATA[] rgelt, out int pceltFetched);
[PreserveSig]
int Skip(int celt);
[PreserveSig]
void Reset();
void Clone(out UCOMIEnumConnections ppenum);
}
This affects not only IEnumConnections.Next but also IEnumConnectionPoints.Next, for the very same reason, i.e. the SizeParamIndex being incorrectly set to 1. I have absolutely no idea how this bug could be introduced and have not been able to get any feedback on this. (It is even possible that it is not in fact a bug, just my poor understanding of the matter. If someone could please confirm or clarify this, I would be more than grateful.)
There are two possible options to work around this. First, we could use the obsolete interface imports. Second choice is to redefine the imports correctly and use our definitions instead of those found in System.Runtime.InteropServices.ComTypes. I opt for the latter because I do not like compiler warnings being spat on me. Also, you can never be sure whether obsolete classes/interfaces won't disappear in future versions of the framework without further notice.
Below are the final interface definitions, after being simply taken from Reflector's output and with the corrections discussed above applied to them. Because the interfaces are tightly tied together, it is best to redefine all of them to avoid any unpleasant confusing mix-ups. The only thing we will keep from the Microsoft's namespace is the CONNECTDATA structure.
namespace MyCOMInterfaceDefinitions
{
using System;
using System.Runtime.InteropServices;
using CONNECTDATA=System.Runtime.InteropServices.ComTypes.CONNECTDATA;
[ComImport, Guid("B196B285-BAB4-101A-B69C-00AA00341D07"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IEnumConnectionPoints
{
[PreserveSig]
int Next(int celt, [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IConnectionPoint[] rgelt, IntPtr pceltFetched);
[PreserveSig]
int Skip(int celt);
void Reset();
void Clone(out IEnumConnectionPoints ppenum);
}
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("B196B287-BAB4-101A-B69C-00AA00341D07")]
public interface IEnumConnections
{
[PreserveSig]
int Next(int celt, [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] CONNECTDATA[] rgelt, IntPtr pceltFetched);
[PreserveSig]
int Skip(int celt);
void Reset();
void Clone(out IEnumConnections ppenum);
}
[ComImport, Guid("B196B286-BAB4-101A-B69C-00AA00341D07"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IConnectionPoint
{
void GetConnectionInterface(out Guid pIID);
void GetConnectionPointContainer(out IConnectionPointContainer ppCPC);
void Advise([MarshalAs(UnmanagedType.Interface)] object pUnkSink, out int pdwCookie);
void Unadvise(int dwCookie);
void EnumConnections(out IEnumConnections ppEnum);
}
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("B196B284-BAB4-101A-B69C-00AA00341D07")]
public interface IConnectionPointContainer
{
void EnumConnectionPoints(out IEnumConnectionPoints ppEnum);
void FindConnectionPoint([In] ref Guid riid, out IConnectionPoint ppCP);
}
}
Implementing the interfaces
With the correctly defined interface imports in place, it's about time to get our hands dirty, implement them and finally have some fun. I rolled a set of helper objects which will aid us in supporting the COM events infrastructure. See below (I'll discuss the code later in detail) :
/// <summary>
/// The event container helper. Objects which wish to implement <see cref="IConnectionPointContainer"/> maintain
/// an instance of this class and delegate all calls to the interface methods to that object.
/// </summary>
public class EventContainerHelper : IConnectionPointContainer
{
private readonly IList<eventHelper> eventHelpers = new List<eventHelper>();
private readonly IDictionary<guid, IConnectionPoint> guidToConnectionPoint = new Dictionary<guid, IConnectionPoint>();
private readonly ConnectionPointList connectionPoints = new ConnectionPointList();
private readonly IConnectionPointContainer connectionPointContainer;
/// <summary>
/// Creates a new instance of the event container helper.
/// </summary>
/// <param name="connectionPointContainer">The connection point container. All calls to that connection
/// point container are to be delegated to this newly created instance of <see cref="NetComEventsTest.EventContainerHelper"/>.
/// </param>
public EventContainerHelper(IConnectionPointContainer connectionPointContainer)
{
if (connectionPointContainer == null) throw new ArgumentNullException("connectionPointContainer");
this.connectionPointContainer = connectionPointContainer;
}
/// <summary>
/// Adds a new event interface to which this helper should react.
/// </summary>
/// <typeparam name="NETEventInterface">The .NET event interface which the type library importer
/// creates for a COM event source interface.</typeparam>
/// <returns>An event helper which can be used to raise events on the specified event interface.</returns>
public EventHelper<neteventInterface> AddEvents<neteventInterface>()
{
EventHelper<neteventInterface> eventHelper =
new EventHelper<neteventInterface>(connectionPointContainer);
eventHelpers.Add(eventHelper);
guidToConnectionPoint.Add(eventHelper.ComEventInterfaceType.GUID, eventHelper);
connectionPoints.Add(eventHelper);
return eventHelper;
}
#region Implementation of IConnectionPointContainer
public void EnumConnectionPoints(out IEnumConnectionPoints ppEnum)
{
ppEnum = connectionPoints;
}
public void FindConnectionPoint(ref Guid riid, out IConnectionPoint ppCP)
{
ppCP = guidToConnectionPoint.ContainsKey(riid) ? guidToConnectionPoint[riid] : null;
}
#endregion
}
/// <summary>
/// The list of connection points. This class is used in <see cref="EventContainerHelper"/> and serves
/// merely to implement the <see cref="IEnumConnectionPoints"/> interface.
/// </summary>
internal class ConnectionPointList : IEnumConnectionPoints
{
private IList<iconnectionPoint> connectionPoints = new List<iconnectionPoint>();
private int currentEnumIndex;
/// <summary>
/// Adds a connection point to the list.
/// </summary>
/// <param name="connectionPoint">The connection point.</param>
public void Add(IConnectionPoint connectionPoint)
{
if (connectionPoint == null) throw new ArgumentNullException("connectionPoint");
connectionPoints.Add(connectionPoint);
}
#region Implementation of IEnumConnectionPoints
public int Next(int celt, IConnectionPoint[] rgelt, out int pceltFetched)
{
int fetched = 0;
for (int i = currentEnumIndex; i < connectionPoints.Count; i++)
{
rgelt[fetched] = connectionPoints[i];
fetched = fetched + 1;
if (fetched == celt) break;
}
currentEnumIndex = currentEnumIndex + fetched;
pceltFetched = fetched;
return fetched == celt ? 0 : 1;
}
public int Skip(int celt)
{
currentEnumIndex += celt;
return currentEnumIndex < connectionPoints.Count ? 0 : 1;
}
public void Reset()
{
currentEnumIndex = 0;
}
public void Clone(out IEnumConnectionPoints ppenum)
{
ConnectionPointList clone = new ConnectionPointList();
clone.connectionPoints = connectionPoints;
clone.currentEnumIndex = currentEnumIndex;
ppenum = clone;
}
#endregion
}
/// <summary>
/// Base event helper class.
/// </summary>
public abstract class EventHelper
{
}
/// <summary>
/// The event helper class. This class aids in publishing .NET events to COM via connection points
/// infrastructure.
/// </summary>
/// <typeparam name="NETEventInterface">The .NET event interface (associated with a COM event source interface)
/// created by the type library importer.</typeparam>
public class EventHelper<neteventInterface> : EventHelper, IConnectionPoint
{
private readonly IDictionary<type, MethodInfo> delegatesToMethods = new Dictionary<type, MethodInfo>();
private readonly ConnectionList observers = new ConnectionList();
private readonly IConnectionPointContainer connectionPointContainer;
private readonly Type comEventInterfaceType;
/// <summary>
/// Creates a new instance of the event helper class.
/// </summary>
/// <param name="connectionPointContainer">The connection point container.</param>
public EventHelper(IConnectionPointContainer connectionPointContainer)
{
if (connectionPointContainer == null) throw new ArgumentNullException("connectionPointContainer");
// find the COM event interface associated with the NET event interface
Type netEventsType = typeof(NETEventInterface);
foreach (object attribute in netEventsType.GetCustomAttributes(typeof(ComEventInterfaceAttribute), false))
{
ComEventInterfaceAttribute comEventInterfaceAttribute = (ComEventInterfaceAttribute)attribute;
comEventInterfaceType = comEventInterfaceAttribute.SourceInterface;
break;
}
if (comEventInterfaceType == null)
{
throw new ArgumentException("The type parameter is not a .NET event interface corresponding to a COM event interface.");
}
foreach (MethodInfo methodInfo in comEventInterfaceType.GetMethods())
{
EventInfo eventInfo = netEventsType.GetEvent(methodInfo.Name);
if (eventInfo == null || eventInfo.EventHandlerType == null) continue;
delegatesToMethods.Add(eventInfo.EventHandlerType, methodInfo);
}
this.connectionPointContainer = connectionPointContainer;
}
/// <summary>
/// The COM event source interface associated with the .NET event interface which was
/// specified as the type parameter.
/// </summary>
public Type ComEventInterfaceType
{
get { return comEventInterfaceType; }
}
/// <summary>
/// Raises a COM event which COM clients can consume. The number and type of parameters
/// specified in <paramref name="args"/> must exactly match the event method parameters.
/// </summary>
/// <typeparam name="EventDelegate">The event delegate which the type library importer created
/// for the COM event which you want to raise.</typeparam>
/// <param name="args">COM event method arguments. Their number and type must match exactly.</param>
public void Raise<eventDelegate>(params object[] args)
{
if (!delegatesToMethods.ContainsKey(typeof(EventDelegate))) return;
MethodInfo methodInfo = delegatesToMethods[typeof(EventDelegate)];
foreach (object obj in observers.Connections)
methodInfo.Invoke(obj, args);
}
#region IConnectionPoint Members
void IConnectionPoint.GetConnectionInterface(out Guid pIID)
{
pIID = comEventInterfaceType.GUID;
}
void IConnectionPoint.GetConnectionPointContainer(out IConnectionPointContainer ppCPC)
{
ppCPC = connectionPointContainer;
}
void IConnectionPoint.Advise(object pUnkSink, out int pdwCookie)
{
pdwCookie = observers.Add(pUnkSink);
}
void IConnectionPoint.Unadvise(int dwCookie)
{
observers.Remove(dwCookie);
}
void IConnectionPoint.EnumConnections(out IEnumConnections ppEnum)
{
ppEnum = observers;
}
#endregion
}
/// <summary>
/// The connection list. This class is used in <see cref="EventHelper{NETEventInterface}"/>s and it maintains
/// list of connections and their cookies.
/// </summary>
internal class ConnectionList : IEnumConnections
{
private IList<keyValuePair<int, object>> connections = new List<keyValuePair<int, object>>();
private int currentCookie;
private int currentEnumIndex;
/// <summary>
/// Adds an object (event sink) to the list.
/// </summary>
/// <param name="obj">Object.</param>
/// <returns>The object's cookie which can be later used in the <see cref="Remove"/> method.</returns>
public int Add(object obj)
{
currentCookie++;
connections.Add(new KeyValuePair<int, object>(currentCookie, obj));
return currentCookie;
}
/// <summary>
/// Removes an object from the list.
/// </summary>
/// <param name="cookie">The objects cookie previously returned from the <see cref="Add"/> method.</param>
public void Remove(int cookie)
{
for (int i = 0; i < connections.Count; i++)
{
if (connections[i].Key == cookie)
{
connections.RemoveAt(i);
return;
}
}
}
/// <summary>
/// The enumeration of connection objects.
/// </summary>
public IEnumerable<object> Connections
{
get
{
foreach (KeyValuePair<int, object> pair in connections)
{
yield return pair.Value;
}
}
}
#region Implementation of IEnumConnections
public int Next(int celt, CONNECTDATA[] rgelt, IntPtr pceltFetched)
{
int fetched = 0;
for (int i = currentEnumIndex; i < connections.Count; i++)
{
CONNECTDATA connectData = new CONNECTDATA();
connectData.dwCookie = connections[i].Key;
connectData.pUnk = connections[i].Value;
rgelt[fetched] = connectData;
fetched = fetched + 1;
if (fetched == celt) break;
}
currentEnumIndex = currentEnumIndex + fetched;
if (pceltFetched != IntPtr.Zero)
{
Marshal.WriteInt32(pceltFetched, fetched);
}
return fetched == celt ? 0 : 1;
}
public int Skip(int celt)
{
currentEnumIndex += celt;
return currentEnumIndex < connections.Count ? 0 : 1;
}
public void Reset()
{
currentEnumIndex = 0;
}
public void Clone(out IEnumConnections ppenum)
{
ConnectionList clone = new ConnectionList();
clone.connections = connections;
clone.currentCookie = currentCookie;
clone.currentEnumIndex = currentEnumIndex;
ppenum = clone;
}
#endregion
}
Do not be scared by its length, once you get the gist of it, it becomes pretty straightforward. The good thing about this code is that it can be used in a very generic fashion, i.e. whenever you want to expose COM events in a .NET object. The bad thing is that this solution does not play well with standard .NET events, which is not, however, the focus of this little article.
Putting it together
Now, let's show how these helper classes are used:
[ComSourceInterfaces(typeof(IMapSurroundEvents))]
[ProgId("...")]
[Guid("...")]
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class MyMapSurround : IConnectionPointContainer, IMapSurround, IBoundsProperties, IClone, IPersistVariant
{
private readonly EventContainerHelper eventContainerHelper;
private readonly EventHelper<imapSurroundEvents_Event> eventHelper;
public MyMapSurround()
{
eventContainerHelper = new EventContainerHelper(this);
eventHelper = eventContainerHelper.AddEvents<imapSurroundEvents_Event>();
}
#region IConnectionPointContainer Members
public void EnumConnectionPoints(out IEnumConnectionPoints ppEnum)
{
eventContainerHelper.EnumConnectionPoints(out ppEnum);
}
public void FindConnectionPoint(ref Guid riid, out IConnectionPoint ppCP)
{
eventContainerHelper.FindConnectionPoint(ref riid, out ppCP);
}
#endregion
// This is the IMapSurround.Draw method.
public void Draw(IDisplay display, ITrackCancel trackCancel, IEnvelope bounds)
{
eventHelper.Raise<imapSurroundEvents_BeforeDrawEventHandler>(display);
// do some drawing
eventHelper.Raise<imapSurroundEvents_AfterDrawEventHandler>(display);
}
// ... other IMapSurround methods as well as other implemented interface methods
Basically, these are the essential steps:
- Create an instance of the EventContainerHelper class and use its AddEvents method to tell it the event interface which you want to support. You specify the imported .NET event interface, the method will determine the associated original COM event source interface automatically using reflection.
- Implement IConnectionPointContainer on your map surround object, and delegate all calls on this interface methods to your instance of EventContainerHelper. As you have specified which event interfaces you want to support by calling AddEvents, this object knows how to respond to FindConnectionPoint when it is called by a COM client (i.e. ArcMap in our case).
- The AddEvents method hands you over an instance of EventHelper class, which you can later on use to raise the events, as shown in the example implementation of map surround's Draw method. The important bit to keep in mind here is that the parameters you pass to the Raise method must exactly match the parameters of the event delegate you specify as the type parameter.
When you add your map surround object to the ArcMap's page layout, ArcMap will immediately subscribe for these events, providing its event sinks, listening and waiting for you to raise the events. Most importantly, when you implement a map surround of your own, do not forget to raise ContentsChanged event, which causes you object to redraw correctly (i.e., ArcMap will call your Draw implementation if it needs to).
The end
Now, I will leave you to examine the code. If you do not understand its inner workings, I suggest you roll your own IMapSurround implementation, use the helper objects as shown, and go through the code step by step, for example using a debbuger. It will suddenly come clear what the code does and how ArcMap interacts with your object and its events.
Conversely, if you have any question or (even better) suggestions, feel free to express yourself in the comments below. ANY corrections and suggestions are more than welcome!
This is a brilliant article, and is a topic that I have struggled with for years. We are attempting to use this eventing model in a slightly different way. We would like external .NET applications to be able to bind to events raised from ArcMap. We can already do this using the already published events like IDocumentEvents, but not so with custom events. Have you had any experience and pointers for this type of thing?
Hi, I actually thought no one has ever read it. In just noticed there are some casing errors in the code, no idea how that happened and I'm sorry about that. As for your question, I am not sure I completely understand. Try asking the question in more detail on gis.stackexchange (which is a great site) and I'll try to answer there.
@Zach: Ah, I think I understand. If you are using .NET and want to subcribe to your own events from an external .NET application, you do not need to fiddle with the COM-related stuff at all. You may try these simple steps:
There are problems lurking around the corner, though. You are effectively doing cross-process calls which are executed in a different way. I imagine issues may arise when the external application subscribes to some events and then exits without unsubscribing from them correctly etc. Anyway, I believe this approach is worth trying.