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);
}
}
Answer
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 _layerEventsHelper;
public FakeLayer()
{
_eventContainerHelper = new EventContainerHelper(this);
_layerEventsHelper = _eventContainerHelper.AddEvents();
}
#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(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;
///
/// The event container helper. Objects which wish to implement maintain
/// an instance of this class and delegate all calls to the interface methods to that object.
///
public class EventContainerHelper : IConnectionPointContainer
{
private readonly IList _eventHelpers = new List();
private readonly IDictionary _guidToConnectionPoint = new Dictionary();
private readonly ConnectionPointList _connectionPoints = new ConnectionPointList();
private readonly IConnectionPointContainer _connectionPointContainer;
///
/// Creates a new instance of the event container helper.
///
/// The connection point container. All calls to that connection
/// point container are to be delegated to this newly created instance of .
///
public EventContainerHelper(IConnectionPointContainer connectionPointContainer)
{
if (connectionPointContainer == null) throw new ArgumentNullException("connectionPointContainer");
_connectionPointContainer = connectionPointContainer;
}
///
/// Adds a new event interface to which this helper should react.
///
/// The .NET event interface which the type library importer
/// creates for a COM event source interface.
/// An event helper which can be used to raise events on the specified event interface.
public EventHelper AddEvents()
{
EventHelper eventHelper =
new EventHelper(_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
}
///
/// The list of connection points. This class is used in and serves
/// merely to implement the interface.
///
internal class ConnectionPointList : IEnumConnectionPoints
{
private IList _connectionPoints = new List();
private int _currentEnumIndex;
///
/// Adds a connection point to the list.
///
/// The connection point.
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
}
///
/// Base event helper class.
///
public abstract class EventHelper
{
}
///
/// The event helper class. This class aids in publishing .NET events to COM via connection points
/// infrastructure.
///
/// The .NET event interface (associated with a COM event source interface)
/// created by the type library importer.
public class EventHelper : EventHelper, IConnectionPoint
{
private readonly IDictionary _delegatesToMethods = new Dictionary();
private readonly ConnectionList _observers = new ConnectionList();
private readonly IConnectionPointContainer _connectionPointContainer;
private readonly Type _comEventInterfaceType;
///
/// Creates a new instance of the event helper class.
///
/// The connection point container.
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;
}
///
/// The COM event source interface associated with the .NET event interface which was
/// specified as the type parameter.
///
public Type ComEventInterfaceType
{
get { return _comEventInterfaceType; }
}
///
/// Raises a COM event which COM clients can consume. The number and type of parameters
/// specified in must exactly match the event method parameters.
///
/// The event delegate which the type library importer created
/// for the COM event which you want to raise.
/// COM event method arguments. Their number and type must match exactly.
public void Raise(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
}
///
/// The connection list. This class is used in s and it maintains
/// list of connections and their cookies.
///
internal class ConnectionList : IEnumConnections
{
private IList> _connections = new List>();
private int _currentCookie;
private int _currentEnumIndex;
///
/// Adds an object (event sink) to the list.
///
/// Object.
/// The object's cookie which can be later used in the method.
public int Add(object obj)
{
_currentCookie++;
_connections.Add(new KeyValuePair(_currentCookie, obj));
return _currentCookie;
}
///
/// Removes an object from the list.
///
/// The objects cookie previously returned from the method.
public void Remove(int cookie)
{
for (int i = 0; i < _connections.Count; i++)
{
if (_connections[i].Key == cookie)
{
_connections.RemoveAt(i);
return;
}
}
}
///
/// The enumeration of connection objects.
///
public IEnumerable
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.
No comments:
Post a Comment