In this section:
Object Model Diagram Click here.
Example Code Click here.
Description This project provides a custom snap agent that can be used to snap to particular subtypes of features while editing. A dockable window helps to manage the snap agent, and a command allows you to show and hide this dockable window. An editor extension ties all the custom classes together.
License required ArcEditor or above
Libraries ArcMapUI, Carto, Display, Editor, Framework, Geodatabase, Geometry, System, and SystemUI
Languages Visual Basic
Categories ESRI Snap Agents, ESRI Editor Extensions, ESRI Dockable Windows, and ESRI Mx Commands
Interfaces ISnapAgent, ISnapAgentFeedback, IPersistVariant, IExtension, IDockableWindowDef, and ICommand
How to use
By using the standard editing functionality in ArcMap, you can create new features by snapping to the vertices, edges, or ends of the existing features.
However, if your data has subtypes and you would like to snap to only certain of the subtypes, you cannot do this using the existing functionality.
A custom snap agent mimics the standard feature snap agents except that it additionally checks for subtypes. The snap agents will have properties controlling which feature class and which subtype it targets.
This example demonstrates how to create a custom snap agent that can snap the edit sketch to specific subtypes of features in the existing feature classes.
By reviewing the Editor object model diagram, you will see the SnapAgent abstract
class, which implements ISnapAgent and also, optionally, ISnapAgentCategory and
ISnapAgentFeedback. You can also see the FeatureSnap coclass, which implements
its own IFeatureSnapAgent interface.
ISnapAgentFeedback allows snap agents to report back to the user what was snapped to. For example, feature snap agents report back the feature class and geometry part (vertex, edge, end) they successfully snapped to. ISnapAgentCategory helps organize regular snap agents by grouping them in categories in the bottom half of the snap environment dialog box as illustrated below. (This has nothing to do with component categories.) For example, all snap agents that work with the edit sketch are classified under Edit Sketch. Using ISnapAgentCategory, you can group your custom snap agents in an existing category or create your own.
To meet the requirements described above, you will create a subtype of the SnapAgent abstract class by implementing ISnapAgent and ISnapAgentFeedback. You will not implement IExtension, as you will create a separate extension class (see the section later in this topic for more information). You will make the snap agent persistable by implementing IPersistVariant (as the example code is in VB6). You will also create a custom interface, ISubtypesSnap, to allow access to the custom functionality of your class.
The design of the snap agent will be such that a new agent will be created for each subtype of each feature class in the current map.
As you saw in About Snap Agents, each agent is part of a larger Snapping Environment framework. The snapping environment sets up snap agents and allows a user to control their properties and state. The SubtypesSnap cannot be used in isolationan essential part of this customization is the accompanying editor extension and dockable window, which are discussed later in this example.
Your SubtypesSnap needs to be able to identify which Subtype of which FeatureClass it needs to snap to. You also need to be able to turn the snap agent on and off, as you can for other snap agents by selecting and deselecting the agent in the Snapping Environment dockable window. As there may be many snap agents, one for each subtype in each feature class, you should be able to name the snap agent.
To achieve these goals, create an interface called ISubtypesSnapAgent. Add five read-write properties to the interface to allow another class to set a FeatureClass, a SubtypeName and SubtypeCode, and a boolean to indicate if the agent is switched on.
The custom ISubtypesSnapAgent interface will allow clients to specify which FeatureClass and subtype the agent snaps to and also to identify each snap agent individually.
Now implement ISubtypesSnapAgent in your SubtypesSnap class. Create member variables to store the values of its properties. Implement each property to store or return the appropriate variable, as shown in the FeatureClass property below.
Privatem_pFeatureClassAsesriGeoDatabase.IFeatureClass' The snap FeatureClassPrivatem_lCodeAs Long' The snap Subtype codePrivatem_sNameAs String' The snap agent namePrivatem_bIsOnAs Boolean' Is the agent active?Private Property GetISubtypesSnapAgent_FeatureClass()AsesriGeoDatabase.IFeatureClassSetISubtypesSnapAgent_FeatureClass = m_pFeatureClassEnd Property Private Property SetISubtypesSnapAgent_FeatureClass(RHSAsesriGeoDatabase.IFeatureClass)Setm_pFeatureClass = RHSEndPropert
The Name property should return the string you want to be displayed in the Snapping Environment dockable window.
Private Property GetISnapAgent_Name()As StringISnapAgent_Name = "Sub Types"EndPropert
Performing the Snap with a FeatureCache
To activate a snap agent, a user starts an edit session and checks the snap agent required in the Snapping Environment dockable window. When an agent is activated and the current tool is one of the sketch tools, the ISnapAgent::Snap method will be called every time the mouse moves. This results in many calls, so to increase performance, the snap agent will use a feature cache.
A feature cache improves snapping performance because it holds onto a small subset (cache) of features from the area immediately surrounding the current tool location; when testing for hits, the snap agent only has to cycle through this subset of features rather than all the features in the database. Note, when the mouse next moves and the whole process begins all over again, the current cache of features is usually still relevant and does not need remaking. Only after the mouse has moved beyond the extent of the cache does it need to be refilled, and this is usually after many mouse moves.
Add a member variable to store your FeatureCache, and add a function to fill the cache. You will fill and use this cache in the Snap member below.
Privatem_FeatureCacheAsesriCarto.IFeatureCache ...Private SubFillCache(FClassAsIFeatureClass, pPointAsIPoint, DistanceAs Double) m_FeatureCache.Initialize pPoint, Distance m_FeatureCache.AddFeatures FClassEnd Sub
As you can see above, a FeatureCache can be automatically filled with features from a specified FeatureClass, based on a central point and a maximum distance. For more information about working with feature caches, see the Carto Library Reference.
The actual snapping behavior of the snap agent occurs in the Snap method. The Editor passes a point to this routine, which typically represents the current mouse location. You need to use this point to perform the snap.
If Notm_bIsOnThenISnapAgent_Snap =False Exit Function End If Ifm_pFeatureClassIs Nothing ThenISnapAgent_Snap =False Exit Function End If
DimdMinDistAs DoubledMinDist = Tolerance * 10Ifm_FeatureCacheIs Nothing Then Setm_FeatureCache =NewFeatureCacheEnd If If Notm_FeatureCache.Contains(Point)ThenFillCache m_pFeatureClass, Point, dMinDistEnd If
' pHitPoint will be used in the For loop below.DimpHitPointAsIPointSetpHitPoint =NewPoint' Loop thru all of the featuresDimpFeatureAsIFeatureDimpRowSubtypesAsIRowSubtypesDimpHitTestAsIHitTestDimbHasSnappedAs Boolean DimlPartIndexAs Long, lSegmentIndexAs Long, bRightSideAs Boolean DimdDistAs Double, dXAs Double, dYAs Double DimcountAs Integer Forcount = 0Tom_FeatureCache.count - 1SetpFeature = m_FeatureCache.Feature(count)SetpRowSubtypes = pFeature'QI' Only interrogate features that match subtype codeIfpRowSubtypes.SubtypeCode = m_lCodeThen SetpHitTest = pFeature.ShapeIf(pHitTest.HitTest(Point, Tolerance, esriGeometryPartBoundary, pHitPoint, dDist, lPartIndex, lSegmentIndex, bRightSide))Then IfdDist < dMinDistThenpHitPoint.QueryCoords dX, dY dMinDist = dDist bHasSnapped =True End If End If End If Nextcount
IfdMinDist >= ToleranceThenISnapAgent_Snap =False Exit Function End If
IfbHasSnappedThenPoint.PutCoords dX, dY ISnapAgent_Snap =True End If
The SnapText property should return a string indicating what was snapped to. You can return a string indicating the Object ID, Part, and Segment that was snapped to by writing a string with this information in the Snap member. Add a member variable to store the latest SnapText value, m_sSnapText, and edit Snap as shown.
IfdDist < dMinDistThenpHitPoint.QueryCoords dX, dY dMinDist = dDist bHasSnapped =Truem_sSnapText = "OID:" & pFeature.OID & "; Part:" & lPartIndex & "; Segment:" & lSegmentIndexEnd If...Private Property GetISnapAgentFeedback_SnapText()As StringISnapAgentFeedback_SnapText = m_sSnapTextEnd Property
Persistence functionality is essential for a snap agent. (If you are working in VC++ you should implement IPersist and IPersistStream; if working in VB, implement IPersistVariant.)
Snap agents must be persistable. See Chapter 2, 'Developing Objects', for general information on coding persistence methods.
Add a standard implementation of persistence to the SubtypesSnap agent. You may want to account for having an instantiated SnapAgent where its FeatureClass (and indeed other properties) has not been set yet, as shown below.
Private SubIPersistVariant_Save(ByValStreamAsesriSystem.IVariantStream) Stream.Write c_PersistVersion Stream.Write m_bIsOnIfm_pFeatureClassIs Nothing ThenStream.WriteFalse ElseStream.WriteTrue DimpDatasetAsesriGeoDatabase.IDatasetSetpDataset = m_pFeatureClass Stream.Write pDataset.FullName Stream.Write m_sName Stream.Write m_lCodeEnd If End Sub
In the Load method, read the boolean value to determine if there are a FeatureClass, Name, and Code to read or not.
Private SubIPersistVariant_Load(ByValStreamAsesriSystem.IVariantStream) ...DimhasFeatClassAs BooleanhasFeatClass = Stream.ReadIfhasFeatClassThen DimpNameAsesriSystem.INameSetpName = Stream.Read m_sName = Stream.Read m_lCode = Stream.ReadSetm_pFeatureClass = pName.OpenEnd If End Sub
The SubtypesSnap agent should be registered to the ESRI Snap Agents component category.
At this point, you have a working Snap Agent. However, writing a custom snap agent solves only half of the requirements outlined in the scenario. The problem also requires a mechanism for automatically creating and adding the snap agents to the editor's snap environment. Similarly, the custom snap agents have properties that must be set and a means for turning them on and off.
You can solve this problem with a custom editor extension that automatically creates a subtype snap agent for each subtype it finds in the edit session. The extension should additionally expose a custom dockable window to enable users to turn the snap agents on or off. To complete the customization, create a custom command to open and close the dockable window.
All editor extensions must implement the IExtension interface and be persistable. You can see a number of editor extension classes on the Editor Extension object model diagram.
Editor extensions do not implement IExtensionConfig (and, therefore, they do not show up in the Extensions dialog box), as the user is not expected to switch the extension on and off. Instead, each editor extension should be activated when an edit session begins and deactivated when the session ends.
You need to provide a mechanism for automatically creating and adding the subtype snap agents discussed above into the editor's snap environment and to set the feature class and subtype properties of each SubtypeSnap. The keyword in this scenario is "automatic". Commands need pressing; tools require interactivity; the only option for this case is a custom editor extension.
You will create an editor extension class called SnapExtension. In this case, the extension will be a client to the editor events OnStartEditing. Whenever an edit session is started, the extension will automatically create a new subtype snap agent for each subtype it finds in the edit session.
You will also create a dockable window, following a similar design to the Snapping Environment dockable window, to enable users to turn the individual SubtypeSnap agents on and off. To complete the customization, you will need to add a custom command to open and close the dockable window. You can find a discussion of how to implement these classes following this SnapExtension section.
Add to the project a form containing a Frame, which contains a ListBox. No code is required in the form class. This form will be used by the SnapExtension.
As mentioned earlier, when an editor extension is loaded, its IExtension::Startup routine is called and a reference to the Editor object is passed in via the initializationData parameter. In this method you will need to store a reference to the Editor object and also sink the IEditorEvents interface.
Private SubIExtension_Startup(initializationDataAs Variant)IfinitializationDataIs Nothing Then Exit Sub If Not TypeOfinitializationDataIsIEditorThen Exit Sub Setm_pEditor = initializationDataSetm_pSnapEnv = m_pEditorSetEditorEvents = m_pEditor
At this point, you need to instantiate the Snap Form used by the SnapDockableWindow to display the SubtypeSnap agents to the user. You can then find the SnapDockableWindow. The actions are performed in this order because the SnapDockableWindow will ask the SnapExtension for the window handle to the form, and therefore, the form needs to be instantiated before you find the dockable window. (See below for a discussion of the dockable window class.)
Setm_snapForm =NewSnapAgentVB.SnapForm Load m_snapFormSetListBoxEvents = m_snapForm.List1DimpUIDAs NewesriSystem.UID pUID.Value = "SnapAgentVB.SnapDockableWindow"DimpDockWinMgrAsesriFramework.IDockableWindowManagerSetpDockWinMgr = m_pEditor.ParentSetm_pDockWin = pDockWinMgr.GetDockableWindow(pUID)
The code above also shows that you will listen to events from the ListBox on the Snap Form so that the Extension itself can respond when a user makes changes in the selection of SubtypeSnap agents.
The IExtensionStartup method above begins listening to the IEditEvents interface. To activate your extension when the user starts editing, sink the OnStartEditing event. In this method, you need to set up the SubtypesSnap agents.
Private SubEditorEvents_OnStartEditing()' Don't bother looking for subtypes if the workspace is file basedIf Notm_pEditor.EditWorkspace.Type = esriFileSystemWorkspaceThen' Create an Array object that will locally manage the snap agentsSetm_pSnapAgentArray =NewesriSystem.Array SetUpEnd If End Sub
The internal method SetUp should set up a new SubtypeSnap agent for each subtype of each feature class in the map. Full details of the process which is used to create the snap agents can be found in the sample project code; however, the main points of this function are as follows:
If TypeOfpFeatureClassIsesriGeoDatabase.ISubtypesThen SetpSubtypes = pFeatureClassIfpSubtypes.HasSubtypeThenpMySet.Add pFeatureClassEnd If End If
Set pEnumSubtypes = pSubtypes.Subtypes
pEnumSubtypes.Reset
newSubtypeName = pEnumSubtypes.Next(newSubtypeCodeSetnewSnapAgent =NewSnapAgentVB.SubtypesSnapSetnewSnapAgent.FeatureClass = pSubtypes newSnapAgent.SubtypeCode = newSubtypeCode newSnapAgent.SubtypeName = newSubtypeNam
m_pSnapEnv.AddSnapAgent newSnapAgen
m_snapForm.List1.AddItem newSnapAgent.FeatureClass.AliasName + vbTab + newSnapAgent.SubtypeNameIfnewSnapAgent.IsOnThenm_snapForm.List1.Selected(m_snapForm.List1.ListCount - 1) =True End If
Privatem_pSnapAgentArrayAsesriSystem.IArray ... m_pSnapAgentArray.Add newSnapAgen
In the OnStopEditing method, clear the Snap Form of items. In the next edit session, there may be entirely different feature classes and subtypes. Also, hide the SnapDockableWindow (other editor windows, such as Snapping Environment and Attributes, are automatically hidden when the user stops the edit session.
Private SubEditorEvents_OnStopEditing(ByValSaveAs Boolean)If NotSaveThen If Notm_snapFormIs Nothing Thenm_snapForm.List1.ClearEnd If Ifm_pDockWin.IsVisibleThenm_pDockWin.ShowFalse End If End If End Sub
To respond to a user selecting and deselecting the snap agents in the dockable window, sink the ListboxEvents interface.
In the ItemChecked event, synchronize the listed snap agents' state with the state of the actual SubtypeSnap agent objects. If the listed agent is checked, make sure the corresponding SubtypeSnap agent is turned on.
Private SubListBoxEvents_ItemCheck(ItemAs Integer)DimpSubtypesSnapAgentAsSnapAgentVB.ISubtypesSnapAgentSetpSubtypesSnapAgent = m_pSnapAgentArray.Element(Item)IfpSubtypesSnapAgentIs Nothing Then Exit Sub Ifm_snapForm.List1.Selected(Item) =True ThenpSubtypesSnapAgent.IsOn =True ElsepSubtypesSnapAgent.IsOn =False End If End Sub
As discussed above under 'Implementing IExtension', the SnapDockableWindow needs to be able to get the window handle of the Snap Form m_snapForm from the SnapExtension. Also, the SnapDockableWindow will be made visible and invisible by the ShowSnapWindow command.
Create an interface called ISubtypesSnapExtension. Add a read-only property to identify whether the window SnapDockableWindow is visible and a method to toggle the visibility. Add another read-only property to return the window handle of the Snap Form.
The custom ISubtypesSnapExtension interface will allow the other classes in the component to show and hide the SnapDockableWindow.
Now implement ISubtypesSnapExtension in your SubtypesSnap class.
Private Property GetISubtypesSnapExtension_IsDialogVisible()As Boolean Ifm_pDockWinIs Nothing Then Exit PropertyISubtypesSnapExtension_IsDialogVisible = m_pDockWin.IsVisibleEnd Property Private SubISubtypesSnapExtension_ToggleDialogVisibility()Ifm_pDockWinIs Nothing Then Exit Subm_pDockWin.ShowNotm_pDockWin.IsVisibleEnd Sub Private Property GetISubtypesSnapExtension_SnapDialogHWnd()As Long Ifm_snapFormIs Nothing Then Exit PropertyISubtypesSnapExtension_SnapDialogHWnd = m_snapForm.List1.hWndEnd Property
The SnapExtension should be registered to the ESRI Editor Extensions component category. This will allow ArcMap to find the extension, instantiate it, and ensure it receives a reference to the Editor object.
Now the extension is complete, and you can create the remaining objects required.
To provide a mechanism for users to turn each SubtypeSnap agent on and off, create a subtype of the DockableWindow abstract class called SnapDockableWindow.
In the DockableWindowDef::OnCreate method, use the hook object passed in to find the Editor and, in turn, the SnapExtension editor extension, and store a reference to this extension.
Privatem_snapExtAsSnapAgentVB.ISubtypesSnapExtension ...Private SubIDockableWindowDef_OnCreate(ByValhookAs Object)DimpAppAsesriFramework.IApplicationSetpApp = hookDimpUIDAs NewesriSystem.UID pUID = "esriEditor.Editor"DimpEditorAsesriEditor.IEditorSetpEditor = pApp.FindExtensionByCLSID(pUID) pUID = "SnapAgentVB.SnapExtension"Setm_snapExt = pEditor.FindExtension(pUID)End Sub
Use this reference to return the handle of the Snap Form via the ISubtypesSnapExtension::SnapDialogHWND property from IDockableWindowDef_ChildHWND
Private Property GetIDockableWindowDef_ChildHWND()AsesriSystem.OLE_HANDLEIfm_snapExtIs Nothing Then Exit SubIDockableWindowDef_ChildHWND = m_snapExt.SnapDialogHWndEndProperty
Return strings from the Caption and Name properties to identify the dockable window. The Name property will be displayed on the title bar of the dockable window when it is undocked.
The SnapDockableWindow should be registered to the ESRI Mx Dockable Windows component category. This will allow the DockableWindowManager to find this dockable window and, in turn, allow your extension to find the SnapDockableWindow.
The last thing you need to complete this example is a command that can show and hide the SnapDockableWindow. Add a new class to your project called ShowSnapWindow and implement the ICommand interface in that class.
In the ICommand::OnCreate method, store references to the SnapExtension (as you did in IDockableWindowDef::OnCreate).
Privatem_snapExtAsSnapAgentVB.ISubtypesSnapExtensionPrivatem_pEditorAsesriEditor.IEditor ...Private SubICommand_OnCreate(ByValhookAs Object)If Not TypeOfhookIsesriArcMapUI.IMxApplicationThen Exit Sub Dim pApp As esriFramework.IApplication Set pApp = hook DimpDockWinMgrAsesriFramework.IDockableWindowManagerSetpDockWinMgr = hook' QI for IDockableWindowManagerDimpUIDAs NewesriSystem.UID pUID = "esriEditor.Editor"Setm_pEditor = pApp.FindExtensionByCLSID(pUID) pUID = "SnapAgentVB.SnapExtension"Setm_snapExt = m_pEditor.FindExtension(pUID)End Sub
When the command is clicked, change the visibility of the SnapDockableWindow by using the ISubtypesSnapExtension::ToggleDialogVisibility method.
Private SubICommand_OnClick() m_snapExt.ToggleDialogVisibilityEnd Sub
In the Checked property, indicate the current state of the dockable window.
Private Property GetICommand_Checked()As BooleanICommand_Checked = m_snapExt.IsDialogVisibleEnd Property
Return the Enabled state based on the EditState of the Editor.
Private Property GetICommand_Enabled()As BooleanICommand_Enabled = (m_pEditor.EditState = esriStateEditing)End Property
The ShowSnapWindow command should be registered to the ESRI Mx Commands component category. This will allow users to add the command to a command bar as required.
Now that all the members of the SubtypesSnap example are complete, compile the component, make sure the classes are registered to their appropriate component categories, and use the example as described in the overview at the beginning of this topic.
Go to example code
See Also Extending the Editing Framework, About Snap Agents, and About Editor Extensions.