Communication and coordination between components within an add-in
In most add-in projects, a means for inter-component communication is needed so that one component can activate or obtain state from another. For example, a particular add-in button component—when clicked—may alter the overall add-in state in a way that modifies the content displayed in an associated add-in dockview component.
Traditionally, Component Object Model (COM)-based mechanisms are used for inter-component communication, defining custom interfaces, which are then exposed from button, dockview, or extension components. These interfaces are accessed by determining the component using framework-supplied find functions, such as ICommandBar.Find and IApplication.FindExtension. The returned reference is then cast to the custom interface and used. This approach has the following disadvantages:
- Using COM interfaces restricts communication to the simple types and calling patterns supported by COM and rules out the use of any language-specific types and patterns.
- Communication between components in the same project is generally private; registering COM interfaces ostensibly publicizes these private communication lines and unnecessarily complicates any public interfaces intended for exposure outside the project.
- COM interfaces need additional non-trivial coding steps and require registration in the system registry by an installer with administrative rights. In .NET, such interfaces also require the generation and registration of Primary Interop Assemblies (PIAs), which introduce multiple native and managed context switches for each call (even though both ends of the conversation are managed).
Since, by design, add-ins cannot rely on COM patterns and do not require registration, the traditional approach using COM is not a viable option. One alternative pattern is straightforward and has none of the previously listed disadvantages. Since all framework types are singletons, you can use a simple approach based on static class members to achieve inter-component communication.
The following code example shows how an extension can directly expose its functionality to other components in the same project:
[C#]
public class MainExt: ESRI.ArcGIS.Desktop.AddIns.Extension
{
private static MainExt s_extension;
public MainExt()
{
s_extension = this;
}
internal static MainExt GetExtension()
{
// Extension loads just in time, call FindExtension to load it.
if (s_extension == null)
{
UID extID = new UIDClass();
extID.Value = ThisAddIn.IDs.MainExt;
ArcMap.Application.FindExtensionByCLSID(extID);
}
return s_extension;
}
internal void DoWork()
{
System.Windows.Forms.MessageBox.Show("Do work");
}
}
[VB.NET]
Public Class MainExt
Inherits ESRI.ArcGIS.Desktop.AddIns.Extension
Private Shared s_extension As MainExt
Public Sub New()
s_extension = Me
End Sub
Friend Shared Function GetExtension() As MainExt
' Extension loads just in time, call FindExtension to load it.
If s_extension Is Nothing Then
Dim extID As UID = New UIDClass()
extID.Value = ThisAddIn.IDs.MainExt;
ArcMap.Application.FindExtensionByCLSID(extID)
End If
Return s_extension
End Function
Friend Sub DoWork()
System.Windows.Forms.MessageBox.Show("Do work")
End Sub
End Class
The following code example shows how a typical client in the same add-in project accesses the extension:
[C#]
protected override void OnClick()
{
MainExt mainExt = MainExt.GetExtension();
mainExt.DoWork();
}
[VB.NET]
Protected Overloads Overrides Sub OnClick()
Dim mainExt__1 As MainExt = MainExt.GetExtension()
mainExt__1.DoWork()
End Sub
AddIn.FromID function
The add-in framework also provides a static AddIn.FromID function that returns a reference to an instance of an add-in object given the associated add-in ID string. The FromID function ensures that the requested extension, dockable window, or command is properly loaded and initialized, if necessary, before returning the reference. In addition, you can use the ThisAddIn.IDs class to obtain the add-in ID string without going through the Extensible Markup Language (XML) configuration file. Using the IDs class also avoids typographical errors and eliminates the need for updating all ID string references within your code if the ID in the XML config file is ever changed.
The OnClick implementation in the following code example uses the parameterized FromID function to obtain a reference to an Addin1Ext instance—the add-in extension within the project—and call a custom method. The ThisAddIn.IDs class is used to obtain the extension’s ID.
[C#]
protected override void OnClick()
{
var ext = AddIn.FromID < Addin1Ext > (ThisAddIn.IDs.Addin1Ext);
ext.MyCustomMethod();
}
[VB.NET]
Protected Overloads Overrides Sub OnClick()
Dim ext = AddIn.FromID(Of Addin1Ext)(ThisAddIn.IDs.Addin1Ext)
ext.MyCustomMethod()
End Sub
Using extensions
Well designed projects use a central object to store state and coordinate activities between components in the project. Extension objects are designed specifically for this role, listening for various application events and maintaining state for the add-in. In addition, add-ins can optionally store and retrieve persistent state within document streams. See the following illustration:
Enabling and checking commands
Add-in commands—buttons and tools—are periodically advised to update their enabled and checked state via the OnUpdate method. Because there are many commands, and because this method may be called at relatively high frequencies, keep code within the method to a minimum to avoid affecting application responsiveness. In many circumstances, checks should be forwarded to the add-in’s extension component where the relevant state information can be intelligently updated and cached in response to various application events. Particularly time consuming operations, such as database queries, should never be performed within OnUpdate.
The following code provides a simple example. In this case, the command is enabled whenever the selection count is greater than zero; this command listens for the SelectionChanged event to minimize calls to the Map object.
[C#]
protected override void OnUpdate()
{
this.Enabled = m_enabled;
}
void mapEvents_SelectionChanged()
{
m_enabled = m_focusMap.SelectionCount > 0;
}
[VB.NET]
Protected Overloads Overrides Sub OnUpdate()
Me.Enabled = m_enabled
End Sub
Private Sub mapEvents_SelectionChanged()
m_enabled = m_focusMap.SelectionCount > 0
End Sub
The following code example shows a more realistic situation, where a sub-optimal implementation is improved.
The initial implementation of this button checks for any selectable feature layer in OnUpdate. This function, particularly if cut and pasted to many other buttons, can degrade application responsiveness.
[C#]
protected override void OnUpdate()
{
IMap map = ArcMap.Document.FocusMap;
// Bail if map has no layers.
if (map.LayerCount == 0)
{
this.Enabled = false;
return ;
}
// Fetch all the feature layers in the focus map
// to determine if at least one is selectable.
UIDClass uid = new UIDClass();
uid.Value = "{40A9E885-5533-11d0-98BE-00805F7CED21}";
IEnumLayer enumLayers = map.get_Layers(uid, true);
IFeatureLayer featureLayer = enumLayers.Next()as IFeatureLayer;
while (featureLayer != null)
{
if (featureLayer.Selectable == true)
{
this.Enabled = true;
return ;
}
featureLayer = enumLayers.Next()as IFeatureLayer;
}
this.Enabled = false;
}
[VB.NET]
Protected Overloads Overrides Sub OnUpdate()
Dim map As IMap = ArcMap.Document.FocusMap
' Bail if map has no layers.
If map.LayerCount = 0 Then
Me.Enabled = False
Exit Sub
End If
' Fetch all the feature layers in the focus map
' to determine if at least one is selectable.
Dim uid As New UIDClass()
uid.Value = "{40A9E885-5533-11d0-98BE-00805F7CED21}"
Dim enumLayers As IEnumLayer = map.get_Layers(uid, True)
Dim featureLayer As IFeatureLayer = TryCast(enumLayers.[Next](), IFeatureLayer)
While featureLayer IsNot Nothing
If featureLayer.Selectable = True Then
Me.Enabled = True
Exit Sub
End If
featureLayer = TryCast(enumLayers.[Next](), IFeatureLayer)
End While
Me.Enabled = False
End Sub
After refactoring, the same button has been optimized to only check for a selectable feature layer when something in the map changes as indicated by the ContentsChanged event. The new implementation is much more efficient than the original; the code to check for a selectable layer now executes only when something about the map changes, such as when a new layer is added. The button uses an extension to perform the real work so that other commands can benefit as well.
[C#]
// Button’s OnUpdate.
protected override void OnUpdate()
{
this.Enabled = m_mainExtension.HasSelectableLayer();
}
// Extension listens for map events.
void MapEvents_ContentsChanged()
{
m_hasSelectableLayer = CheckForSelectableLayer();
}
private bool CheckForSelectableLayer()
{
IMap map = ArcMap.Document.FocusMap;
// Bail if map has no layers.
if (map.LayerCount == 0)
return false;
// Fetch all the feature layers in the focus map
// to determine if at least one is selectable.
UIDClass uid = new UIDClass();
uid.Value = "{40A9E885-5533-11d0-98BE-00805F7CED21}";
IEnumLayer enumLayers = map.get_Layers(uid, true);
IFeatureLayer featureLayer = enumLayers.Next()as IFeatureLayer;
while (featureLayer != null)
{
if (featureLayer.Selectable == true)
return true;
featureLayer = enumLayers.Next()as IFeatureLayer;
}
return false;
}
internal bool HasSelectableLayer()
{
return m_hasSelectableLayer;
}
[VB.NET]
' Button’s OnUpdate.
Protected Overloads Overrides Sub OnUpdate()
Me.Enabled = m_mainExtension.HasSelectableLayer()
End Sub
' Extension listens for map events.
Private Sub MapEvents_ContentsChanged()
m_hasSelectableLayer = CheckForSelectableLayer()
End Sub
Private Function CheckForSelectableLayer() As Boolean
Dim map As IMap = ArcMap.Document.FocusMap
' Bail if map has no layers.
If map.LayerCount = 0 Then Return False
End If
' Fetch all the feature layers in the focus map
' to determine if at least one is selectable.
Dim uid As New UIDClass()
uid.Value = "{40A9E885-5533-11d0-98BE-00805F7CED21}"
Dim enumLayers As IEnumLayer = map.get_Layers(uid, True)
Dim featureLayer As IFeatureLayer = TryCast(enumLayers.[Next](), IFeatureLayer)
While featureLayer IsNot Nothing
If featureLayer.Selectable = True Then
Return True
End If
featureLayer = TryCast(enumLayers.[Next](), IFeatureLayer)
End While
Return False
End Function
Friend Function HasSelectableLayer() As Boolean
Return m_hasSelectableLayer
End Function
Persisting data in documents using application extensions
Application extensions can read and write data directly from and to map documents. This persistence option is useful to store and re-establish state on a per document basis. For example, a custom selection tool can have its associated extension persist the selection environment settings a user has defined.
When documents are saved, each loaded application extension component is given an opportunity to write data to a dedicated stream within the current document. Similarly, when a document is loaded, each extension is given an opportunity to read data from this stream. You can write any data to this stream.
The following code example shows an extension writing two integers:
[C#]
protected override void OnLoad(Stream inStrm)
{
_selectionTolerance = Convert.ToInt32(inStrm.ReadByte());
_ selectionThreshold = Convert.ToInt32(inStrm.ReadByte());
}
protected override void OnSave(Stream outStrm)
{
outStrm.WriteByte(Convert.ToByte(_selectionTolerance));
outStrm.WriteByte(Convert.ToByte(_selectionThreshold));
}
[VB.NET]
Protected Overloads Overrides Sub OnLoad(ByVal inStrm As Stream)
_selectionTolerance = Convert.ToInt32(inStrm.ReadByte())
Dim selectionThreshold As _ = Convert.ToInt32(inStrm.ReadByte())
End Sub
Protected Overloads Overrides Sub OnSave(ByVal outStrm As Stream)
outStrm.WriteByte(Convert.ToByte(_selectionTolerance))
outStrm.WriteByte(Convert.ToByte(_selectionThreshold))
End Sub
The following code example shows one way to write a string:
[C#]
protected override void OnLoad(Stream inStrm)
{
var binaryFormatter = new
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
_lastEditedBy = binaryFormatter.Deserialize(inStrm)as string;
}
protected override void OnSave(Stream outStrm)
{
var binaryFormatter = new
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
binaryFormatter.Serialize(outStrm, _lastEditedBy);
}
[VB.NET]
Protected Overloads Overrides Sub OnLoad(ByVal inStrm As Stream)
Dim binaryFormatter = New System.Runtime.Serialization.Formatters.Binary.BinaryFormatter()
_lastEditedBy = TryCast(binaryFormatter.Deserialize(inStrm), String)
End Sub
Protected Overloads Overrides Sub OnSave(ByVal outStrm As Stream)
Dim binaryFormatter = New System.Runtime.Serialization.Formatters.Binary.BinaryFormatter()
binaryFormatter.Serialize(outStrm, _lastEditedBy)
End Sub
To store custom types and object graphs, use the ArcObjects PersistHelper class. The following code example shows an extension writing a private struct, which manages an ArcObjects type:
[C#]
private MyPersistentData _data;
protected override void OnLoad(Stream inStrm)
{
// Initialize the struct.
_data.Location = "";
_data.Point = new ESRI.ArcGIS.Geometry.PointClass();
PersistHelper.Load < MyPersistentData > (inStrm, ref _data);
}
protected override void OnSave(Stream outStrm)
{
PersistHelper.Save < MyPersistentData > (outStrm, _data);
}
[Serializable()]
private struct MyPersistentData
{
public string Location;
public ESRI.ArcGIS.Geometry.PointClass Point;
}
[VB.NET]
Private _data As MyPersistentData
Protected Overloads Overrides Sub OnLoad(ByVal inStrm As Stream)
' Initialize the struct.
_data.Location = ""
_data.Point = New ESRI.ArcGIS.Geometry.PointClass()
PersistHelper.Load(Of MyPersistentData)(inStrm, _data)
End Sub
Protected Overloads Overrides Sub OnSave(ByVal outStrm As Stream)
PersistHelper.Save(Of MyPersistentData)(outStrm, _data)
End Sub
<Serializable()> _
Private Structure MyPersistentData
Public Location As String
Public Point As ESRI.ArcGIS.Geometry.PointClass
End Structure