Few years back I wrote a blog post on this exact subject: Exposing COM events from .NET: Implementing MapSurround in ArcMap. It discusses the implementation of IMapSurroundEvents outbound interface, but the concept stays the same.
The bottom line is: it's not that easy, you need to implement the underlying COM event-related interfaces by yourself, as well as mark the class with ComSourceInterfaces attribute. Note that you only need to go down this road if there are going to be some COM-only, non-.NET listeners. It is absolutely not needed if all the subscribers to the outbound events are managed clients.
Also please note that while the post will give you instructions on how to implement an existing outbound COM event interface, this may not be enough in your particular scenario, that is ILayerEvents implemented on a layer. There is no documentation saying that ArcGIS will wire up to this interface upon adding the layer to the map. In case IMapSurroundEvents interface dicussed in the post, it does. I will try to see what the behaviour is for ILayerEvents.
Disclaimer: there are some grammatical mistakes and typos in the linked blog post as well as few casing errors in the C# code. I don't have time to fix it, so I hope you'll be able to get over it. If that's not the case, do not hesitate to ask.
UPDATE: ArcMap does not seem to subscribe to ILayerEvents. It makes sense, since no event interface is required for a custom layer to work. Just for completeness, I include the full code required to publish an outbound COM IUnknown-derived interface in .NET. It is a corrected version of the code in the aforementioned blog post. I also modified it to publish the ILayerEvents outbound interface. Besides this, to demonstrate that COM clients can subscribe to this interface, I include a VBA script which you can run from inside ArcMap.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using ESRI.ArcGIS.Carto;
namespace AgLayerEvents
{
using System.Runtime.InteropServices;
using ESRI.ArcGIS.ADF.BaseClasses;
using ESRI.ArcGIS.Display;
using ESRI.ArcGIS.esriSystem;
using ComHelpers;
[ComVisible(true)]
[Guid("2D034995-D588-47D1-90AA-E3D408DA4B69")]
[ProgId("AgLayerEventsTest.FakeLayer")]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(ILayerEvents))]
public class FakeLayer : BaseCustomLayer,
ComInterfaceDefinitions.IConnectionPointContainer, // COM-only clients will subscribe to events via this interface
ILayerEvents_Event // this is for managed clients
{
private readonly EventContainerHelper _eventContainerHelper;
private readonly EventHelper<ILayerEvents_Event> _layerEventsHelper;
public FakeLayer()
{
_eventContainerHelper = new EventContainerHelper(this);
_layerEventsHelper = _eventContainerHelper.AddEvents<ILayerEvents_Event>();
}
#region Overrides of BaseCustomLayer
public override void Draw(esriDrawPhase drawPhase, IDisplay display, ITrackCancel trackCancel)
{
}
public override bool Visible
{
get
{
return base.Visible;
}
set
{
if (base.Visible == value) return;
base.Visible = value;
FireVisibilityChanged();
}
}
#endregion
private void FireVisibilityChanged()
{
_layerEventsHelper.Raise<ILayerEvents_VisibilityChangedEventHandler>(Visible); // notify COM-only clients
// notify managed clients
var visibilityChangedEvent = VisibilityChanged;
if (visibilityChangedEvent != null)
{
visibilityChangedEvent(Visible);
}
}
#region Implementation of ILayerEvents_Event
public event ILayerEvents_VisibilityChangedEventHandler VisibilityChanged;
#endregion
#region Implementation of IConnectionPointContainer
public void EnumConnectionPoints(out ComInterfaceDefinitions.IEnumConnectionPoints ppEnum)
{
_eventContainerHelper.EnumConnectionPoints(out ppEnum);
}
public void FindConnectionPoint(ref Guid riid, out ComInterfaceDefinitions.IConnectionPoint ppCp)
{
_eventContainerHelper.FindConnectionPoint(ref riid, out ppCp);
}
#endregion
}
}
namespace ComHelpers
{
using ComInterfaceDefinitions;
using CONNECTDATA = System.Runtime.InteropServices.ComTypes.CONNECTDATA;
/// <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="ComHelpers.EventContainerHelper"/>.
/// </param>
public EventContainerHelper(IConnectionPointContainer connectionPointContainer)
{
if (connectionPointContainer == null) throw new ArgumentNullException("connectionPointContainer");
_connectionPointContainer = connectionPointContainer;
}
/// <summary>
/// Adds a new event interface to which this helper should react.
/// </summary>
/// <typeparam name="TNetEventInterface">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<TNetEventInterface> AddEvents<TNetEventInterface>()
{
EventHelper<TNetEventInterface> eventHelper =
new EventHelper<TNetEventInterface>(_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, IntPtr pceltFetched)
{
var fetched = 0;
for (var i = _currentEnumIndex; i < _connectionPoints.Count; i++)
{
rgelt[fetched] = _connectionPoints[i];
fetched = fetched + 1;
if (fetched == celt) break;
}
_currentEnumIndex = _currentEnumIndex + fetched;
if (pceltFetched != IntPtr.Zero)
{
if (pceltFetched != IntPtr.Zero)
{
Marshal.WriteInt32(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="TNetEventInterface">The .NET event interface (associated with a COM event source interface)
/// created by the type library importer.</typeparam>
public class EventHelper<TNetEventInterface> : 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(TNetEventInterface);
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) continue;
_delegatesToMethods.Add(eventInfo.EventHandlerType, methodInfo);
}
_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="TEventDelegate">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<TEventDelegate>(params object[] args)
{
if (!_delegatesToMethods.ContainsKey(typeof(TEventDelegate))) return;
MethodInfo methodInfo = _delegatesToMethods[typeof(TEventDelegate)];
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
{
return _connections.Select(pair => 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
}
}
namespace ComInterfaceDefinitions
{
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);
}
}
The VBA script showing the COM outbound event subscription:
Option Explicit
Private WithEvents m_pLayerEvents As LayerEventsHelper
Sub TestEvents()
Dim pDoc As IMxDocument
Dim pLayer As ILayer
Set pDoc = Application.Document
Set pLayer = CreateObject("AgLayerEventsTest.FakeLayer")
pLayer.Name = "Layer"
Set m_pLayerEvents = pLayer
pDoc.FocusMap.AddLayer pLayer
' Following statement triggers the handler below. Please note that without implementing
' IConnectionPointContainer on FakeLayer, the event subscription would not be possible
' here at all since we are inside VBA, which needs this interface. So does any unmanaged client.
pLayer.Visible = False
End Sub
Private Sub m_pLayerEvents_VisibilityChanged(ByVal currentState As Boolean)
Debug.Print "VisibilityChanged: " & currentState
End Sub
You can see that as soon as you hit Set m_pLayerEvents = pLayer in the VBA code, the IConnectionPointContainer.FindConnectionPoint is hit on the FakeLayer class. The event helper class does the rest of the necessary (rather elaborate) COM event wireup. For full details on how this exactly works (which is a rather elaborate standard COM mechanism), see the blog post.