Take the 2-minute tour ×
Geographic Information Systems Stack Exchange is a question and answer site for cartographers, geographers and GIS professionals. It's 100% free, no registration required.

I would like to write a class in C# that implements one or more outbound interfaces, for example ILayerEvents and IActiveViewEvents.

Does anyone know how to do this with C#?

Update Following @Chaz's response, I have tried this out.

class MyCustomLayer : BaseCustomLayer, ILayerEvents_Event
{
    public override bool Visible
    {
        get { return base.Visible;}
        set
        {
            base.Visible = value;
            if (this.VisibilityChanged != null)
                this.VisibilityChanged(value);
        }
    }
    public override void Draw(esriDrawPhase drawPhase, IDisplay Display, ITrackCancel trackCancel)
    {
        //throw new NotImplementedException();
    }
    public event ILayerEvents_VisibilityChangedEventHandler VisibilityChanged;
}

I expected that when the map sees that a layer being added implements ILayerEvents_Event, that it would wire events accordingly. However, when I add my custom layer to the map this doesn't happen. I wrote a test command that toggles ILayer.Visible but the checkbox in the TOC doesn't change ... this.VisibilityChanged is always null, indicating that there are no subscribers. Or maybe I'm wrong in assuming the TOC somehow subscribes to this interface?

protected override void OnClick()
{
    MyCustomLayer l = null;
    for (int i = 0; i < ArcMap.Document.FocusMap.LayerCount; i++)
    {
        var layer = ArcMap.Document.FocusMap.get_Layer(i);
        if (layer is MyCustomLayer)
            l = layer as MyCustomLayer;
    }
    if (l != null)
        l.Visible = !l.Visible;
    else
    {
        l = new MyCustomLayer() { Name = "a custom layer" };
        ArcMap.Document.FocusMap.AddLayer(l);
    }
}
share|improve this question

2 Answers 2

up vote 5 down vote accepted

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.

share|improve this answer
    
Excellent response! I just noticed (also) that toggling ILayer.Visible for a featurelayer doesn't trigger a change in the checkbox on the TOC, even though featurelayer implements ILayerEvents as an outbound interface. I wonder what the intent behind that interface is. This is the sort of thing that INotifyPropertyChanged sure would simplify. I've never heard about using that across the interop though. If it is possible it seems like Esri could rely on that instead of COM. –  Kirk Kuykendall Jan 2 '12 at 22:29
    
To me the intent of ILayerEvents is quite simple - in case someone is interested in visibility change of a particular layer, the chance is there. The fact that most ILayers in ArcObjects do implement this interface is probably due to the fact that they internally derive from the same base class. But it is not required that layers implement this outbound interface and no part of ArcObjects seems to make any assumptions about this. –  Petr Krebs Jan 2 '12 at 22:44
    
The story behind IMapSurroundEvents is largely different in the fact that when you want to implement a map surround (say, a custom scalebar, legend etc.), you absolutely need to implement this outbound interface. Otherwise ArcMap will just blow up in your face. Believe me, I had to go to great lengths to make this work, there is very little good information on this out there. .NET does a wonderful and clever job at helping you consume COM events, but when you want to do the reverse and the event interface happens to inherit from IUnknown, you are on your own. Fortunately, this need occurs only –  Petr Krebs Jan 2 '12 at 22:50
    
very rarely. I am aware of only one case (the map surrounds) in ArcObjects when this is absolutely needed. But, well, should the need occur, the supporting helper classes in the code above should do the trick since I attempted to come up with a general solution. –  Petr Krebs Jan 2 '12 at 22:52
    
Some sort of INotifyPropertyChanged would be great, but would require a great deal of effort from Esri, I just do not see that happening outside WPF and Silverlight :-) –  Petr Krebs Jan 2 '12 at 22:54

Are you syncing your controls? This works for me and shows the message from the listening event (VChange) every time I click on the checkbox in the TOC. (I'm using a synchronizer which does the whole SetBuddyControl() thing automagically.)

// in main form
FakeLayer fakeLayer = new FakeLayer();
ILayerEvents_Event layerEventSink;

private void MainForm_Load( object sender, EventArgs e ) {
    this.MapControl.AddLayer( fakeLayer, 0 );
    layerEventSink = fakeLayer as ILayerEvents_Event;
    layerEventSink.VisibilityChanged += VChange;
}

private void VChange( bool currentState ) {
    Debug.Print( "Fired as: {0}", currentState );
}

// in class file
public sealed class FakeLayer : BaseCustomLayer, ILayerEvents_Event
{
    public override bool Visible {
        get { return base.Visible; }
        set {
            base.Visible = value;
            if ( this.VisibilityChanged != null )
                this.VisibilityChanged( value );
        }
    }
    public override void Draw( esriDrawPhase drawPhase, IDisplay Display, ITrackCancel trackCancel ) {
        //throw new NotImplementedException();
    }
    public event ILayerEvents_VisibilityChangedEventHandler VisibilityChanged;
}

Oh! I see! No one is listening! Sounds like a bug to me. <-- No, no, no, no!!!

Why should the TOC listen? The layer is implementing the correct interface for those who want to listen - end of story. The notification is caused by the TOC changing the value of the .Visible property and that fires the event - not the other way around. The event is only for those who are interested.

share|improve this answer
    
Thanks Chaz, I've replied in an update to my question. –  Kirk Kuykendall Dec 30 '11 at 5:08
    
Chaz - without a subscriber, though, the TOC ends up out of sync, which seems wrong. –  Reed Copsey Dec 30 '11 at 17:52
    
I grok that but it is listening because when the value is changed thru code the UI of the TOC is updated. It is just not subscribing to the ILayerEvents_Event interface. –  Chaz Dec 30 '11 at 18:36
    
ITOCBuddyEvents_Event? Nope, not listening. But it may be the reason the TOC UI updates. –  Chaz Dec 30 '11 at 20:49
    
Chaz, although you may be correct that the TOC will not subscribe to the event interface (there is no documentation that it behaves that way), your answer is not actually correct. The snippet you included exposes the interfaces only for managed .NET clients, COM-only clients (like ArcMap) will not be able to subscribe to the interface. You can easily test by trying to listen to the events from VBA or VB6, it will NOT work. See my answer for details. –  Petr Krebs Jan 2 '12 at 16:05

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.